From 075cbe60bbf746f719b7dc717528494f051f38a3 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:13:51 +0100 Subject: [PATCH 1/4] fix(network): consume uncompressed Bedrock frame payloads The 26.60 branch translates the uint8 frame sentinel correctly but still retains the payload without advancing the decoder buffer. Netty can then interpret payload bytes as another compression header and fail decoding. Emit a retained slice that consumes the complete payload. Cover direct and negotiated no-compression frames with real EmbeddedChannel tests. Add the JUnit test runtime without duplicating compile-only dependency versions. Both regressions fail with the upstream decoder. --- build.gradle | 11 +++ .../viabedrock/netty/CompressionCodec.java | 3 +- .../netty/CompressionCodecTest.java | 77 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/raphimc/viabedrock/netty/CompressionCodecTest.java diff --git a/build.gradle b/build.gradle index 8a16054d7..891865a2a 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,10 @@ repositories { } } +configurations { + testImplementation.extendsFrom compileOnly +} + dependencies { compileOnly "com.viaversion:viaversion-common:5.12.0" compileOnly "org.yaml:snakeyaml:2.7" @@ -101,6 +105,13 @@ dependencies { toolImplementation "org.jsoup:jsoup:1.23.2" toolImplementation "org.ow2.asm:asm-tree:9.11.1" toolImplementation "net.lenni0451.commons:asm:1.9.2" + + testImplementation "org.junit.jupiter:junit-jupiter:6.1.3" + testRuntimeOnly "org.junit.platform:junit-platform-launcher:6.1.3" +} + +test { + useJUnitPlatform() } def registerToolTask(String name, String toolMainClass, String toolDescription) { diff --git a/src/main/java/net/raphimc/viabedrock/netty/CompressionCodec.java b/src/main/java/net/raphimc/viabedrock/netty/CompressionCodec.java index a0bfdd37c..4ef6cf68d 100644 --- a/src/main/java/net/raphimc/viabedrock/netty/CompressionCodec.java +++ b/src/main/java/net/raphimc/viabedrock/netty/CompressionCodec.java @@ -86,6 +86,7 @@ protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final L } final int rawAlgorithm = in.readUnsignedByte(); + // NetworkSettings uses a uint16 sentinel; a compressed frame has only a uint8 header. final PacketCompressionAlgorithm algorithm = rawAlgorithm == 0xFF ? PacketCompressionAlgorithm.None : PacketCompressionAlgorithm.getByValue(rawAlgorithm); if (algorithm == null) { // Bedrock client drops the packet if the algorithm is unknown ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Dropping packet with unknown PacketCompressionAlgorithm: " + rawAlgorithm); @@ -95,7 +96,7 @@ protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final L final CompressionAlgorithm compressionAlgorithm = this.getCompressionAlgorithm(algorithm); if (compressionAlgorithm instanceof NoopCompression) { - out.add(in.retain()); + out.add(in.readRetainedSlice(in.readableBytes())); } else { final ByteBuf uncompressedData = ctx.alloc().buffer(); compressionAlgorithm.decompress(in, uncompressedData); // Bedrock client would drop packets with invalid data, but this would be too insane to do diff --git a/src/test/java/net/raphimc/viabedrock/netty/CompressionCodecTest.java b/src/test/java/net/raphimc/viabedrock/netty/CompressionCodecTest.java new file mode 100644 index 000000000..3893779b5 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/netty/CompressionCodecTest.java @@ -0,0 +1,77 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.viabedrock.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.PacketCompressionAlgorithm; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +final class CompressionCodecTest { + + @Test + void consumesAnUncompressedFrameWithoutDecodingItsPayloadAgain() { + final var channel = new EmbeddedChannel(new CompressionCodec(PacketCompressionAlgorithm.None, 0)); + try { + assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(new byte[]{(byte) 0xFF, 1, 3, 5, 7}))); + final ByteBuf decoded = channel.readInbound(); + try { + assertEquals(4, decoded.readableBytes()); + assertEquals(0x01030507, decoded.readInt()); + } finally { + decoded.release(); + } + assertNull(channel.readInbound()); + } finally { + channel.finishAndReleaseAll(); + } + } + + @Test + void readsTheByteFrameSentinelAfterNegotiatingTheUnsignedShortSentinel() { + final ByteBuf settings = Unpooled.wrappedBuffer(new byte[]{(byte) 0xFF, (byte) 0xFF}); + final PacketCompressionAlgorithm negotiated = PacketCompressionAlgorithm.getByValue(settings.readUnsignedShortLE()); + settings.release(); + final var channel = new EmbeddedChannel(new CompressionCodec(negotiated, 0)); + try { + assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(new byte[]{(byte) 0xFF, 1, 3, 5, 7}))); + final ByteBuf decoded = channel.readInbound(); + try { + assertEquals(4, decoded.readableBytes()); + assertEquals(0x01030507, decoded.readInt()); + } finally { + decoded.release(); + } + assertTrue(channel.writeOutbound(Unpooled.wrappedBuffer(new byte[]{2, 4, 6, 8}))); + final ByteBuf encoded = channel.readOutbound(); + try { + assertEquals(5, encoded.readableBytes()); + assertEquals(0xFF, encoded.readUnsignedByte()); + assertEquals(0x02040608, encoded.readInt()); + } finally { + encoded.release(); + } + } finally { + channel.finishAndReleaseAll(); + } + } + +} From 16d5b5ad04b2154f87b602f34939a8568622e55d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:13:51 +0100 Subject: [PATCH 2/4] fix(blocks): preserve explicit connection states during upgrades The 26.60 palette retains block-state version 1.21.60.33 while adding connection properties. Its added-properties schema also runs on current redstone, fire and chorus states, replacing supplied connections with legacy defaults. Add a property only when it is missing. Cover explicit redstone direction, fire and chorus domains, legacy defaults and repeated upgrades. The regression changes redstone north from up to none on the upstream code. --- .../JsonBlockStateUpgradeSchema.java | 4 +- .../JsonBlockStateUpgradeSchemaTest.java | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchemaTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchema.java b/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchema.java index 943e631d0..5d12c68f7 100644 --- a/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchema.java +++ b/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchema.java @@ -212,7 +212,9 @@ public JsonBlockStateUpgradeSchema(final JsonObject jsonObject) { if (tag.get("states") instanceof CompoundTag states) { for (Pair property : toAdd) { - states.put(property.key(), NbtUtil.createTag(property.value())); + if (!states.contains(property.key())) { + states.put(property.key(), NbtUtil.createTag(property.value())); + } } } }); diff --git a/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchemaTest.java b/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchemaTest.java new file mode 100644 index 000000000..29133c2bf --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/JsonBlockStateUpgradeSchemaTest.java @@ -0,0 +1,54 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package net.raphimc.viabedrock.api.chunk.blockstate; + +import com.viaversion.nbt.tag.CompoundTag; +import com.viaversion.viaversion.libs.gson.JsonParser; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +final class JsonBlockStateUpgradeSchemaTest { + + @Test + void addsLegacyConnectionsWithoutReplacingCurrentConnections() throws Exception { + final JsonBlockStateUpgradeSchema schema; + try (var input = getClass().getResourceAsStream("/assets/viabedrock/block_state_upgrade_schema/0361_1.26.50_to_1.26.60.json")) { + assertNotNull(input); + schema = new JsonBlockStateUpgradeSchema(JsonParser.parseString(new String(input.readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject()); + } + for (final String name : new String[]{"minecraft:redstone_wire", "minecraft:fire", "minecraft:chorus_plant"}) { + final var tag = new CompoundTag(); + tag.putString("name", name); + tag.putInt("version", 1 << 24 | 21 << 16 | 60 << 8 | 33); + final var states = new CompoundTag(); + final boolean redstone = name.equals("minecraft:redstone_wire"); + if (redstone) { + states.putString("redstone_north", "up"); + states.putInt("redstone_signal", 12); + } else { + states.putByte("minecraft:connection_north", (byte) 1); + } + tag.put("states", states); + schema.upgrade(tag); + if (redstone) { + assertEquals("up", states.getStringTag("redstone_north").getValue()); + assertEquals("none", states.getStringTag("redstone_east").getValue()); + assertEquals(12, states.getIntTag("redstone_signal").asInt()); + } else { + assertEquals(1, states.getByteTag("minecraft:connection_north").asByte()); + assertEquals(0, states.getByteTag("minecraft:connection_east").asByte()); + assertEquals(name.equals("minecraft:chorus_plant"), states.contains("minecraft:connection_down")); + } + final var once = tag.copy(); + schema.upgrade(tag); + assertEquals(once, tag); + } + } + +} From 6632a2bd3a7d1cc812ee80607b9ecffddc031689 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:13:51 +0100 Subject: [PATCH 3/4] fix(skins): restore the bundled vanilla login skin archive The regenerated vanilla skin archive contains only LICENSE. ResourcePack rejects it because manifest.json is missing, and the default login skin provider cannot find its pack, Steve texture or geometry. Restore the complete archive from the validated 1.26.60.5 GDK snapshot. The 13 resource files match the original package and preserve the Mojang license. Verify pack parsing, Steve image decoding and matching humanoid geometry dimensions against the actual bundled archive. --- .../viabedrock/skin_packs/vanilla.mcpack | Bin 322 -> 12882 bytes .../api/resourcepack/VanillaSkinPackTest.java | 30 ++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/VanillaSkinPackTest.java diff --git a/src/main/resources/assets/viabedrock/skin_packs/vanilla.mcpack b/src/main/resources/assets/viabedrock/skin_packs/vanilla.mcpack index b7929951bc996bdeba044e80a5f63f2fc66b0d61..995166202b27d59cfca57c6f53c9ea6d724e44d7 100644 GIT binary patch literal 12882 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 E2g({$TmS$7 delta 35 kcmcbVa)@cdhskUPe3Q@W8Hlj5F#rK05ZVFhcOVV}0KF#%T>t<8 diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/VanillaSkinPackTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/VanillaSkinPackTest.java new file mode 100644 index 000000000..fda770862 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/VanillaSkinPackTest.java @@ -0,0 +1,30 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package net.raphimc.viabedrock.api.resourcepack; + +import net.raphimc.viabedrock.api.resourcepack.content.ZipContent; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +final class VanillaSkinPackTest { + + @Test + void loadsTheBundledSkinAndGeometryUsedDuringLogin() throws Exception { + try (var input = getClass().getResourceAsStream("/assets/viabedrock/skin_packs/vanilla.mcpack")) { + assertNotNull(input); + final var pack = new ResourcePack(new ZipContent(input.readAllBytes())); + final var image = pack.content().getImage("steve.png").getImage(); + final var geometry = pack.content().getSortedJson("geometry.json").getAsJsonArray("minecraft:geometry").asList().stream() + .map(element -> element.getAsJsonObject().getAsJsonObject("description")) + .filter(description -> description.get("identifier").getAsString().equals("geometry.humanoid.custom")) + .findFirst().orElseThrow(); + assertEquals(image.getWidth(), geometry.get("texture_width").getAsInt()); + assertEquals(image.getHeight(), geometry.get("texture_height").getAsInt()); + } + } + +} From 6674651c0851f0ec80675b64944d0df78f9d4b26 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:13:51 +0100 Subject: [PATCH 4/4] fix(dimensions): apply synchronized Bedrock cloud settings Protocol 2225 supplies dimension cloud height and visibility, but the current decoder discards both. Retain them with the minimum Y and height range, then project them through Java 26.3 environment attributes. Preserve the existing Overworld override boundary and other authored attributes. Transparent cloud color suppresses clouds because the Java day timeline multiplies that color and preserves zero alpha. Reject nonpositive ranges and upper-bound overflow. Six regressions cover wire boundaries, signed bounds and attribute projection. Mojang protocol-2225 metadata and its cloud-synchronization notes establish the new fields. The earlier native 1.26.51.1 Hive capture establishes the minimum-Y plus height-range semantics. This change includes no inventory prerequisites or new claim of native visual parity. --- .../protocol/data/JavaRegistries.java | 27 ++++-- .../protocol/model/DimensionDefinition.java | 33 +++++++ .../protocol/packet/JoinPackets.java | 27 +++--- .../protocol/storage/GameSessionStorage.java | 11 +-- .../data/JavaDimensionRegistryTest.java | 55 +++++++++++ .../packet/DimensionDataPacketTest.java | 96 +++++++++++++++++++ 6 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/DimensionDefinition.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/data/JavaDimensionRegistryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/DimensionDataPacketTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java b/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java index ea3cd9011..d850863cf 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/data/JavaRegistries.java @@ -20,7 +20,6 @@ import com.viaversion.nbt.tag.CompoundTag; import com.viaversion.nbt.tag.ListTag; import com.viaversion.nbt.tag.StringTag; -import com.viaversion.viaversion.libs.fastutil.ints.IntIntPair; import com.viaversion.viaversion.util.Key; import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.api.resourcepack.definition.BiomeDefinitions; @@ -29,6 +28,7 @@ import net.raphimc.viabedrock.protocol.BedrockProtocol; import net.raphimc.viabedrock.protocol.data.enums.Dimension; import net.raphimc.viabedrock.protocol.data.generated.java.RegistryKeys; +import net.raphimc.viabedrock.protocol.model.DimensionDefinition; import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; @@ -62,13 +62,24 @@ private static void modifyDimensionRegistry(final GameSessionStorage gameSession dimensionRegistry.getCompoundTag(Dimension.OVERWORLD.getKey()).putInt("height", 256); dimensionRegistry.getCompoundTag(Dimension.OVERWORLD.getKey()).putInt("logical_height", 256); } - for (Map.Entry entry : gameSession.getBedrockDimensionDefinitions().entrySet()) { - final CompoundTag dimensionTag = new CompoundTag(); - final int height = entry.getValue().rightInt() - entry.getValue().leftInt(); - dimensionTag.putInt("min_y", entry.getValue().leftInt()); - dimensionTag.putInt("height", height); - dimensionTag.putInt("logical_height", height); - dimensionRegistry.getCompoundTag(entry.getKey()).putAll(dimensionTag); + for (Map.Entry entry : gameSession.getBedrockDimensionDefinitions().entrySet()) { + applyDimensionDefinition(dimensionRegistry.getCompoundTag(entry.getKey()), entry.getValue()); + } + } + + static void applyDimensionDefinition(final CompoundTag dimensionTag, final DimensionDefinition definition) { + dimensionTag.putInt("min_y", definition.minimumY()); + dimensionTag.putInt("height", definition.height()); + dimensionTag.putInt("logical_height", definition.height()); + CompoundTag attributes = dimensionTag.getCompoundTag("attributes"); + if (attributes == null) { + attributes = new CompoundTag(); + dimensionTag.put("attributes", attributes); + } + attributes.putFloat("minecraft:visual/cloud_height", definition.cloudHeight()); + if (!definition.renderClouds()) { + // Java's day timeline multiplies cloud color, preserving zero alpha. + attributes.putString("minecraft:visual/cloud_color", "#00ffffff"); } } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/DimensionDefinition.java b/src/main/java/net/raphimc/viabedrock/protocol/model/DimensionDefinition.java new file mode 100644 index 000000000..351421c8f --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/DimensionDefinition.java @@ -0,0 +1,33 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.viabedrock.protocol.model; + +public record DimensionDefinition(int minimumY, int height, int cloudHeight, boolean renderClouds) { + + public DimensionDefinition { + if (height <= 0) { + throw new IllegalArgumentException("Dimension height range must be positive"); + } + Math.addExact(minimumY, height); + } + + public int maximumY() { + return this.minimumY + this.height; + } + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java index 4f9cc6a83..2e456cd7d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -31,7 +31,6 @@ import com.viaversion.viaversion.api.protocol.remapper.PacketHandlers; import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import com.viaversion.viaversion.api.type.Types; -import com.viaversion.viaversion.libs.fastutil.ints.IntIntImmutablePair; import com.viaversion.viaversion.protocols.base.ClientboundLoginPackets; import com.viaversion.viaversion.protocols.base.v1_7.ClientboundBaseProtocol1_7; import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundConfigurationPackets26_3; @@ -55,6 +54,7 @@ import net.raphimc.viabedrock.protocol.data.enums.java.Relative; import net.raphimc.viabedrock.protocol.data.enums.java.generated.PlayerInfoUpdateAction; import net.raphimc.viabedrock.protocol.data.generated.java.Attributes; +import net.raphimc.viabedrock.protocol.model.DimensionDefinition; import net.raphimc.viabedrock.protocol.model.*; import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; @@ -434,16 +434,9 @@ protected void register() { final int count = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // entry count for (int i = 0; i < count; i++) { final String dimensionIdentifier = wrapper.read(BedrockTypes.STRING); // dimension identifier - final int minimumHeight = wrapper.read(BedrockTypes.VAR_INT); // minimum height - final int maximumHeight = minimumHeight + wrapper.read(BedrockTypes.VAR_INT); // maximum height - wrapper.read(BedrockTypes.VAR_INT); // generator type - wrapper.read(BedrockTypes.VAR_INT); // dimension type - wrapper.read(BedrockTypes.UUID); // pack id - wrapper.read(BedrockTypes.STRING); // default biome - wrapper.read(BedrockTypes.VAR_INT); // cloud height - wrapper.read(Types.BOOLEAN); // render clouds - if (dimensionIdentifier.equals(Dimension.OVERWORLD.getKey())) { // Bedrock client currently only supports overworld - gameSession.putBedrockDimensionDefinition(dimensionIdentifier, new IntIntImmutablePair(minimumHeight, maximumHeight)); + final DimensionDefinition definition = readDimensionDefinition(wrapper); + if (dimensionIdentifier.equals(Dimension.OVERWORLD.getKey())) { // Only Overworld overrides currently map to Java + gameSession.putBedrockDimensionDefinition(dimensionIdentifier, definition); } } }, State.PLAY, (PacketHandler) PacketWrapper::cancel // Bedrock client ignores dimension data after start game @@ -636,6 +629,18 @@ private static void handleJavaClientGameJoin(final UserConnection user) { setTime.send(BedrockProtocol.class, false); } + static DimensionDefinition readDimensionDefinition(final PacketWrapper wrapper) { + final int minimumY = wrapper.read(BedrockTypes.VAR_INT); + final int heightRange = wrapper.read(BedrockTypes.VAR_INT); + wrapper.read(BedrockTypes.VAR_INT); // generator type + wrapper.read(BedrockTypes.VAR_INT); // dimension type + wrapper.read(BedrockTypes.UUID); // pack id + wrapper.read(BedrockTypes.STRING); // default biome + final int cloudHeight = wrapper.read(BedrockTypes.VAR_INT); + final boolean renderClouds = wrapper.read(Types.BOOLEAN); + return new DimensionDefinition(minimumY, heightRange, cloudHeight, renderClouds); + } + private JoinPackets() { } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java index 7589ed5ef..b86fa9903 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java @@ -21,10 +21,9 @@ import com.viaversion.nbt.tag.CompoundTag; import com.viaversion.viaversion.api.connection.StoredObject; import com.viaversion.viaversion.api.connection.UserConnection; -import com.viaversion.viaversion.libs.fastutil.ints.IntIntImmutablePair; -import com.viaversion.viaversion.libs.fastutil.ints.IntIntPair; import com.viaversion.viaversion.libs.mcstructs.text.TextComponent; import net.raphimc.viabedrock.protocol.BedrockProtocol; +import net.raphimc.viabedrock.protocol.model.DimensionDefinition; import net.raphimc.viabedrock.protocol.data.JavaRegistries; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ChatRestrictionLevel; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.GameType; @@ -38,7 +37,7 @@ public class GameSessionStorage extends StoredObject { private CompoundTag javaRegistries; private CompoundTag bedrockBiomeDefinitions = BedrockProtocol.MAPPINGS.getBedrockBiomeDefinitions(); - private final Map bedrockDimensionDefinitions = new HashMap<>(); + private final Map bedrockDimensionDefinitions = new HashMap<>(); private final Set availableEntityIdentifiers = new HashSet<>(BedrockProtocol.MAPPINGS.getBedrockEntities().keySet()); private Semver bedrockVanillaVersion; private boolean flatGenerator; @@ -57,7 +56,7 @@ public class GameSessionStorage extends StoredObject { public GameSessionStorage(final UserConnection user) { super(user); - this.bedrockDimensionDefinitions.put("minecraft:the_nether", new IntIntImmutablePair(0, 128)); + this.bedrockDimensionDefinitions.put("minecraft:the_nether", new DimensionDefinition(0, 128, 0, false)); } public CompoundTag getJavaRegistries() { @@ -76,11 +75,11 @@ public void setBedrockBiomeDefinitions(final CompoundTag bedrockBiomeDefinitions this.javaRegistries = null; } - public Map getBedrockDimensionDefinitions() { + public Map getBedrockDimensionDefinitions() { return this.bedrockDimensionDefinitions; } - public void putBedrockDimensionDefinition(final String dimensionIdentifier, final IntIntPair dimensionDefinition) { + public void putBedrockDimensionDefinition(final String dimensionIdentifier, final DimensionDefinition dimensionDefinition) { this.bedrockDimensionDefinitions.put(dimensionIdentifier, dimensionDefinition); this.javaRegistries = null; } diff --git a/src/test/java/net/raphimc/viabedrock/protocol/data/JavaDimensionRegistryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/data/JavaDimensionRegistryTest.java new file mode 100644 index 000000000..06add2ea7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/data/JavaDimensionRegistryTest.java @@ -0,0 +1,55 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package net.raphimc.viabedrock.protocol.data; + +import com.viaversion.nbt.tag.CompoundTag; +import net.raphimc.viabedrock.protocol.model.DimensionDefinition; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class JavaDimensionRegistryTest { + + @Test + void projectsServerBoundsAndCloudHeightWithoutReplacingOtherAttributes() { + final var registry = registry(); + final var dimension = registry.getCompoundTag("minecraft:overworld"); + JavaRegistries.applyDimensionDefinition(dimension, new DimensionDefinition(-64, 384, 96, true)); + assertEquals(-64, dimension.getInt("min_y")); + assertEquals(384, dimension.getInt("height")); + assertEquals(384, dimension.getInt("logical_height")); + final var attributes = dimension.getCompoundTag("attributes"); + assertEquals(96F, attributes.getFloat("minecraft:visual/cloud_height")); + assertEquals("#ccffffff", attributes.getString("minecraft:visual/cloud_color")); + assertEquals("#123456", attributes.getString("minecraft:visual/fog_color")); + } + + @Test + void suppressesCloudsWithTransparentColorAndAcceptsAnAbsentAttributeMap() { + for (final boolean attributesPresent : new boolean[]{false, true}) { + final var registry = registry(); + final var dimension = registry.getCompoundTag("minecraft:overworld"); + if (!attributesPresent) { + dimension.remove("attributes"); + } + JavaRegistries.applyDimensionDefinition(dimension, new DimensionDefinition(0, 256, 128, false)); + assertEquals("#00ffffff", dimension.getCompoundTag("attributes").getString("minecraft:visual/cloud_color")); + assertEquals(128F, dimension.getCompoundTag("attributes").getFloat("minecraft:visual/cloud_height")); + } + } + + private static CompoundTag registry() { + final var registry = new CompoundTag(); + final var dimension = new CompoundTag(); + final var attributes = new CompoundTag(); + attributes.putString("minecraft:visual/cloud_color", "#ccffffff"); + attributes.putString("minecraft:visual/fog_color", "#123456"); + dimension.put("attributes", attributes); + registry.put("minecraft:overworld", dimension); + return registry; + } + +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/DimensionDataPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/DimensionDataPacketTest.java new file mode 100644 index 000000000..bc354000a --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/DimensionDataPacketTest.java @@ -0,0 +1,96 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package net.raphimc.viabedrock.protocol.packet; + +import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; +import net.raphimc.viabedrock.protocol.types.BedrockTypes; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class DimensionDataPacketTest { + + @Test + void retainsBoundsAndCloudSettingsWithoutConsumingTheNextDefinition() { + for (final int[] range : new int[][]{{0, 256}, {-64, 384}, {32, 64}}) { + for (final boolean clouds : new boolean[]{false, true}) { + final ByteBuf buffer = definition(range[0], range[1], -32, clouds); + try { + buffer.writeByte(77); + final var wrapper = new PacketWrapperImpl(ClientboundBedrockPackets.DIMENSION_DATA, buffer, null); + final var decoded = JoinPackets.readDimensionDefinition(wrapper); + assertEquals(range[0], decoded.minimumY()); + assertEquals(range[1], decoded.height()); + assertEquals(range[0] + range[1], decoded.maximumY()); + assertEquals(-32, decoded.cloudHeight()); + assertEquals(clouds, decoded.renderClouds()); + assertEquals(77, buffer.readUnsignedByte()); + assertFalse(buffer.isReadable()); + } finally { + buffer.release(); + } + } + } + } + + @Test + void rejectsEmptyAndNegativeHeightRanges() { + for (final int range : new int[]{0, -16, Integer.MIN_VALUE}) { + final ByteBuf buffer = definition(-64, range, 192, true); + try { + final var wrapper = new PacketWrapperImpl(ClientboundBedrockPackets.DIMENSION_DATA, buffer, null); + assertThrows(IllegalArgumentException.class, () -> JoinPackets.readDimensionDefinition(wrapper)); + } finally { + buffer.release(); + } + } + } + + @Test + void rejectsUpperBoundOverflowInsteadOfWrappingTheDimension() { + final ByteBuf buffer = definition(Integer.MAX_VALUE - 15, 16, 0, false); + try { + final var wrapper = new PacketWrapperImpl(ClientboundBedrockPackets.DIMENSION_DATA, buffer, null); + assertThrows(ArithmeticException.class, () -> JoinPackets.readDimensionDefinition(wrapper)); + } finally { + buffer.release(); + } + } + + @Test + void retainsRepresentableBoundsAtTheSignedIntegerLimits() { + for (final int minimum : new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE - 16}) { + final ByteBuf buffer = definition(minimum, 16, 128, true); + try { + final var wrapper = new PacketWrapperImpl(ClientboundBedrockPackets.DIMENSION_DATA, buffer, null); + final var decoded = JoinPackets.readDimensionDefinition(wrapper); + assertEquals(minimum, decoded.minimumY()); + assertEquals(minimum + 16, decoded.maximumY()); + } finally { + buffer.release(); + } + } + } + + private static ByteBuf definition(final int minimum, final int range, final int cloudHeight, final boolean clouds) { + final ByteBuf buffer = Unpooled.buffer(); + BedrockTypes.VAR_INT.writePrimitive(buffer, minimum); + BedrockTypes.VAR_INT.writePrimitive(buffer, range); + BedrockTypes.VAR_INT.writePrimitive(buffer, 1); // generator + BedrockTypes.VAR_INT.writePrimitive(buffer, 0); // dimension type + BedrockTypes.UUID.write(buffer, UUID.randomUUID()); + BedrockTypes.STRING.write(buffer, "minecraft:plains"); + BedrockTypes.VAR_INT.writePrimitive(buffer, cloudHeight); + buffer.writeBoolean(clouds); + return buffer; + } + +}