diff --git a/MinecraftClient/Mapping/Chunk.cs b/MinecraftClient/Mapping/Chunk.cs index 3ae88b73..0ee8c7ed 100644 --- a/MinecraftClient/Mapping/Chunk.cs +++ b/MinecraftClient/Mapping/Chunk.cs @@ -20,11 +20,6 @@ namespace MinecraftClient.Mapping /// private readonly Block[,,] blocks = new Block[SizeX, SizeY, SizeZ]; - /// - /// Lock for thread safety - /// - private readonly ReaderWriterLockSlim blockLock = new ReaderWriterLockSlim(); - /// /// Read, or set the specified block /// @@ -43,15 +38,7 @@ namespace MinecraftClient.Mapping if (blockZ < 0 || blockZ >= SizeZ) throw new ArgumentOutOfRangeException("blockZ", "Must be between 0 and " + (SizeZ - 1) + " (inclusive)"); - blockLock.EnterReadLock(); - try - { - return blocks[blockX, blockY, blockZ]; - } - finally - { - blockLock.ExitReadLock(); - } + return blocks[blockX, blockY, blockZ]; } set { @@ -62,18 +49,22 @@ namespace MinecraftClient.Mapping if (blockZ < 0 || blockZ >= SizeZ) throw new ArgumentOutOfRangeException("blockZ", "Must be between 0 and " + (SizeZ - 1) + " (inclusive)"); - blockLock.EnterWriteLock(); - try - { - blocks[blockX, blockY, blockZ] = value; - } - finally - { - blockLock.ExitWriteLock(); - } + blocks[blockX, blockY, blockZ] = value; } } + /// + /// Used when parsing chunks + /// + /// Block X + /// Block Y + /// Block Z + /// Block + public void SetWithoutCheck(int blockX, int blockY, int blockZ, Block block) + { + blocks[blockX, blockY, blockZ] = block; + } + /// /// Get block at the specified location /// diff --git a/MinecraftClient/Mapping/ChunkColumn.cs b/MinecraftClient/Mapping/ChunkColumn.cs index 3c2272e3..ca828712 100644 --- a/MinecraftClient/Mapping/ChunkColumn.cs +++ b/MinecraftClient/Mapping/ChunkColumn.cs @@ -20,11 +20,6 @@ namespace MinecraftClient.Mapping /// private readonly Chunk[] chunks; - /// - /// Lock for thread safety - /// - private readonly ReaderWriterLockSlim chunkLock = new ReaderWriterLockSlim(); - /// /// Create a new ChunkColumn /// @@ -44,27 +39,11 @@ namespace MinecraftClient.Mapping { get { - chunkLock.EnterReadLock(); - try - { - return chunks[chunkY]; - } - finally - { - chunkLock.ExitReadLock(); - } + return chunks[chunkY]; } set { - chunkLock.EnterWriteLock(); - try - { - chunks[chunkY] = value; - } - finally - { - chunkLock.ExitWriteLock(); - } + chunks[chunkY] = value; } } diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index 454d5a3b..5810d2cc 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -14,17 +14,17 @@ namespace MinecraftClient.Mapping /// /// The chunks contained into the Minecraft world /// - private Dictionary> chunks = new Dictionary>(); + private Dictionary> chunks = new(); /// /// The dimension info of the world /// - private static Dimension dimension = new Dimension(); + private static Dimension dimension = new(); /// /// Lock for thread safety /// - private readonly ReaderWriterLockSlim chunksLock = new ReaderWriterLockSlim(); + private readonly ReaderWriterLockSlim chunksLock = new(); /// /// Chunk data parsing progress @@ -109,6 +109,54 @@ namespace MinecraftClient.Mapping return dimension; } + /// + /// Set chunk column at the specified location + /// + /// ChunkColumn X + /// ChunkColumn Y + /// ChunkColumn Z + /// ChunkColumn size + /// Chunk data + /// Whether the ChunkColumn has been fully loaded + public void StoreChunk(int chunkX, int chunkY, int chunkZ, int chunkColumnSize, Chunk chunk, bool loadCompleted) + { + ChunkColumn? chunkColumn = null; + + chunksLock.EnterUpgradeableReadLock(); + try + { + //Read a chunk + if (chunks.ContainsKey(chunkX)) + if (chunks[chunkX].ContainsKey(chunkZ)) + chunkColumn = chunks[chunkX][chunkZ]; + + if (chunkColumn == null) + { + chunkColumn = new ChunkColumn(chunkColumnSize); + chunksLock.EnterWriteLock(); + try + { + //Update a chunk column + if (!chunks.ContainsKey(chunkX)) + chunks[chunkX] = new Dictionary(); + chunks[chunkX][chunkZ] = chunkColumn; + } + finally + { + chunksLock.ExitWriteLock(); + } + } + } + finally + { + chunksLock.ExitUpgradeableReadLock(); + } + + chunkColumn[chunkY] = chunk; + if (loadCompleted) + chunkColumn.FullyLoaded = true; + } + /// /// Get chunk column at the specified location /// diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 334de9bc..e3b6aa63 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -16,6 +16,7 @@ using MinecraftClient.WinAPI; using MinecraftClient.Protocol.Keys; using System.Security.Cryptography; using System.Xml.Linq; +using System.Threading.Tasks; namespace MinecraftClient { @@ -84,9 +85,12 @@ namespace MinecraftClient //Take advantage of Windows 10 / Mac / Linux UTF-8 console if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // If we're on windows, check if our version is Win10 or greater. - if (WindowsVersion.WinMajorVersion >= 10) - Console.OutputEncoding = Console.InputEncoding = Encoding.UTF8; + Parallel.Invoke(() => + { + // If we're on windows, check if our version is Win10 or greater. + if (WindowsVersion.WinMajorVersion >= 10) + Console.OutputEncoding = Console.InputEncoding = Encoding.UTF8; + }); } else { diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 624924ab..2d9d7226 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -253,17 +253,15 @@ namespace MinecraftClient.Protocol.Handlers /// The integer public int ReadNextVarInt(Queue cache) { - string rawData = BitConverter.ToString(cache.ToArray()); int i = 0; int j = 0; - int k = 0; - while (true) + byte b; + do { - k = ReadNextByte(cache); - i |= (k & 0x7F) << j++ * 7; - if (j > 5) throw new OverflowException("VarInt too big " + rawData); - if ((k & 0x80) != 128) break; - } + b = cache.Dequeue(); + i |= (b & 127) << j++ * 7; + if (j > 5) throw new OverflowException("VarInt too big"); + } while ((b & 128) == 128); return i; } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 5ba94832..b3bd4ff0 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -558,12 +558,19 @@ namespace MinecraftClient.Protocol.Handlers int dataSize = dataTypes.ReadNextVarInt(packetData); // Size - new Task(() => + Parallel.Invoke(() => { bool loaded = pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData, cancellationToken); if (loaded) + { Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - }).Start(); + if (handler.GetWorld().chunkCnt == 464) + { + log.Info("Loaded 464 chunks, exit"); + System.Environment.Exit(0); + } + } + }); // Block Entity data: ignored // Light data: ignored @@ -582,12 +589,12 @@ namespace MinecraftClient.Protocol.Handlers int compressedDataSize = dataTypes.ReadNextInt(packetData); byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); byte[] decompressed = ZlibUtils.Decompress(compressed); - new Task(() => + Parallel.Invoke(() => { bool loaded = pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue(decompressed), cancellationToken); if (loaded) Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - }).Start(); + }); } else { @@ -611,12 +618,12 @@ namespace MinecraftClient.Protocol.Handlers else dataTypes.ReadData(1024 * 4, packetData); // Biomes - 1.15 and above } int dataSize = dataTypes.ReadNextVarInt(packetData); - new Task(() => + Parallel.Invoke(() => { bool loaded = pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData, cancellationToken); if (loaded) Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); - }).Start(); + }); } } } @@ -872,7 +879,7 @@ namespace MinecraftClient.Protocol.Handlers } //Process chunk records - new Task(() => + Parallel.Invoke(() => { for (int chunkColumnNo = 0; chunkColumnNo < chunkCount; chunkColumnNo++) { @@ -882,7 +889,7 @@ namespace MinecraftClient.Protocol.Handlers if (loaded) Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); } - }).Start(); + }); } break; diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs index 015a5538..06109f79 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using System.Threading; //using System.Linq; //using System.Text; @@ -42,24 +43,19 @@ namespace MinecraftClient.Protocol.Handlers if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC_1_18_1_Version) { // Palettes: Single valued - 1.18(1.18.1) and above - ushort value = (ushort)dataTypes.ReadNextVarInt(cache); + ushort blockId = (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) + if (new Block(blockId).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); - } - } - } + chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(blockId)); + } else { @@ -129,7 +125,7 @@ namespace MinecraftClient.Protocol.Handlers } // We have our block, save the block into the chunk - chunk[blockX, blockY, blockZ] = new Block(blockId); + chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(blockId)); } } } @@ -152,7 +148,7 @@ namespace MinecraftClient.Protocol.Handlers if (cancellationToken.IsCancellationRequested) return false; - var world = handler.GetWorld(); + World world = handler.GetWorld(); int chunkColumnSize = (World.GetDimension().height + 15) / 16; // Round up @@ -167,10 +163,8 @@ namespace MinecraftClient.Protocol.Handlers // 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.MC_1_18_1_Version) || - (((protocolversion == Protocol18Handler.MC_1_17_Version) || - (protocolversion == Protocol18Handler.MC_1_17_1_Version)) && - ((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0))) + if ((protocolversion >= Protocol18Handler.MC_1_18_1_Version) || + ((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0)) { // Non-air block count inside chunk section, for lighting purposes int blockCnt = dataTypes.ReadNextShort(cache); @@ -186,9 +180,7 @@ namespace MinecraftClient.Protocol.Handlers //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; + world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == (chunkColumnSize - 1)); }); // Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above @@ -218,11 +210,6 @@ namespace MinecraftClient.Protocol.Handlers // 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) } - handler.InvokeOnMainThread(() => - { - if (handler.GetWorld()[chunkX, chunkZ] != null) - handler.GetWorld()[chunkX, chunkZ]!.FullyLoaded = true; - }); return true; } @@ -244,12 +231,15 @@ namespace MinecraftClient.Protocol.Handlers if (cancellationToken.IsCancellationRequested) return false; + World world = handler.GetWorld(); + const int chunkColumnSize = 16; if (protocolversion >= Protocol18Handler.MC_1_9_Version) { // 1.9 and above chunk format // Unloading chunks is handled by a separate packet - for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) + int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask); + for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) { if (cancellationToken.IsCancellationRequested) return false; @@ -363,7 +353,7 @@ namespace MinecraftClient.Protocol.Handlers } // We have our block, save the block into the chunk - chunk[blockX, blockY, blockZ] = new Block(blockId); + chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(blockId)); } } } @@ -376,9 +366,7 @@ namespace MinecraftClient.Protocol.Handlers //We have our chunk, save the chunk into the world handler.InvokeOnMainThread(() => { - if (handler.GetWorld()[chunkX, chunkZ] == null) - handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); - handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; + world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY); }); //Pre-1.14 Lighting data @@ -406,13 +394,14 @@ namespace MinecraftClient.Protocol.Handlers //Unload the entire chunk column handler.InvokeOnMainThread(() => { - handler.GetWorld()[chunkX, chunkZ] = null; + world[chunkX, chunkZ] = null; }); } else { //Load chunk data from the server - for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) + int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask); + for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) { if (cancellationToken.IsCancellationRequested) return false; @@ -426,7 +415,7 @@ namespace MinecraftClient.Protocol.Handlers 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()); + chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(queue.Dequeue())); // check before store chunk if (cancellationToken.IsCancellationRequested) @@ -435,9 +424,7 @@ namespace MinecraftClient.Protocol.Handlers //We have our chunk, save the chunk into the world handler.InvokeOnMainThread(() => { - if (handler.GetWorld()[chunkX, chunkZ] == null) - handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); - handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; + world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY); }); } } @@ -469,7 +456,7 @@ namespace MinecraftClient.Protocol.Handlers //Unload the entire chunk column handler.InvokeOnMainThread(() => { - handler.GetWorld()[chunkX, chunkZ] = null; + world[chunkX, chunkZ] = null; }); } else @@ -505,7 +492,8 @@ namespace MinecraftClient.Protocol.Handlers dataTypes.ReadData(Chunk.SizeX * Chunk.SizeZ, cache); //Biomes //Load chunk data - for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) + int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask); + for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) { if ((chunkMask & (1 << chunkY)) != 0) { @@ -514,7 +502,7 @@ namespace MinecraftClient.Protocol.Handlers 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(blockTypes.Dequeue(), blockMeta.Dequeue()); + chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(blockTypes.Dequeue(), blockMeta.Dequeue())); // check before store chunk if (cancellationToken.IsCancellationRequested) @@ -522,19 +510,12 @@ namespace MinecraftClient.Protocol.Handlers handler.InvokeOnMainThread(() => { - if (handler.GetWorld()[chunkX, chunkZ] == null) - handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); - handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; + world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY); }); } } } } - handler.InvokeOnMainThread(() => - { - if (handler.GetWorld()[chunkX, chunkZ] != null) - handler.GetWorld()[chunkX, chunkZ]!.FullyLoaded = true; - }); return true; } } diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 6f0f6654..f0794927 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -7,6 +7,8 @@ using System.Text.RegularExpressions; using MinecraftClient.Protocol.Session; using MinecraftClient.Protocol; using MinecraftClient.Mapping; +using System.Threading.Tasks; +using System.Collections.Concurrent; namespace MinecraftClient { @@ -261,29 +263,39 @@ namespace MinecraftClient try { string[] Lines = File.ReadAllLines(file); - Section section = Section.Default; - foreach (string lineRAW in Lines) - { - string line = section == Section.Main && lineRAW.ToLower().Trim().StartsWith("password") - ? lineRAW.Trim() //Do not strip # in passwords - : lineRAW.Split('#')[0].Trim(); - if (line.Length > 0) + ConcurrentDictionary sectionInfo = new(); + Parallel.For(0, Lines.Length, idx => + { + string line = Lines[idx].Split('#')[0].Trim(); + if (line.Length > 2 && line[0] == '[' && line[^1] == ']') + sectionInfo[idx] = GetSection(line[1..^1]); + }); + + Section section = Section.Default; + for (int idx = 0; idx < Lines.Length; ++idx) + { + if (sectionInfo.ContainsKey(idx)) { - if (line[0] == '[' && line[line.Length - 1] == ']') - { - section = GetSection(line.Substring(1, line.Length - 2).ToLower()); - } - else + section = sectionInfo[idx]; + continue; + } + + Parallel.Invoke(() => + { + string line = Lines[idx].Split('#')[0].Trim(); + if (line.Length > 1) { string argName = line.Split('=')[0]; + if (section == Section.Main && argName == "password") + line = Lines[idx].Trim(); //Do not strip # in passwords if (line.Length > (argName.Length + 1)) { - string argValue = line.Substring(argName.Length + 1); + string argValue = line[(argName.Length + 1)..]; LoadSingleSetting(section, argName, argValue); } } - } + }); } } catch (IOException) { }