From a2f7511ef73a09d83821e1b674fc467f066e4827 Mon Sep 17 00:00:00 2001 From: Anon Date: Wed, 25 Mar 2026 23:57:02 +0100 Subject: [PATCH 1/4] Optimized World.cs --- MinecraftClient/Mapping/World.cs | 10 +++--- .../config/sample-script-packet-capture.cs | 2 -- tools/run-creative-e2e.sh | 32 ++++++++++++++++--- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index 66290a40..6e4c4ecc 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -12,9 +12,9 @@ namespace MinecraftClient.Mapping { /// /// The chunks contained into the Minecraft world - /// Tuple: Tuple + /// (int ChunkX, int ChunkZ): chunkX, chunkZ /// - private ConcurrentDictionary, ChunkColumn> chunks = new(); + private ConcurrentDictionary<(int ChunkX, int ChunkZ), ChunkColumn> chunks = new(); /// /// The dimension info of the world @@ -49,12 +49,12 @@ namespace MinecraftClient.Mapping { get { - chunks.TryGetValue(new(chunkX, chunkZ), out ChunkColumn? chunkColumn); + chunks.TryGetValue((chunkX, chunkZ), out ChunkColumn? chunkColumn); return chunkColumn; } set { - Tuple chunkCoord = new(chunkX, chunkZ); + var chunkCoord = (chunkX, chunkZ); if (value is null) chunks.TryRemove(chunkCoord, out _); else @@ -361,7 +361,7 @@ namespace MinecraftClient.Mapping /// Whether the ChunkColumn has been fully loaded public void StoreChunk(int chunkX, int chunkY, int chunkZ, int chunkColumnSize, Chunk? chunk, bool loadCompleted) { - ChunkColumn chunkColumn = chunks.GetOrAdd(new(chunkX, chunkZ), (_) => new(chunkColumnSize)); + ChunkColumn chunkColumn = chunks.GetOrAdd((chunkX, chunkZ), (_) => new(chunkColumnSize)); chunkColumn[chunkY] = chunk; if (loadCompleted) chunkColumn.FullyLoaded = true; diff --git a/MinecraftClient/config/sample-script-packet-capture.cs b/MinecraftClient/config/sample-script-packet-capture.cs index 981ddb49..aa8b15ff 100644 --- a/MinecraftClient/config/sample-script-packet-capture.cs +++ b/MinecraftClient/config/sample-script-packet-capture.cs @@ -4,8 +4,6 @@ MCC.LoadBot(new PacketCadenceCaptureBot()); //MCCScript Extensions -using System.Threading; - public class PacketCadenceCaptureBot : ChatBot { private const int CaptureDurationSeconds = 5; diff --git a/tools/run-creative-e2e.sh b/tools/run-creative-e2e.sh index 35555ad9..117c3031 100644 --- a/tools/run-creative-e2e.sh +++ b/tools/run-creative-e2e.sh @@ -19,6 +19,7 @@ EOF SERVER_DIR="${1:-}" MC_VERSION="${2:-}" PROFILE="${3:-}" +SERVER_PORT="" if [[ -z "$SERVER_DIR" || -z "$MC_VERSION" || -z "$PROFILE" ]]; then usage >&2 @@ -75,6 +76,22 @@ wait_for_server_ready() { return 1 } +wait_for_rcon_port_free() { + local timeout="${1:-30}" + local elapsed=0 + + while (( elapsed < timeout )); do + if ! ss -ltn '( sport = :25575 )' 2>/dev/null | grep -Fq ':25575'; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for RCON port 25575 to become free" >&2 + return 1 +} + kill_other_servers() { local sessions sessions="$(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true)" @@ -100,16 +117,18 @@ cleanup() { fi tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true + wait_for_rcon_port_free 30 || true } trap cleanup EXIT prepare_config() { - cp "$REPO_ROOT/MinecraftClient.ini" "$CFG" + MCC_TEST_ACCOUNT_TYPE=mojang MCC_TEST_PASSWORD=- \ + bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \ + "$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" CursorBot >/dev/null sed -i \ - -e 's/Account = { Login = "test", Password = "-" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e "s/MinecraftVersion = \"auto\"/MinecraftVersion = \"$MC_VERSION\"/" \ + -e "s#^Server = .*#Server = { Host = \"localhost\", Port = $SERVER_PORT }#" \ -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ -e 's/InventoryHandling = false/InventoryHandling = true/' \ -e 's/EntityHandling = false/EntityHandling = true/' \ @@ -195,8 +214,8 @@ modern_mob_and_effects() { run_server_command "effect give CursorBot minecraft:regeneration 10 1 true" } -prepare_config kill_other_servers +wait_for_rcon_port_free 30 || true rm -f "$MCC_LOG" "$INPUT_FILE" bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null @@ -206,12 +225,15 @@ fi mc-start "$SERVER_DIR" >/dev/null wait_for_server_ready || exit 1 +SERVER_PORT="$(bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" "$SERVER_DIR")" +prepare_config : > "$INPUT_FILE" ( cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" > "$MCC_LOG" 2>&1 + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + "$CFG" CursorBot - "localhost:$SERVER_PORT" > "$MCC_LOG" 2>&1 ) & MCC_PID=$! From ca5e7520cbf794e33884008385b46944fa784ad5 Mon Sep 17 00:00:00 2001 From: Anon Date: Thu, 26 Mar 2026 14:14:14 +0100 Subject: [PATCH 2/4] Optimized SendPacket --- .../Protocol/Handlers/Protocol18.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index b9128587..4e9fdfab 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3162,26 +3162,42 @@ namespace MinecraftClient.Protocol.Handlers /// packet Data private void SendPacket(int packetId, IEnumerable packetData) { + byte[] payload = packetData as byte[] ?? packetData.ToArray(); + if (handler.GetNetworkPacketCaptureEnabled()) { - var clone = packetData.ToList(); - handler.OnNetworkPacket(packetId, clone, currentState == CurrentState.Login, false); + handler.OnNetworkPacket(packetId, payload.ToList(), currentState == CurrentState.Login, false); } //log.Info($"[C -> S] Sending packet {packetId:X} > {dataTypes.ByteArrayToString(packetData.ToArray())}"); //The inner packet - var thePacket = dataTypes.ConcatBytes(DataTypes.GetVarInt(packetId), packetData.ToArray()); + byte[] packetIdBytes = DataTypes.GetVarInt(packetId); + byte[] thePacket = new byte[packetIdBytes.Length + payload.Length]; + Buffer.BlockCopy(packetIdBytes, 0, thePacket, 0, packetIdBytes.Length); + Buffer.BlockCopy(payload, 0, thePacket, packetIdBytes.Length, payload.Length); if (compression_treshold >= 0) //Compression enabled? { - thePacket = thePacket.Length >= compression_treshold - ? dataTypes.ConcatBytes(DataTypes.GetVarInt(thePacket.Length), ZlibUtils.Compress(thePacket)) - : dataTypes.ConcatBytes(DataTypes.GetVarInt(0), thePacket); + byte[] compressedHeader = thePacket.Length >= compression_treshold + ? DataTypes.GetVarInt(thePacket.Length) + : DataTypes.GetVarInt(0); + byte[] compressedPayload = thePacket.Length >= compression_treshold + ? ZlibUtils.Compress(thePacket) + : thePacket; + + byte[] compressedPacket = new byte[compressedHeader.Length + compressedPayload.Length]; + Buffer.BlockCopy(compressedHeader, 0, compressedPacket, 0, compressedHeader.Length); + Buffer.BlockCopy(compressedPayload, 0, compressedPacket, compressedHeader.Length, compressedPayload.Length); + thePacket = compressedPacket; } //log.Debug("[C -> S] Sending packet " + packetId + " > " + dataTypes.ByteArrayToString(dataTypes.ConcatBytes(dataTypes.GetVarInt(thePacket.Length), thePacket))); - socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(thePacket.Length), thePacket)); + byte[] packetLengthBytes = DataTypes.GetVarInt(thePacket.Length); + byte[] fullPacket = new byte[packetLengthBytes.Length + thePacket.Length]; + Buffer.BlockCopy(packetLengthBytes, 0, fullPacket, 0, packetLengthBytes.Length); + Buffer.BlockCopy(thePacket, 0, fullPacket, packetLengthBytes.Length, thePacket.Length); + socketWrapper.SendDataRAW(fullPacket); } /// From 747662ea8caacabc1354a6c42eafc1e5096fa065 Mon Sep 17 00:00:00 2001 From: Anon Date: Thu, 26 Mar 2026 18:01:56 +0100 Subject: [PATCH 3/4] More minor optimizations --- MinecraftClient/Mapping/Movement.cs | 4 ++-- MinecraftClient/Physics/BlockShapes.cs | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index 6786dbee..0e972e09 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -232,8 +232,8 @@ namespace MinecraftClient.Mapping int tentativeGScore = current.GScore + (int)current.Location.DistanceSquared(neighbor); // If the neighbor is not in the GScoreDict OR its current tentativeGScore is lower than the previously saved one: - if (!gScoreDict.ContainsKey(neighbor) || - (gScoreDict.ContainsKey(neighbor) && tentativeGScore < gScoreDict[neighbor])) + if (!gScoreDict.TryGetValue(neighbor, out int existingGScore) || + tentativeGScore < existingGScore) { // Save the new relation between the neighbored block and the current one cameFrom[neighbor] = current.Location; diff --git a/MinecraftClient/Physics/BlockShapes.cs b/MinecraftClient/Physics/BlockShapes.cs index 535c1ee4..c960913c 100644 --- a/MinecraftClient/Physics/BlockShapes.cs +++ b/MinecraftClient/Physics/BlockShapes.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,7 +20,7 @@ namespace MinecraftClient.Physics private static readonly Aabb[] FullBlockArray = { FullBlock }; private static readonly Aabb[] EmptyArray = Array.Empty(); - private static Dictionary? stateToShape; + private static FrozenDictionary? stateToShape; private static Dictionary? prismarineBlocks; private static Dictionary? prismarineShapes; @@ -136,14 +137,21 @@ namespace MinecraftClient.Physics private static void BuildStateMap() { - stateToShape = new Dictionary(); + var builder = new Dictionary(); if (prismarineBlocks is null || prismarineShapes is null) + { + stateToShape = builder.ToFrozenDictionary(); return; + } var palette = Block.Palette; var dict = GetPaletteDict(palette); - if (dict is null) return; + if (dict is null) + { + stateToShape = builder.ToFrozenDictionary(); + return; + } // Group consecutive state IDs by Material to find state ranges per block var materialRanges = new Dictionary>(); @@ -182,20 +190,20 @@ namespace MinecraftClient.Physics { var shapes = prismarineShapes.GetValueOrDefault(singleShapeId, EmptyArray); for (int sid = start; sid <= end; sid++) - stateToShape[sid] = shapes; + builder[sid] = shapes; } else if (blockShapeData is List shapeIdList) { for (int i = 0; i < stateCount && (globalStateOffset + i) < shapeIdList.Count; i++) { int shapeId = shapeIdList[globalStateOffset + i]; - stateToShape[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray); + builder[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray); } } globalStateOffset += stateCount; } } - + stateToShape = builder.ToFrozenDictionary(); } /// From 7a75328f0aed6a957a5890a6888af1a0c996f9c2 Mon Sep 17 00:00:00 2001 From: Anon Date: Thu, 26 Mar 2026 19:16:48 +0100 Subject: [PATCH 4/4] Fixed a JSON exception --- MinecraftClient/Json.cs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/MinecraftClient/Json.cs b/MinecraftClient/Json.cs index da3aa838..08f5b24c 100644 --- a/MinecraftClient/Json.cs +++ b/MinecraftClient/Json.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -23,10 +24,46 @@ public static class Json public static JsonNode? ParseJson(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; + ReadOnlySpan text = json.AsSpan().TrimStart(); + if (!LooksLikeJson(text)) + return JsonValue.Create(json); + try { return JsonNode.Parse(json); } catch (JsonException) { return JsonValue.Create(json); } } + private static bool LooksLikeJson(ReadOnlySpan text) + { + if (text.IsEmpty) + return false; + + return text[0] switch + { + '{' or '"' => true, + '[' => LooksLikeJsonArray(text[1..]), + '-' => text.Length > 1 && char.IsAsciiDigit(text[1]), + >= '0' and <= '9' => true, + 't' or 'f' or 'n' => true, + _ => false + }; + } + + private static bool LooksLikeJsonArray(ReadOnlySpan text) + { + text = text.TrimStart(); + if (text.IsEmpty) + return false; + + return text[0] switch + { + ']' or '{' or '[' or '"' => true, + '-' => text.Length > 1 && char.IsAsciiDigit(text[1]), + >= '0' and <= '9' => true, + 't' or 'f' or 'n' => true, + _ => false + }; + } + /// /// Escape a string for embedding inside a JSON string literal. /// Uses System.Text.Json serialization and strips the surrounding quotes. @@ -52,4 +89,4 @@ public static class JsonNodeExtensions JsonValue val when val.TryGetValue(out var s) => s, _ => node.ToJsonString() }; -} \ No newline at end of file +}