From 516effa81d25bc2dfad990369c4287bca251b7c1 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 24 Jul 2022 21:41:56 +0800 Subject: [PATCH] terrain handling for 1.18(1.18.1) and 1.18.2 --- MinecraftClient/Mapping/Dimension.cs | 169 +++++++++++++ MinecraftClient/Mapping/Location.cs | 10 +- MinecraftClient/Mapping/World.cs | 30 +++ MinecraftClient/MinecraftClient.csproj | 1 + .../Protocol/Handlers/Protocol18.cs | 65 +++-- .../Protocol/Handlers/Protocol18Terrain.cs | 229 ++++++++++-------- 6 files changed, 384 insertions(+), 120 deletions(-) create mode 100644 MinecraftClient/Mapping/Dimension.cs diff --git a/MinecraftClient/Mapping/Dimension.cs b/MinecraftClient/Mapping/Dimension.cs new file mode 100644 index 00000000..0ca08e6e --- /dev/null +++ b/MinecraftClient/Mapping/Dimension.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MinecraftClient.Mapping +{ + + /// + /// The dimension type, available after 1.16.2 + /// + public class Dimension + { + /// + /// The name of the dimension type (for example, "minecraft:overworld"). + /// + public readonly string Name; + + /// + /// Whether piglins shake and transform to zombified piglins. + /// + public readonly bool piglin_safe; + + /// + /// When false, compasses spin randomly. When true, nether portals can spawn zombified piglins. + /// + public readonly bool natural; + + /// + /// How much light the dimension has. + /// + public readonly float ambient_light; + + + /// + /// If set, the time of the day is the specified value. + /// Value: -1: not set + /// Value: [0, 24000]: time of the day + /// + public readonly long fixed_time = -1; + + /// + /// A resource location defining what block tag to use for infiniburn. + /// Value: "" or minecraft resource "minecraft:...". + /// + public readonly string infiniburn; + + /// + /// Whether players can charge and use respawn anchors. + /// + public readonly bool respawn_anchor_works; + + /// + /// Whether the dimension has skylight access or not. + /// + public readonly bool has_skylight; + + /// + /// Whether players can use a bed to sleep. + /// + public readonly bool bed_works; + + /// + /// unknown + /// Values: "minecraft:overworld", "minecraft:the_nether", "minecraft:the_end" or something else. + /// + public readonly string effects; + + /// + /// Whether players with the Bad Omen effect can cause a raid. + /// + public readonly bool has_raids; + + /// + /// The minimum Y level. + /// + public readonly int min_y = 0; + + /// + /// The minimum Y level. + /// + public readonly int max_y = 256; + + /// + /// The maximum height. + /// + public readonly int height = 256; + + /// + /// The maximum height to which chorus fruits and nether portals can bring players within this dimension. + /// + public readonly int logical_height; + + /// + /// The multiplier applied to coordinates when traveling to the dimension. + /// + public readonly double coordinate_scale; + + /// + /// Whether the dimensions behaves like the nether (water evaporates and sponges dry) or not. Also causes lava to spread thinner. + /// + public readonly bool ultrawarm; + + /// + /// Whether the dimension has a bedrock ceiling or not. When true, causes lava to spread faster. + /// + public readonly bool has_ceiling; + + + /// + /// Create from the "Dimension Codec" NBT Tag Compound + /// + /// ChunkColumn X + /// ChunkColumn Y + /// chunk at the given location + public Dimension(string name, Dictionary nbt) + { + if (name == null) + throw new ArgumentNullException("name"); + if (nbt == null) + throw new ArgumentNullException("nbt Data"); + + this.Name = name; + + if (nbt.ContainsKey("piglin_safe")) + this.piglin_safe = 1 == (byte)nbt["piglin_safe"]; + if (nbt.ContainsKey("natural")) + this.natural = 1 == (byte)nbt["natural"]; + if (nbt.ContainsKey("ambient_light")) + this.ambient_light = (float)nbt["ambient_light"]; + if (nbt.ContainsKey("fixed_time")) + this.fixed_time = (long)nbt["fixed_time"]; + if (nbt.ContainsKey("infiniburn")) + this.infiniburn = (string)nbt["infiniburn"]; + if (nbt.ContainsKey("respawn_anchor_works")) + this.respawn_anchor_works = 1 == (byte)nbt["respawn_anchor_works"]; + if (nbt.ContainsKey("has_skylight")) + this.has_skylight = 1 == (byte)nbt["has_skylight"]; + if (nbt.ContainsKey("bed_works")) + this.bed_works = 1 == (byte)nbt["bed_works"]; + if (nbt.ContainsKey("effects")) + this.effects = (string)nbt["effects"]; + if (nbt.ContainsKey("has_raids")) + this.has_raids = 1 == (byte)nbt["has_raids"]; + if (nbt.ContainsKey("min_y")) + this.min_y = (int)nbt["min_y"]; + if (nbt.ContainsKey("height")) + this.height = (int)nbt["height"]; + if (nbt.ContainsKey("min_y") && nbt.ContainsKey("height")) + this.max_y = this.min_y + this.height; + if (nbt.ContainsKey("logical_height")) + this.logical_height = (int)nbt["logical_height"]; + if (nbt.ContainsKey("coordinate_scale")) + { + var coordinate_scale_obj = nbt["coordinate_scale"]; + if (coordinate_scale_obj.GetType() == typeof(float)) + this.coordinate_scale = (float)coordinate_scale_obj; + else + this.coordinate_scale = (double)coordinate_scale_obj; + } + if (nbt.ContainsKey("ultrawarm")) + this.ultrawarm = 1 == (byte)nbt["ultrawarm"]; + if (nbt.ContainsKey("has_ceiling")) + this.has_ceiling = 1 == (byte)nbt["has_ceiling"]; + } + + } +} diff --git a/MinecraftClient/Mapping/Location.cs b/MinecraftClient/Mapping/Location.cs index 00f18071..ad02d75a 100644 --- a/MinecraftClient/Mapping/Location.cs +++ b/MinecraftClient/Mapping/Location.cs @@ -25,6 +25,11 @@ namespace MinecraftClient.Mapping /// public double Z; + /// + /// Current world: to get the lowest Y coordinate + /// + public static World world; + /// /// Get location with zeroed coordinates /// @@ -79,7 +84,10 @@ namespace MinecraftClient.Mapping { get { - return (int)Math.Floor(Y / Chunk.SizeY); + if (world.GetDimension() == null) + return (int)Math.Floor(Y / Chunk.SizeY); // old version, always start at zero + else + return (int)Math.Floor((Y - world.GetDimension().min_y) / Chunk.SizeY); } } diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index 9a57baae..b324007a 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -16,6 +16,11 @@ namespace MinecraftClient.Mapping /// private Dictionary> chunks = new Dictionary>(); + /// + /// The dimension info of the world + /// + private Dimension dimension; + /// /// Lock for thread safety /// @@ -78,6 +83,30 @@ namespace MinecraftClient.Mapping } } + public World() + { + Location.world = this; + } + + /// + /// Set dimension type + /// + /// The name of the dimension type + /// The dimension type (NBT Tag Compound) + public void SetDimension(string name, Dictionary nbt) + { + this.dimension = new Dimension(name, nbt); + } + + /// + /// Get dimension type + /// + /// The chunk column + public Dimension GetDimension() + { + return this.dimension; + } + /// /// Get chunk column at the specified location /// @@ -176,6 +205,7 @@ namespace MinecraftClient.Mapping try { chunks = new Dictionary>(); + dimension = null; } finally { diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 44aec940..86351c15 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -79,6 +79,7 @@ + diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index a276ee7a..a55a214c 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -302,17 +302,23 @@ namespace MinecraftClient.Protocol.Handlers for (int i = 0; i < worldCount; i++) dataTypes.ReadNextString(packetData); // World Names - 1.16 and above dataTypes.ReadNextNbt(packetData); // Dimension Codec - 1.16 and above + } - //Current dimension - String identifier in 1.16, varInt below 1.16, byte below 1.9.1 + string currentDimensionName = null; + Dictionary currentDimensionType = null; + + // Current dimension + // NBT Tag Compound: 1.16.2 and above + // String identifier: 1.16 and 1.16.1 + // varInt: [1.9.1 to 1.15.2] + // byte: below 1.9.1 if (protocolversion >= MC116Version) { if (protocolversion >= MC1162Version) - dataTypes.ReadNextNbt(packetData); + currentDimensionType = dataTypes.ReadNextNbt(packetData); else dataTypes.ReadNextString(packetData); - // TODO handle dimensions for 1.16+, needed for terrain handling - // TODO this data give min and max y which will be needed for chunk collumn handling this.currentDimension = 0; } else if (protocolversion >= MC191Version) @@ -322,8 +328,16 @@ namespace MinecraftClient.Protocol.Handlers if (protocolversion < MC114Version) dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below + if (protocolversion >= MC116Version) - dataTypes.ReadNextString(packetData); // World Name - 1.16 and above + currentDimensionName = dataTypes.ReadNextString(packetData); // Dimension Name (World Name) - 1.16 and above + + if (protocolversion >= MC1162Version) + new Task(() => + { + handler.GetWorld().SetDimension(currentDimensionName, currentDimensionType); + }).Start(); + if (protocolversion >= MC115Version) dataTypes.ReadNextLong(packetData); // Hashed world seed - 1.15 and above @@ -362,13 +376,14 @@ namespace MinecraftClient.Protocol.Handlers handler.OnTextReceived(message, true); break; case PacketTypesIn.Respawn: + string dimensionNameInRespawn = null; + Dictionary dimensionTypeInRespawn = null; if (protocolversion >= MC116Version) { - // TODO handle dimensions for 1.16+, needed for terrain handling if (protocolversion >= MC1162Version) - dataTypes.ReadNextNbt(packetData); + dimensionTypeInRespawn = dataTypes.ReadNextNbt(packetData); else - dataTypes.ReadNextString(packetData); + dataTypes.ReadNextString(packetData); this.currentDimension = 0; } else @@ -377,7 +392,14 @@ namespace MinecraftClient.Protocol.Handlers this.currentDimension = dataTypes.ReadNextInt(packetData); } if (protocolversion >= MC116Version) - dataTypes.ReadNextString(packetData); // World Name - 1.16 and above + dimensionNameInRespawn = dataTypes.ReadNextString(packetData); // World Name - 1.16 and above + + if (protocolversion >= MC1162Version) + new Task(() => + { + handler.GetWorld().SetDimension(dimensionNameInRespawn, dimensionTypeInRespawn); + }).Start(); + if (protocolversion < MC114Version) dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below if (protocolversion >= MC115Version) @@ -435,19 +457,26 @@ namespace MinecraftClient.Protocol.Handlers int chunkZ = dataTypes.ReadNextInt(packetData); if (protocolversion >= MC117Version) { - ulong[] verticalStripBitmask = dataTypes.ReadNextULongArray(packetData); // Bit Mask Length and Primary Bit Mask + ulong[] verticalStripBitmask = null; + + if (protocolversion == MC117Version || protocolversion == MC1171Version) + 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++) + if (protocolversion == MC117Version || protocolversion == MC1171Version) { - dataTypes.SkipNextVarInt(packetData); // Biomes + 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, verticalStripBitmask, currentDimension, packetData); + pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData); }).Start(); } else @@ -600,10 +629,10 @@ namespace MinecraftClient.Protocol.Handlers if (protocolversion >= MC1162Version) { long chunkSection = dataTypes.ReadNextLong(packetData); - int sectionX = (int)(chunkSection >> 42); - int sectionY = (int)((chunkSection << 44) >> 44); - int sectionZ = (int)((chunkSection << 22) >> 42); - dataTypes.ReadNextBool(packetData); // Useless boolean + int sectionX = (int)((chunkSection >> 42) & 0x3FFFFF); + int sectionZ = (int)((chunkSection >> 20) & 0x3FFFFF); + int sectionY = (int)((chunkSection) & 0xFFFFF); + dataTypes.ReadNextBool(packetData); // Useless boolean (Related to light update) int blocksSize = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < blocksSize; i++) { diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs index 0a932bca..f2a22b9f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs @@ -27,123 +27,150 @@ namespace MinecraftClient.Protocol.Handlers this.handler = handler; } + /// + /// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section. + /// + /// Blocks will store in this chunk + /// Cache for reading data + private Chunk ReadBlockStatesField(ref Chunk chunk, Queue cache) + { + // read Block states (Type: Paletted Container) + byte bitsPerEntry = dataTypes.ReadNextByte(cache); + + // 1.18(1.18.1) add a pattle named "Single valued" to replace the vertical strip bitmask in the old + if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC1181Version) + { + // Palettes: Single valued - 1.18(1.18.1) and above + ushort value = (ushort)dataTypes.ReadNextVarInt(cache); + + dataTypes.SkipNextVarInt(cache); // Data Array Length will be zero + + // Empty chunks will not be stored + if (new Block(value).Type == Material.Air) + return null; + + 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 bitsPerEntry bits + // EG, if bitsPerEntry = 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); + } + } + } + } + + return chunk; + } + /// /// 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) + /// Chunk mask for reading data, store in bitset, used in 1.17 and 1.17.1 /// Cache for reading chunk data - public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[] chunkMasks, int currentDimension, Queue cache) + public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[] verticalStripBitmask, Queue cache) { - int chunkColumnSize = chunkMasks.Length * 64; + var world = handler.GetWorld(); + while (world.GetDimension() == null) + ; // Dimension parsing unfinished + + int chunkColumnSize = (world.GetDimension().height + 15) / 16; + 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) + // 1.18 and above always contains all chunk section in data + // 1.17 and 1.17.1 need vertical strip bitmask to know if the chunk section is included + if ((protocolversion >= Protocol18Handler.MC1181Version) || + (((protocolversion == Protocol18Handler.MC117Version) || + (protocolversion == Protocol18Handler.MC1171Version)) && + ((verticalStripBitmask[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) + // 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); - } - } - } - } + ReadBlockStatesField(ref chunk, cache); //We have our chunk, save the chunk into the world handler.InvokeOnMainThread(() => @@ -153,11 +180,11 @@ namespace MinecraftClient.Protocol.Handlers handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; }); + // Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above if (protocolversion >= Protocol18Handler.MC1181Version) { - // skip read Biomes (Type: Paletted Container) byte bitsPerEntryBiome = dataTypes.ReadNextByte(cache); // Bits Per Entry - if (bitsPerEntryBiome == 0 && protocolversion >= Protocol18Handler.MC1181Version) + if (bitsPerEntryBiome == 0) { dataTypes.SkipNextVarInt(cache); // Value dataTypes.SkipNextVarInt(cache); // Data Array Length