diff --git a/MinecraftClient/Mapping/ChunkColumn.cs b/MinecraftClient/Mapping/ChunkColumn.cs index 22c9d2ae..a7e704ce 100644 --- a/MinecraftClient/Mapping/ChunkColumn.cs +++ b/MinecraftClient/Mapping/ChunkColumn.cs @@ -11,18 +11,27 @@ namespace MinecraftClient.Mapping /// public class ChunkColumn { - public const int ColumnSize = 16; + public int ColumnSize; /// /// Blocks contained into the chunk /// - private readonly Chunk[] chunks = new Chunk[ColumnSize]; + private readonly Chunk[] chunks; /// /// Lock for thread safety /// private readonly ReaderWriterLockSlim chunkLock = new ReaderWriterLockSlim(); + /// + /// Create a new ChunkColumn + /// + public ChunkColumn(int size = 16) + { + ColumnSize = size; + chunks = new Chunk[size]; + } + /// /// Get or set the specified chunk column /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index f4cb507f..a276ee7a 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -111,7 +111,7 @@ namespace MinecraftClient.Protocol.Handlers { if (protocolVersion > MC1182Version && handler.GetTerrainEnabled()) throw new NotImplementedException(Translations.Get("exception.palette.block")); - if (protocolVersion >= MC1181Version) + if (protocolVersion >= MC117Version) Block.Palette = new Palette117(); else if (protocolVersion >= MC116Version) Block.Palette = new Palette116(); @@ -425,56 +425,77 @@ namespace MinecraftClient.Protocol.Handlers SendPacket(PacketTypesOut.TeleportConfirm, dataTypes.GetVarInt(teleportID)); } - if (protocolversion >= MC117Version) dataTypes.ReadNextBool(packetData); + if (protocolversion >= MC117Version) + dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 and above break; - case PacketTypesIn.ChunkData: //TODO implement for 1.17, bit mask is not limited to 0-15 anymore + case PacketTypesIn.ChunkData: if (handler.GetTerrainEnabled()) { int chunkX = dataTypes.ReadNextInt(packetData); int chunkZ = dataTypes.ReadNextInt(packetData); - bool chunksContinuous = dataTypes.ReadNextBool(packetData); - if (protocolversion >= MC116Version && protocolversion <= MC1161Version) - dataTypes.ReadNextBool(packetData); // Ignore old data - 1.16 to 1.16.1 only - ushort chunkMask = protocolversion >= MC19Version - ? (ushort)dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextUShort(packetData); - if (protocolversion < MC18Version) + if (protocolversion >= MC117Version) { - ushort addBitmap = dataTypes.ReadNextUShort(packetData); - int compressedDataSize = dataTypes.ReadNextInt(packetData); - byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); - byte[] decompressed = ZlibUtils.Decompress(compressed); + ulong[] verticalStripBitmask = dataTypes.ReadNextULongArray(packetData); // Bit Mask Length and Primary Bit Mask + dataTypes.ReadNextNbt(packetData); // Heightmaps + + int biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length + for (int i = 0; i < biomesLength; i++) + { + dataTypes.SkipNextVarInt(packetData); // Biomes + } + + int dataSize = dataTypes.ReadNextVarInt(packetData); // Size new Task(() => { - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue(decompressed)); + pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, currentDimension, packetData); }).Start(); } else { - if (protocolversion >= MC114Version) - dataTypes.ReadNextNbt(packetData); // Heightmaps - 1.14 and above - int biomesLength = 0; - if (protocolversion >= MC1162Version) - if (chunksContinuous) - biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length - 1.16.2 and above - if (protocolversion >= MC115Version && chunksContinuous) + bool chunksContinuous = dataTypes.ReadNextBool(packetData); + if (protocolversion >= MC116Version && protocolversion <= MC1161Version) + dataTypes.ReadNextBool(packetData); // Ignore old data - 1.16 to 1.16.1 only + ushort chunkMask = protocolversion >= MC19Version + ? (ushort)dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextUShort(packetData); + if (protocolversion < MC18Version) { - if (protocolversion >= MC1162Version) + ushort addBitmap = dataTypes.ReadNextUShort(packetData); + int compressedDataSize = dataTypes.ReadNextInt(packetData); + byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); + byte[] decompressed = ZlibUtils.Decompress(compressed); + new Task(() => { - for (int i = 0; i < biomesLength; i++) - { - // Biomes - 1.16.2 and above - // Don't use ReadNextVarInt because it cost too much time - dataTypes.SkipNextVarInt(packetData); - } - } - else dataTypes.ReadData(1024 * 4, packetData); // Biomes - 1.15 and above + pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue(decompressed)); + }).Start(); } - int dataSize = dataTypes.ReadNextVarInt(packetData); - new Task(() => + else { - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); - }).Start(); + if (protocolversion >= MC114Version) + dataTypes.ReadNextNbt(packetData); // Heightmaps - 1.14 and above + int biomesLength = 0; + if (protocolversion >= MC1162Version) + if (chunksContinuous) + biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length - 1.16.2 and above + if (protocolversion >= MC115Version && chunksContinuous) + { + if (protocolversion >= MC1162Version) + { + for (int i = 0; i < biomesLength; i++) + { + // Biomes - 1.16.2 and above + // Don't use ReadNextVarInt because it cost too much time + dataTypes.SkipNextVarInt(packetData); + } + } + else dataTypes.ReadData(1024 * 4, packetData); // Biomes - 1.15 and above + } + int dataSize = dataTypes.ReadNextVarInt(packetData); + new Task(() => + { + pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); + }).Start(); + } } } break; @@ -839,7 +860,7 @@ namespace MinecraftClient.Protocol.Handlers int stateId = -1; - if (protocolversion >= MC1181Version) + if (protocolversion >= MC1171Version) // State ID - 1.17.1 and above stateId = dataTypes.ReadNextVarInt(packetData); int elements = dataTypes.ReadNextVarInt(packetData); @@ -851,6 +872,9 @@ namespace MinecraftClient.Protocol.Handlers inventorySlots[slotId] = item; } handler.OnWindowItems(windowId, inventorySlots, stateId); + + if (protocolversion >= MC1171Version) // Carried Item - 1.17.1 and above + dataTypes.ReadNextItemSlot(packetData, itemPalette); } break; case PacketTypesIn.SetSlot: @@ -860,7 +884,7 @@ namespace MinecraftClient.Protocol.Handlers int stateId = -1; - if (protocolversion >= MC1181Version) + if (protocolversion >= MC1171Version) // State ID - 1.17.1 and above stateId = dataTypes.ReadNextVarInt(packetData); short slotID = dataTypes.ReadNextShort(packetData); @@ -888,6 +912,8 @@ namespace MinecraftClient.Protocol.Handlers { forced = dataTypes.ReadNextBool(packetData); String forcedMessage = ChatParser.ParseText(dataTypes.ReadNextString(packetData)); + // skip: Has Prompt Message (Boolean) + // skip: Prompt Message (Optional Chat) } // Some server plugins may send invalid resource packs to probe the client and we need to ignore them (issue #1056) if (!url.StartsWith("http") && hash.Length != 40) // Some server may have null hash value @@ -986,7 +1012,20 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.DestroyEntity: if (handler.GetEntityHandlingEnabled()) { - handler.OnDestroyEntities(new[] { dataTypes.ReadNextVarInt(packetData) }); + int[] entityIDs; + if (protocolversion == MC117Version) // single Entity ID per packet - 1.17 only + { + entityIDs = new int[1]; + entityIDs[0] = dataTypes.ReadNextVarInt(packetData); + } + else + { + int count = dataTypes.ReadNextVarInt(packetData); + entityIDs = new int[count]; + for (int i = 0; i < count; ++i) + entityIDs[i] = dataTypes.ReadNextVarInt(packetData); + } + handler.OnDestroyEntities(entityIDs); } break; case PacketTypesIn.EntityPosition: @@ -1646,7 +1685,7 @@ namespace MinecraftClient.Protocol.Handlers if (protocolversion >= MC19Version) fields.AddRange(dataTypes.GetVarInt(mainHand)); if (protocolversion >= MC117Version) - fields.Add(0); // Enables text filtering. Always false + fields.Add(1); // 1.17 and above - Disable text filtering. (Always true) if (protocolversion >= MC1181Version) fields.Add(1); // 1.18 and above - Allow server listings SendPacket(PacketTypesOut.ClientSettings, fields); @@ -1938,7 +1977,7 @@ namespace MinecraftClient.Protocol.Handlers packet.Add((byte)windowId); // 1.18+ - if (protocolversion > MC1171Version) + if (protocolversion >= MC1181Version) { packet.AddRange(dataTypes.GetVarInt(stateId)); packet.AddRange(dataTypes.GetShort((short)slotId)); @@ -1973,7 +2012,7 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(arrayOfSlots); } - packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); + packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); // Clicked item log.Info("Packet data: " + dataTypes.ByteArrayToString(packet.ToArray())); diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs index 14202599..5ce8644f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs @@ -28,7 +28,164 @@ namespace MinecraftClient.Protocol.Handlers } /// - /// Process chunk column data from the server and (un)load the chunk from the Minecraft world + /// Process chunk column data from the server and (un)load the chunk from the Minecraft world - 1.17 and above + /// + /// Chunk X location + /// Chunk Z location + /// Chunk mask for reading data, store in bitset + /// Current dimension type (0 = overworld) + /// Cache for reading chunk data + public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[] chunkMasks, int currentDimension, Queue cache) + { + byte[] mirror = new byte[cache.Count]; + cache.CopyTo(mirror, 0); + int chunkColumnSize = chunkMasks.Length * 64; + if (protocolversion >= Protocol18Handler.MC117Version) + { + // 1.17 and above chunk format + // Unloading chunks is handled by a separate packet + for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) + { + if ((chunkMasks[chunkY / 64] & (1UL << (chunkY % 64))) != 0) + { + // Non-air block count inside chunk section, for lighting purposes + int blockCnt = dataTypes.ReadNextShort(cache); + + // read Block states (Type: Paletted Container) + Chunk chunk = new Chunk(); + + byte bitsPerEntry = dataTypes.ReadNextByte(cache); + if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC1181Version) + { + // Palettes: Single valued - 1.xx and above + ushort value = (ushort)dataTypes.ReadNextVarInt(cache); + + dataTypes.SkipNextVarInt(cache); // Data Array Length will be zero + + for (int blockY = 0; blockY < Chunk.SizeY; blockY++) + { + for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++) + { + for (int blockX = 0; blockX < Chunk.SizeX; blockX++) + { + chunk[blockX, blockY, blockZ] = new Block(value); + } + } + } + } + else + { + // Palettes: Indirect or Direct + bool usePalette = (bitsPerEntry <= 8); + + // Indirect Mode: For block states with bits per entry <= 4, 4 bits are used to represent a block. + if (bitsPerEntry < 4) bitsPerEntry = 4; + + // Direct Mode: Bit mask covering bitsPerBlock bits + // EG, if bitsPerBlock = 5, valueMask = 00011111 in binary + uint valueMask = (uint)((1 << bitsPerEntry) - 1); + + int paletteLength = 0; // Assume zero when length is absent + if (usePalette) paletteLength = dataTypes.ReadNextVarInt(cache); + + int[] palette = new int[paletteLength]; + for (int i = 0; i < paletteLength; i++) + palette[i] = dataTypes.ReadNextVarInt(cache); + + // Block IDs are packed in the array of 64-bits integers + ulong[] dataArray = dataTypes.ReadNextULongArray(cache); + + int longIndex = 0; + int startOffset = 0 - bitsPerEntry; + for (int blockY = 0; blockY < Chunk.SizeY; blockY++) + { + for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++) + { + for (int blockX = 0; blockX < Chunk.SizeX; blockX++) + { + // NOTICE: In the future a single ushort may not store the entire block id; + // the Block class may need to change if block state IDs go beyond 65535 + ushort blockId; + + // Calculate location of next block ID inside the array of Longs + startOffset += bitsPerEntry; + + if ((startOffset + bitsPerEntry) > 64) + { + // In MC 1.16+, padding is applied to prevent overlapping between Longs: + // [ LONG INTEGER ][ LONG INTEGER ] + // [Block][Block][Block]XXXXX[Block][Block][Block]XXXXX + + // When overlapping, move forward to the beginning of the next Long + startOffset = 0; + longIndex++; + } + + // Extract Block ID + blockId = (ushort)((dataArray[longIndex] >> startOffset) & valueMask); + + // Map small IDs to actual larger block IDs + if (usePalette) + { + if (paletteLength <= blockId) + { + int blockNumber = (blockY * Chunk.SizeZ + blockZ) * Chunk.SizeX + blockX; + throw new IndexOutOfRangeException(String.Format("Block ID {0} is outside Palette range 0-{1}! (bitsPerBlock: {2}, blockNumber: {3})", + blockId, + paletteLength - 1, + bitsPerEntry, + blockNumber)); + } + + blockId = (ushort)palette[blockId]; + } + + // We have our block, save the block into the chunk + chunk[blockX, blockY, blockZ] = new Block(blockId); + } + } + } + } + + //We have our chunk, save the chunk into the world + handler.InvokeOnMainThread(() => + { + if (handler.GetWorld()[chunkX, chunkZ] == null) + handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(chunkColumnSize); + handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; + }); + + if (protocolversion >= Protocol18Handler.MC1181Version) + { + // skip read Biomes (Type: Paletted Container) + byte bitsPerEntryBiome = dataTypes.ReadNextByte(cache); // Bits Per Entry + if (bitsPerEntryBiome == 0 && protocolversion >= Protocol18Handler.MC1181Version) + { + dataTypes.SkipNextVarInt(cache); // Value + dataTypes.SkipNextVarInt(cache); // Data Array Length + // Data Array must be empty + } + else + { + if (bitsPerEntryBiome <= 3) + { + int paletteLength = dataTypes.ReadNextVarInt(cache); // Palette Length + for (int i = 0; i < paletteLength; i++) + dataTypes.SkipNextVarInt(cache); // Palette + } + int dataArrayLength = dataTypes.ReadNextVarInt(cache); // Data Array Length + dataTypes.ReadData(dataArrayLength * 8, cache); // Data Array + } + } + } + } + // Don't worry about skipping remaining data since there is no useful data afterwards in 1.9 + // (plus, it would require parsing the tile entity lists' NBT) + } + } + + /// + /// Process chunk column data from the server and (un)load the chunk from the Minecraft world - 1.17 below /// /// Chunk X location /// Chunk Z location @@ -40,11 +197,12 @@ namespace MinecraftClient.Protocol.Handlers /// Cache for reading chunk data public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue cache) { + const int chunkColumnSize = 16; if (protocolversion >= Protocol18Handler.MC19Version) { // 1.9 and above chunk format // Unloading chunks is handled by a separate packet - for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) { @@ -200,7 +358,7 @@ namespace MinecraftClient.Protocol.Handlers else { //Load chunk data from the server - for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) { @@ -224,7 +382,7 @@ namespace MinecraftClient.Protocol.Handlers } //Skip light information - for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) { @@ -258,7 +416,7 @@ namespace MinecraftClient.Protocol.Handlers //Count chunk sections int sectionCount = 0; int addDataSectionCount = 0; - for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) sectionCount++; @@ -286,7 +444,7 @@ namespace MinecraftClient.Protocol.Handlers dataTypes.ReadData(Chunk.SizeX * Chunk.SizeZ, cache); //Biomes //Load chunk data - for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) {