From 01ef9a89ca1a3a58ff3d4abb3541d87701fc4f00 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Wed, 24 Aug 2022 18:16:16 +0800 Subject: [PATCH] Bug fix: Cancel chunk load task when switching worlds --- MinecraftClient/Commands/Move.cs | 7 +- MinecraftClient/Mapping/ChunkColumn.cs | 2 +- MinecraftClient/Mapping/World.cs | 18 ++--- MinecraftClient/McClient.cs | 21 ++++++ .../Protocol/Handlers/Protocol18.cs | 43 ++++++++---- .../Protocol/Handlers/Protocol18Terrain.cs | 66 ++++++++++++++++--- .../Protocol/IMinecraftComHandler.cs | 1 + 7 files changed, 127 insertions(+), 31 deletions(-) diff --git a/MinecraftClient/Commands/Move.cs b/MinecraftClient/Commands/Move.cs index 6e9e7e2b..101b6845 100644 --- a/MinecraftClient/Commands/Move.cs +++ b/MinecraftClient/Commands/Move.cs @@ -71,6 +71,11 @@ namespace MinecraftClient.Commands case "get": return handler.GetCurrentLocation().ToString(); default: return Translations.Get("cmd.look.unknown", args[0]); } + + Location goal = Movement.Move(handler.GetCurrentLocation(), direction); + if (handler.GetWorld().GetChunkColumn(goal) == null || handler.GetWorld().GetChunkColumn(goal)!.FullyLoaded == false) + return Translations.Get("cmd.move.chunk_not_loaded"); + if (Movement.CanMove(handler.GetWorld(), handler.GetCurrentLocation(), direction)) { if (handler.MoveTo(Movement.Move(handler.GetCurrentLocation(), direction), allowUnsafe: takeRisk)) @@ -88,7 +93,7 @@ namespace MinecraftClient.Commands int z = int.Parse(args[2]); Location goal = new Location(x, y, z); - if (handler.GetWorld().GetChunkColumn(goal) == null || handler.GetWorld().GetChunkColumn(goal).FullyLoaded == false) + if (handler.GetWorld().GetChunkColumn(goal) == null || handler.GetWorld().GetChunkColumn(goal)!.FullyLoaded == false) return Translations.Get("cmd.move.chunk_not_loaded"); Location current = handler.GetCurrentLocation(); diff --git a/MinecraftClient/Mapping/ChunkColumn.cs b/MinecraftClient/Mapping/ChunkColumn.cs index 5cbeda73..3c2272e3 100644 --- a/MinecraftClient/Mapping/ChunkColumn.cs +++ b/MinecraftClient/Mapping/ChunkColumn.cs @@ -73,7 +73,7 @@ namespace MinecraftClient.Mapping /// /// Location, a modulo will be applied /// The chunk, or null if not loaded - public Chunk GetChunk(Location location) + public Chunk? GetChunk(Location location) { try { diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index aee80a25..454d5a3b 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -36,9 +36,9 @@ namespace MinecraftClient.Mapping /// Read, set or unload the specified chunk column /// /// ChunkColumn X - /// ChunkColumn Y + /// ChunkColumn Z /// chunk at the given location - public ChunkColumn this[int chunkX, int chunkZ] + public ChunkColumn? this[int chunkX, int chunkZ] { get { @@ -114,7 +114,7 @@ namespace MinecraftClient.Mapping /// /// Location to retrieve chunk column /// The chunk column - public ChunkColumn GetChunkColumn(Location location) + public ChunkColumn? GetChunkColumn(Location location) { return this[location.ChunkX, location.ChunkZ]; } @@ -126,10 +126,10 @@ namespace MinecraftClient.Mapping /// Block at specified location or Air if the location is not loaded public Block GetBlock(Location location) { - ChunkColumn column = GetChunkColumn(location); + ChunkColumn? column = GetChunkColumn(location); if (column != null) { - Chunk chunk = column.GetChunk(location); + Chunk? chunk = column.GetChunk(location); if (chunk != null) return chunk.GetBlock(location); } @@ -188,10 +188,10 @@ namespace MinecraftClient.Mapping /// Block to set public void SetBlock(Location location, Block block) { - ChunkColumn column = this[location.ChunkX, location.ChunkZ]; - if (column != null) + ChunkColumn? column = this[location.ChunkX, location.ChunkZ]; + if (column != null && column.ColumnSize >= location.ChunkY) { - Chunk chunk = column[location.ChunkY]; + Chunk? chunk = column.GetChunk(location); if (chunk == null) column[location.ChunkY] = chunk = new Chunk(); chunk[location.ChunkBlockX, location.ChunkBlockY, location.ChunkBlockZ] = block; @@ -207,6 +207,8 @@ namespace MinecraftClient.Mapping try { chunks = new Dictionary>(); + chunkCnt = 0; + chunkLoadNotCompleted = 0; } finally { diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index abc001e8..bcfe8b6e 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -60,6 +60,7 @@ namespace MinecraftClient private float playerYaw; private float playerPitch; private double motionY; + private CancellationTokenSource chunkProcessCancelSource = new(); public enum MovementType { Sneak, Walk, Sprint} public int currentMovementSpeed = 4; private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..) @@ -111,6 +112,7 @@ namespace MinecraftClient public int GetSequenceId() { return sequenceId; } public float GetPitch() { return playerPitch; } public World GetWorld() { return world; } + public CancellationToken GetChunkProcessCancelToken() { return chunkProcessCancelSource.Token; } public Double GetServerTPS() { return averageTPS; } public bool GetIsSupportPreviewsChat() { return isSupportPreviewsChat; } public float GetHealth() { return playerHealth; } @@ -475,7 +477,9 @@ namespace MinecraftClient /// public void OnConnectionLost(ChatBot.DisconnectReason reason, string message) { + chunkProcessCancelSource.Cancel(); world.Clear(); + chunkProcessCancelSource = new(); if (timeoutdetector != null) { @@ -768,6 +772,17 @@ namespace MinecraftClient InvokeOnMainThread(() => { task(); return true; }); } + /// + /// Clear all tasks + /// + public void ClearTasks() + { + lock (threadTasksLock) + { + threadTasks.Clear(); + } + } + /// /// Check if running on a different thread and InvokeOnMainThread is required /// @@ -892,7 +907,9 @@ namespace MinecraftClient terrainAndMovementsEnabled = false; terrainAndMovementsRequested = false; locationReceived = false; + chunkProcessCancelSource.Cancel(); world.Clear(); + chunkProcessCancelSource = new(); } return true; } @@ -1922,6 +1939,8 @@ namespace MinecraftClient /// public void OnRespawn() { + ClearTasks(); + if (terrainAndMovementsRequested) { terrainAndMovementsEnabled = true; @@ -1931,7 +1950,9 @@ namespace MinecraftClient if (terrainAndMovementsEnabled) { + chunkProcessCancelSource.Cancel(); world.Clear(); + chunkProcessCancelSource = new(); } entities.Clear(); diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index ecb3dfed..5ba94832 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -533,6 +533,11 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.ChunkData: if (handler.GetTerrainEnabled()) { + CancellationToken cancellationToken = handler.GetChunkProcessCancelToken(); + + Interlocked.Increment(ref handler.GetWorld().chunkCnt); + Interlocked.Increment(ref handler.GetWorld().chunkLoadNotCompleted); + int chunkX = dataTypes.ReadNextInt(packetData); int chunkZ = dataTypes.ReadNextInt(packetData); if (protocolversion >= MC_1_17_Version) @@ -540,7 +545,7 @@ namespace MinecraftClient.Protocol.Handlers ulong[]? verticalStripBitmask = null; if (protocolversion == MC_1_17_Version || protocolversion == MC_1_17_1_Version) - verticalStripBitmask = dataTypes.ReadNextULongArray(packetData); // Bit Mask Le:ngth and Primary Bit Mask + verticalStripBitmask = dataTypes.ReadNextULongArray(packetData); // Bit Mask Length and Primary Bit Mask dataTypes.ReadNextNbt(packetData); // Heightmaps @@ -548,20 +553,20 @@ namespace MinecraftClient.Protocol.Handlers { int biomesLength = dataTypes.ReadNextVarInt(packetData); // Biomes length for (int i = 0; i < biomesLength; i++) - { dataTypes.SkipNextVarInt(packetData); // Biomes - } } int dataSize = dataTypes.ReadNextVarInt(packetData); // Size - Interlocked.Increment(ref handler.GetWorld().chunkCnt); - Interlocked.Increment(ref handler.GetWorld().chunkLoadNotCompleted); new Task(() => { - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData); - Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); + bool loaded = pTerrain.ProcessChunkColumnData(chunkX, chunkZ, verticalStripBitmask, packetData, cancellationToken); + if (loaded) + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); }).Start(); + + // Block Entity data: ignored + // Light data: ignored } else { @@ -579,7 +584,9 @@ namespace MinecraftClient.Protocol.Handlers byte[] decompressed = ZlibUtils.Decompress(compressed); new Task(() => { - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue(decompressed)); + 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 @@ -606,7 +613,9 @@ namespace MinecraftClient.Protocol.Handlers int dataSize = dataTypes.ReadNextVarInt(packetData); new Task(() => { - pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); + bool loaded = pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData, cancellationToken); + if (loaded) + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); }).Start(); } } @@ -825,6 +834,8 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.MapChunkBulk: if (protocolversion < MC_1_9_Version && handler.GetTerrainEnabled()) { + CancellationToken cancellationToken = handler.GetChunkProcessCancelToken(); + int chunkCount; bool hasSkyLight; Queue chunkData = packetData; @@ -861,8 +872,18 @@ namespace MinecraftClient.Protocol.Handlers } //Process chunk records - for (int chunkColumnNo = 0; chunkColumnNo < chunkCount; chunkColumnNo++) - pTerrain.ProcessChunkColumnData(chunkXs[chunkColumnNo], chunkZs[chunkColumnNo], chunkMasks[chunkColumnNo], addBitmaps[chunkColumnNo], hasSkyLight, true, currentDimension, chunkData); + new Task(() => + { + for (int chunkColumnNo = 0; chunkColumnNo < chunkCount; chunkColumnNo++) + { + if (cancellationToken.IsCancellationRequested) + break; + bool loaded = pTerrain.ProcessChunkColumnData(chunkXs[chunkColumnNo], chunkZs[chunkColumnNo], chunkMasks[chunkColumnNo], addBitmaps[chunkColumnNo], hasSkyLight, true, currentDimension, chunkData, cancellationToken); + if (loaded) + Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); + } + }).Start(); + } break; case PacketTypesIn.UnloadChunk: diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs index 1ccabb2e..015a5538 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.Threading; //using System.Linq; //using System.Text; using MinecraftClient.Mapping; @@ -32,7 +33,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Blocks will store in this chunk /// Cache for reading data - private Chunk ReadBlockStatesField(ref Chunk chunk, Queue cache) + private Chunk? ReadBlockStatesField(ref Chunk chunk, Queue cache) { // read Block states (Type: Paletted Container) byte bitsPerEntry = dataTypes.ReadNextByte(cache); @@ -144,8 +145,13 @@ namespace MinecraftClient.Protocol.Handlers /// Chunk Z location /// 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[] verticalStripBitmask, Queue cache) + /// token to cancel the task + /// true if successfully loaded + public bool ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue cache, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + return false; + var world = handler.GetWorld(); int chunkColumnSize = (World.GetDimension().height + 15) / 16; // Round up @@ -156,12 +162,15 @@ namespace MinecraftClient.Protocol.Handlers // Unloading chunks is handled by a separate packet for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { + if (cancellationToken.IsCancellationRequested) + return false; + // 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))) + ((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0))) { // Non-air block count inside chunk section, for lighting purposes int blockCnt = dataTypes.ReadNextShort(cache); @@ -170,12 +179,16 @@ namespace MinecraftClient.Protocol.Handlers Chunk chunk = new Chunk(); ReadBlockStatesField(ref chunk, cache); + // check before store chunk + if (cancellationToken.IsCancellationRequested) + return false; + //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; + handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; }); // Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above @@ -205,7 +218,12 @@ 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.GetWorld()[chunkX, chunkZ].FullyLoaded = true; + handler.InvokeOnMainThread(() => + { + if (handler.GetWorld()[chunkX, chunkZ] != null) + handler.GetWorld()[chunkX, chunkZ]!.FullyLoaded = true; + }); + return true; } /// @@ -219,8 +237,13 @@ namespace MinecraftClient.Protocol.Handlers /// Are the chunk continuous /// Current dimension type (0 = overworld) /// Cache for reading chunk data - public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue cache) + /// token to cancel the task + /// true if successfully loaded + public bool ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue cache, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + return false; + const int chunkColumnSize = 16; if (protocolversion >= Protocol18Handler.MC_1_9_Version) { @@ -228,6 +251,9 @@ namespace MinecraftClient.Protocol.Handlers // Unloading chunks is handled by a separate packet for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { + if (cancellationToken.IsCancellationRequested) + return false; + if ((chunkMask & (1 << chunkY)) != 0) { // 1.14 and above Non-air block count inside chunk section, for lighting purposes @@ -343,12 +369,16 @@ namespace MinecraftClient.Protocol.Handlers } } + // check before store chunk + if (cancellationToken.IsCancellationRequested) + return false; + //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; + handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; }); //Pre-1.14 Lighting data @@ -384,6 +414,9 @@ namespace MinecraftClient.Protocol.Handlers //Load chunk data from the server for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) { + if (cancellationToken.IsCancellationRequested) + return false; + if ((chunkMask & (1 << chunkY)) != 0) { Chunk chunk = new Chunk(); @@ -395,12 +428,16 @@ namespace MinecraftClient.Protocol.Handlers for (int blockX = 0; blockX < Chunk.SizeX; blockX++) chunk[blockX, blockY, blockZ] = new Block(queue.Dequeue()); + // check before store chunk + if (cancellationToken.IsCancellationRequested) + return false; + //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; + handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; }); } } @@ -479,17 +516,26 @@ namespace MinecraftClient.Protocol.Handlers for (int blockX = 0; blockX < Chunk.SizeX; blockX++) chunk[blockX, blockY, blockZ] = new Block(blockTypes.Dequeue(), blockMeta.Dequeue()); + // check before store chunk + if (cancellationToken.IsCancellationRequested) + return false; + handler.InvokeOnMainThread(() => { if (handler.GetWorld()[chunkX, chunkZ] == null) handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); - handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; + handler.GetWorld()[chunkX, chunkZ]![chunkY] = chunk; }); } } } } - handler.GetWorld()[chunkX, chunkZ].FullyLoaded = true; + handler.InvokeOnMainThread(() => + { + if (handler.GetWorld()[chunkX, chunkZ] != null) + handler.GetWorld()[chunkX, chunkZ]!.FullyLoaded = true; + }); + return true; } } } diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 96b4e4b7..60300976 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -29,6 +29,7 @@ namespace MinecraftClient.Protocol PlayerInfo? GetPlayerInfo(Guid uuid); Location GetCurrentLocation(); World GetWorld(); + public System.Threading.CancellationToken GetChunkProcessCancelToken(); bool GetIsSupportPreviewsChat(); bool GetTerrainEnabled(); bool SetTerrainEnabled(bool enabled);