From e6a93ad0c6f15c91e5be0bec6916d94fcf663d06 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sat, 21 Aug 2021 02:17:34 -0400 Subject: [PATCH] Clean up JoinGame packet encoding/decoding Specifically, the JoinGame packet handling is now split between pre-1.16 encodings of the packet and post-1.16 handlings of the packet. This packet is one of the most amorphous packets in the entire Minecraft protocol, from Velocity's perspective. --- .../netty/MinecraftVarintFrameDecoder.java | 6 +- .../protocol/netty/VarintByteDecoder.java | 12 +- .../proxy/protocol/packet/JoinGame.java | 214 +++++++++++------- 3 files changed, 145 insertions(+), 87 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java index d81f53f5b..c1596e4e2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java @@ -44,6 +44,10 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { if (varintEnd == -1) { // We tried to go beyond the end of the buffer. This is probably a good sign that the // buffer was too short to hold a proper varint. + if (reader.getResult() == DecodeResult.RUN_OF_ZEROES) { + // Special case where the entire packet is just a run of zeroes. We ignore them all. + in.clear(); + } return; } @@ -54,7 +58,7 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { in.clear(); throw BAD_LENGTH_CACHED; } else if (readVarint == 0) { - // skip over the empty packet and ignore it + // skip over the empty packet(s) and ignore it in.readerIndex(varintEnd + 1); } else { int minimumRead = bytesRead + readVarint; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java index c50672f18..c6fb89b5d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java @@ -27,6 +27,15 @@ class VarintByteDecoder implements ByteProcessor { @Override public boolean process(byte k) { + if (k == 0 && bytesRead == 0) { + // tentatively say it's invalid, but there's a possibility of redemption + result = DecodeResult.RUN_OF_ZEROES; + return true; + } + if (result == DecodeResult.RUN_OF_ZEROES) { + result = DecodeResult.SUCCESS; + return false; + } readVarint |= (k & 0x7F) << bytesRead++ * 7; if (bytesRead > 3) { result = DecodeResult.TOO_BIG; @@ -54,6 +63,7 @@ class VarintByteDecoder implements ByteProcessor { public enum DecodeResult { SUCCESS, TOO_SHORT, - TOO_BIG + TOO_BIG, + RUN_OF_ZEROES } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index 9899ef0cb..26a293a1a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -99,7 +99,7 @@ public class JoinGame implements MinecraftPacket { return levelType; } - public void setLevelType(String levelType) { + public void setLevelType(@Nullable String levelType) { this.levelType = levelType; } @@ -183,43 +183,22 @@ public class JoinGame implements MinecraftPacket { @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - this.entityId = buf.readInt(); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - this.isHardcore = buf.readBoolean(); - this.gamemode = buf.readByte(); - } else { - this.gamemode = buf.readByte(); - this.isHardcore = (this.gamemode & 0x08) != 0; - this.gamemode &= ~0x08; - } - String dimensionIdentifier = null; - String levelName = null; if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - this.previousGamemode = buf.readByte(); - ImmutableSet levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); - CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); - ListBinaryTag dimensionRegistryContainer = null; - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type") - .getList("value", BinaryTagTypes.COMPOUND); - this.biomeRegistry = registryContainer.getCompound("minecraft:worldgen/biome"); - } else { - dimensionRegistryContainer = registryContainer.getList("dimension", - BinaryTagTypes.COMPOUND); - } - ImmutableSet readData = - DimensionRegistry.fromGameData(dimensionRegistryContainer, version); - this.dimensionRegistry = new DimensionRegistry(readData, levelNames); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); - dimensionIdentifier = ProtocolUtils.readString(buf); - this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version) - .annotateWith(dimensionIdentifier, null); - } else { - dimensionIdentifier = ProtocolUtils.readString(buf); - levelName = ProtocolUtils.readString(buf); - } - } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) { + // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, + // so separate it out. + this.decode116Up(buf, version); + } else { + this.decodeLegacy(buf, version); + } + } + + private void decodeLegacy(ByteBuf buf, ProtocolVersion version) { + this.entityId = buf.readInt(); + this.gamemode = buf.readByte(); + this.isHardcore = (this.gamemode & 0x08) != 0; + this.gamemode &= ~0x08; + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) { this.dimension = buf.readInt(); } else { this.dimension = buf.readByte(); @@ -230,14 +209,8 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { this.partialHashedSeed = buf.readLong(); } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - this.maxPlayers = ProtocolUtils.readVarInt(buf); - } else { - this.maxPlayers = buf.readUnsignedByte(); - } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) < 0) { - this.levelType = ProtocolUtils.readString(buf, 16); - } + this.maxPlayers = buf.readUnsignedByte(); + this.levelType = ProtocolUtils.readString(buf, 16); if (version.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) { this.viewDistance = ProtocolUtils.readVarInt(buf); } @@ -247,15 +220,76 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { this.showRespawnScreen = buf.readBoolean(); } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - boolean isDebug = buf.readBoolean(); - boolean isFlat = buf.readBoolean(); - this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); + } + + private void decode116Up(ByteBuf buf, ProtocolVersion version) { + this.entityId = buf.readInt(); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + this.isHardcore = buf.readBoolean(); + this.gamemode = buf.readByte(); + } else { + this.gamemode = buf.readByte(); + this.isHardcore = (this.gamemode & 0x08) != 0; + this.gamemode &= ~0x08; } + + this.previousGamemode = buf.readByte(); + + ImmutableSet levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); + + CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); + ListBinaryTag dimensionRegistryContainer; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type") + .getList("value", BinaryTagTypes.COMPOUND); + this.biomeRegistry = registryContainer.getCompound("minecraft:worldgen/biome"); + } else { + dimensionRegistryContainer = registryContainer.getList("dimension", + BinaryTagTypes.COMPOUND); + } + ImmutableSet readData = + DimensionRegistry.fromGameData(dimensionRegistryContainer, version); + this.dimensionRegistry = new DimensionRegistry(readData, levelNames); + + String dimensionIdentifier; + String levelName = null; + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER); + dimensionIdentifier = ProtocolUtils.readString(buf); + this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version) + .annotateWith(dimensionIdentifier, null); + } else { + dimensionIdentifier = ProtocolUtils.readString(buf); + levelName = ProtocolUtils.readString(buf); + } + + this.partialHashedSeed = buf.readLong(); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + this.maxPlayers = ProtocolUtils.readVarInt(buf); + } else { + this.maxPlayers = buf.readUnsignedByte(); + } + + this.viewDistance = ProtocolUtils.readVarInt(buf); + this.reducedDebugInfo = buf.readBoolean(); + this.showRespawnScreen = buf.readBoolean(); + boolean isDebug = buf.readBoolean(); + boolean isFlat = buf.readBoolean(); + this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, + // so separate it out. + this.encode116Up(buf, version); + } else { + this.encodeLegacy(buf, version); + } + } + + private void encodeLegacy(ByteBuf buf, ProtocolVersion version) { buf.writeInt(entityId); if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { buf.writeBoolean(isHardcore); @@ -263,29 +297,7 @@ public class JoinGame implements MinecraftPacket { } else { buf.writeByte(isHardcore ? gamemode | 0x8 : gamemode); } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - buf.writeByte(previousGamemode); - ProtocolUtils.writeStringArray(buf, dimensionRegistry.getLevelNames().toArray(new String[0])); - CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder(); - ListBinaryTag encodedDimensionRegistry = dimensionRegistry.encodeRegistry(version); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder(); - dimensionRegistryEntry.putString("type", "minecraft:dimension_type"); - dimensionRegistryEntry.put("value", encodedDimensionRegistry); - registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build()); - registryContainer.put("minecraft:worldgen/biome", biomeRegistry); - } else { - registryContainer.put("dimension", encodedDimensionRegistry); - } - ProtocolUtils.writeCompoundTag(buf, registryContainer.build()); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails()); - ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); - } else { - ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); - ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); - } - } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) { buf.writeInt(dimension); } else { buf.writeByte(dimension); @@ -296,17 +308,11 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { buf.writeLong(partialHashedSeed); } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { - ProtocolUtils.writeVarInt(buf, maxPlayers); - } else { - buf.writeByte(maxPlayers); - } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) < 0) { - if (levelType == null) { - throw new IllegalStateException("No level type specified."); - } - ProtocolUtils.writeString(buf, levelType); + buf.writeByte(maxPlayers); + if (levelType == null) { + throw new IllegalStateException("No level type specified."); } + ProtocolUtils.writeString(buf, levelType); if (version.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) { ProtocolUtils.writeVarInt(buf, viewDistance); } @@ -316,10 +322,48 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) { buf.writeBoolean(showRespawnScreen); } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { - buf.writeBoolean(dimensionInfo.isDebugType()); - buf.writeBoolean(dimensionInfo.isFlat()); + } + + private void encode116Up(ByteBuf buf, ProtocolVersion version) { + buf.writeInt(entityId); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + buf.writeBoolean(isHardcore); + buf.writeByte(gamemode); + } else { + buf.writeByte(isHardcore ? gamemode | 0x8 : gamemode); } + buf.writeByte(previousGamemode); + ProtocolUtils.writeStringArray(buf, dimensionRegistry.getLevelNames().toArray(new String[0])); + CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder(); + ListBinaryTag encodedDimensionRegistry = dimensionRegistry.encodeRegistry(version); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder(); + dimensionRegistryEntry.putString("type", "minecraft:dimension_type"); + dimensionRegistryEntry.put("value", encodedDimensionRegistry); + registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build()); + registryContainer.put("minecraft:worldgen/biome", biomeRegistry); + } else { + registryContainer.put("dimension", encodedDimensionRegistry); + } + ProtocolUtils.writeCompoundTag(buf, registryContainer.build()); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails()); + ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); + } else { + ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); + ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); + } + buf.writeLong(partialHashedSeed); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) { + ProtocolUtils.writeVarInt(buf, maxPlayers); + } else { + buf.writeByte(maxPlayers); + } + ProtocolUtils.writeVarInt(buf, viewDistance); + buf.writeBoolean(reducedDebugInfo); + buf.writeBoolean(showRespawnScreen); + buf.writeBoolean(dimensionInfo.isDebugType()); + buf.writeBoolean(dimensionInfo.isFlat()); } @Override