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/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/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/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/main/resources/assets/viabedrock/skin_packs/vanilla.mcpack b/src/main/resources/assets/viabedrock/skin_packs/vanilla.mcpack index b7929951b..995166202 100644 Binary files a/src/main/resources/assets/viabedrock/skin_packs/vanilla.mcpack and b/src/main/resources/assets/viabedrock/skin_packs/vanilla.mcpack differ 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); + } + } + +} 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()); + } + } + +} 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(); + } + } + +} 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; + } + +}