From e56997a582a67b481b35628ec866136787ec338e Mon Sep 17 00:00:00 2001 From: Pokechu22 Date: Thu, 9 Jun 2016 17:06:23 -0700 Subject: [PATCH 1/2] Add terrainandmovements support to 1.9 This is still a bit unstable, and chunk parsing is _really_ slow, but it's a start. --- .../Protocol/Handlers/Protocol18.cs | 155 ++++++++++++++---- 1 file changed, 123 insertions(+), 32 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 94a75ff5..70058f98 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -46,12 +46,6 @@ namespace MinecraftClient.Protocol.Handlers this.protocolversion = ProtocolVersion; this.handler = Handler; this.forgeInfo = ForgeInfo; - - if (Settings.TerrainAndMovements && protocolversion > MC18Version) - { - ConsoleIO.WriteLineFormatted("§8Terrain & Movements currently not handled for that MC version."); - Settings.TerrainAndMovements = false; - } } private Protocol18Handler(TcpClient Client) @@ -234,7 +228,6 @@ namespace MinecraftClient.Protocol.Handlers } } // Regular in-game packets - switch (getPacketIncomingType(packetID, protocolversion)) { case PacketIncomingType.KeepAlive: @@ -273,7 +266,11 @@ namespace MinecraftClient.Protocol.Handlers handler.UpdateLocation(location); if (protocolversion >= MC19Version) - readNextVarInt(packetData); + { + int teleportID = readNextVarInt(packetData); + // Teleport confirm packet + SendPacket(0x00, getVarInt(teleportID)); + } } break; case PacketIncomingType.ChunkData: @@ -581,51 +578,131 @@ namespace MinecraftClient.Protocol.Handlers private void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, bool hasSkyLight, bool chunksContinuous, List cache) { - if (protocolversion < MC19Version && chunksContinuous && chunkMask == 0) + if (protocolversion >= MC19Version) { - //Unload the entire chunk column - handler.GetWorld()[chunkX, chunkZ] = null; - } - else - { - //Load chunk data from the server + // 1.9 and above chunk format + // Unloading chunks is handled by a separate packet for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) { + byte bitsPerBlock = readNextByte(cache); + bool usePalette = (bitsPerBlock <= 8); + + int paletteLength = readNextVarInt(cache); + int[] palette = new int[paletteLength]; + for (int i = 0; i < paletteLength; i++) + { + palette[i] = readNextVarInt(cache); + } + + // Bit mask covering bitsPerBlock bits + // EG, if bitsPerBlock = 5, valueMask = 00011111 in binary + uint valueMask = (uint)((1 << bitsPerBlock) - 1); + + ulong[] dataArray = readNextULongArray(cache); + Chunk chunk = new Chunk(); - //Read chunk data, all at once for performance reasons, and build the chunk object - Queue queue = new Queue(readNextUShortsLittleEndian(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ, cache)); 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(queue.Dequeue()); + { + int blockNumber = (blockY * Chunk.SizeZ + blockZ) * Chunk.SizeX + blockX; + + int startLong = (blockNumber * bitsPerBlock) / 64; + int startOffset = (blockNumber * bitsPerBlock) % 64; + int endLong = ((blockNumber+ 1) * bitsPerBlock - 1) / 64; + + // TODO: In the future a single ushort may not store the entire block id; + // the Block code may need to change. + ushort blockId; + if (startLong == endLong) + { + blockId = (ushort)((dataArray[startLong] >> startOffset) & valueMask); + } + else + { + int endOffset = 64 - startOffset; + blockId = (ushort)((dataArray[startLong] >> startOffset | dataArray[endLong] << endOffset) & valueMask); + } + + if (usePalette) + { + // Get the real block ID out of the palette + blockId = (ushort)palette[blockId]; + } + + chunk[blockX, blockY, blockZ] = new Block(blockId); + } + } + } //We have our chunk, save the chunk into the world if (handler.GetWorld()[chunkX, chunkZ] == null) handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; - } - } - //Skip light information - for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) - { - if ((chunkMask & (1 << chunkY)) != 0) - { //Skip block light readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); //Skip sky light - if (hasSkyLight) - readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + // TODO: handle hasSkylight check correctly (nether/nonnether) + readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); } } - //Skip biome metadata - if (chunksContinuous) - readData(Chunk.SizeX * Chunk.SizeZ, cache); + // 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) + } + else + { + // Pre 1.9 chunk format + if (chunksContinuous && chunkMask == 0) { + //Unload the entire chunk column + handler.GetWorld()[chunkX, chunkZ] = null; + } else { + //Load chunk data from the server + for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + { + if ((chunkMask & (1 << chunkY)) != 0) + { + Chunk chunk = new Chunk(); + + //Read chunk data, all at once for performance reasons, and build the chunk object + Queue queue = new Queue(readNextUShortsLittleEndian(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ, cache)); + 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(queue.Dequeue()); + + //We have our chunk, save the chunk into the world + if (handler.GetWorld()[chunkX, chunkZ] == null) + handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); + handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; + } + } + + //Skip light information + for (int chunkY = 0; chunkY < ChunkColumn.ColumnSize; chunkY++) + { + if ((chunkMask & (1 << chunkY)) != 0) + { + //Skip block light + readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + + //Skip sky light + if (hasSkyLight) + readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + } + } + + //Skip biome metadata + if (chunksContinuous) + readData(Chunk.SizeX * Chunk.SizeZ, cache); + } } } @@ -755,9 +832,9 @@ namespace MinecraftClient.Protocol.Handlers } /// - /// Read an unsigned short integer from a cache of bytes and remove it from the cache + /// Read an unsigned long integer from a cache of bytes and remove it from the cache /// - /// The unsigned short integer value + /// The unsigned long integer value private static ulong readNextULong(List cache) { @@ -805,6 +882,20 @@ namespace MinecraftClient.Protocol.Handlers return readData(len, cache); } + /// + /// Reads a length-prefixed array of unsigned long integers and removes it from the cache + /// + /// The unsigned long integer values + + private static ulong[] readNextULongArray(List cache) + { + int len = readNextVarInt(cache); + ulong[] result = new ulong[len]; + for (int i = 0; i < len; i++) + result[i] = readNextULong(cache); + return result; + } + /// /// Read a double from a cache of bytes and remove it from the cache /// From 546b307cf3dda6dde9ec5e9b16aeca4185347eec Mon Sep 17 00:00:00 2001 From: Pokechu22 Date: Fri, 10 Jun 2016 16:59:53 -0700 Subject: [PATCH 2/2] Fix 1.9 terrain and movement in the nether The nether doesn't send skylight, so we need to ignore skylight in the nether for it to work. However, that means that dimensions need to be tracked, so the respawn packet is now also tracked (and the forge dimension override packet) --- .../Protocol/Handlers/Protocol18.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 70058f98..2dea6791 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -20,6 +20,7 @@ namespace MinecraftClient.Protocol.Handlers { private const int MC18Version = 47; private const int MC19Version = 107; + private const int MC191Version = 108; private const int MC110Version = 210; private int compression_treshold = 0; @@ -38,6 +39,8 @@ namespace MinecraftClient.Protocol.Handlers IAesStream s; TcpClient c; + int currentDimension; + public Protocol18Handler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler, ForgeInfo ForgeInfo) { ConsoleIO.SetAutoCompleteEngine(this); @@ -136,6 +139,7 @@ namespace MinecraftClient.Protocol.Handlers KeepAlive, JoinGame, ChatMessage, + Respawn, PlayerPositionAndLook, ChunkData, MultiBlockChange, @@ -167,6 +171,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x00: return PacketIncomingType.KeepAlive; case 0x01: return PacketIncomingType.JoinGame; case 0x02: return PacketIncomingType.ChatMessage; + case 0x07: return PacketIncomingType.Respawn; case 0x08: return PacketIncomingType.PlayerPositionAndLook; case 0x21: return PacketIncomingType.ChunkData; case 0x22: return PacketIncomingType.MultiBlockChange; @@ -189,6 +194,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x1F: return PacketIncomingType.KeepAlive; case 0x23: return PacketIncomingType.JoinGame; case 0x0F: return PacketIncomingType.ChatMessage; + case 0x33: return PacketIncomingType.Respawn; case 0x2E: return PacketIncomingType.PlayerPositionAndLook; case 0x20: return PacketIncomingType.ChunkData; case 0x10: return PacketIncomingType.MultiBlockChange; @@ -235,6 +241,16 @@ namespace MinecraftClient.Protocol.Handlers break; case PacketIncomingType.JoinGame: handler.OnGameJoined(); + readNextInt(packetData); + readNextByte(packetData); + if (protocolversion >= MC191Version) + this.currentDimension = readNextInt(packetData); + else + this.currentDimension = (sbyte)readNextByte(packetData); + readNextByte(packetData); + readNextByte(packetData); + readNextString(packetData); + readNextBool(packetData); break; case PacketIncomingType.ChatMessage: string message = readNextString(packetData); @@ -249,6 +265,12 @@ namespace MinecraftClient.Protocol.Handlers catch (ArgumentOutOfRangeException) { /* No message type */ } handler.OnTextReceived(ChatParser.ParseText(message)); break; + case PacketIncomingType.Respawn: + this.currentDimension = readNextInt(packetData); + readNextByte(packetData); + readNextByte(packetData); + readNextString(packetData); + break; case PacketIncomingType.PlayerPositionAndLook: if (Settings.TerrainAndMovements) { @@ -443,11 +465,13 @@ namespace MinecraftClient.Protocol.Handlers SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels))); byte fmlProtocolVersion = readNextByte(packetData); - // There's another value afterwards for the dimension, but we don't need it. if (Settings.DebugMessages) ConsoleIO.WriteLineFormatted("§8Forge protocol version : " + fmlProtocolVersion); + if (fmlProtocolVersion >= 1) + this.currentDimension = readNextInt(packetData); + // Tell the server we're running the same version. SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion }); @@ -614,7 +638,7 @@ namespace MinecraftClient.Protocol.Handlers int startLong = (blockNumber * bitsPerBlock) / 64; int startOffset = (blockNumber * bitsPerBlock) % 64; - int endLong = ((blockNumber+ 1) * bitsPerBlock - 1) / 64; + int endLong = ((blockNumber + 1) * bitsPerBlock - 1) / 64; // TODO: In the future a single ushort may not store the entire block id; // the Block code may need to change. @@ -649,8 +673,9 @@ namespace MinecraftClient.Protocol.Handlers readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); //Skip sky light - // TODO: handle hasSkylight check correctly (nether/nonnether) - readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); + if (this.currentDimension != -1) + // Sky light is not sent in the nether + readData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); } }