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);