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;
+ }
+
+}