Minecraft-Console-Client/MinecraftClient/Mapping/World.cs

512 lines
23 KiB
C#
Raw Normal View History

Fix RegistryData parsing and KnownDataPacks negotiation for 1.20.6 Two critical issues in the 1.20.6 configuration phase that could cause connection instability and packet desync: 1. RegistryData: The handler used an early `break` when it encountered a registryId other than "minecraft:dimension_type" or "minecraft:chat_type". This skipped reading the remaining entries for that registry, leaving unconsumed data in the packet buffer. Subsequent packet reads would start at the wrong offset, causing cascading parse failures and eventual disconnection. Fix: Always read all entries (entryId + hasData + optional NBT) for every registry, regardless of whether we process it. For dimension_type entries, if the server sends inline NBT data (i.e. non-vanilla dimensions from mods/datapacks), parse and store the dimension directly via World.StoreOneDimension(). Only fall back to hardcoded defaults when no dimension data was received. 2. KnownDataPacks: The client echoed back ALL packs the server listed, including non-vanilla ones. This told the server "I have these packs cached" when the client actually did not, so the server would skip sending full registry data for those packs. The result: incomplete registries for modded/datapack content. Fix: Filter the response to only include packs with the "minecraft" namespace. Non-vanilla packs are omitted, forcing the server to send their full registry data inline. Also adds supporting methods to World.cs: - SetDimensionIdMap(): Store VarInt ID -> dimension name mapping from RegistryData entries (needed by JoinGame/Respawn) - GetDimensionNameById(): Look up dimension name by numeric ID - HasAnyDimension(): Check if any dimensions were loaded from server-provided data Made-with: Cursor
2026-03-19 00:13:22 +08:00
using System;
2022-08-31 18:00:00 +08:00
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace MinecraftClient.Mapping
{
/// <summary>
/// Represents a Minecraft World
/// </summary>
public class World
{
/// <summary>
/// The chunks contained into the Minecraft world
2026-03-25 23:57:02 +01:00
/// (int ChunkX, int ChunkZ): chunkX, chunkZ
/// </summary>
2026-03-25 23:57:02 +01:00
private ConcurrentDictionary<(int ChunkX, int ChunkZ), ChunkColumn> chunks = new();
/// <summary>
/// The dimension info of the world
/// </summary>
private static Dimension curDimension = new();
2022-08-28 22:27:21 +08:00
private static readonly Dictionary<string, Dimension> dimensionList = new();
Fix RegistryData parsing and KnownDataPacks negotiation for 1.20.6 Two critical issues in the 1.20.6 configuration phase that could cause connection instability and packet desync: 1. RegistryData: The handler used an early `break` when it encountered a registryId other than "minecraft:dimension_type" or "minecraft:chat_type". This skipped reading the remaining entries for that registry, leaving unconsumed data in the packet buffer. Subsequent packet reads would start at the wrong offset, causing cascading parse failures and eventual disconnection. Fix: Always read all entries (entryId + hasData + optional NBT) for every registry, regardless of whether we process it. For dimension_type entries, if the server sends inline NBT data (i.e. non-vanilla dimensions from mods/datapacks), parse and store the dimension directly via World.StoreOneDimension(). Only fall back to hardcoded defaults when no dimension data was received. 2. KnownDataPacks: The client echoed back ALL packs the server listed, including non-vanilla ones. This told the server "I have these packs cached" when the client actually did not, so the server would skip sending full registry data for those packs. The result: incomplete registries for modded/datapack content. Fix: Filter the response to only include packs with the "minecraft" namespace. Non-vanilla packs are omitted, forcing the server to send their full registry data inline. Also adds supporting methods to World.cs: - SetDimensionIdMap(): Store VarInt ID -> dimension name mapping from RegistryData entries (needed by JoinGame/Respawn) - GetDimensionNameById(): Look up dimension name by numeric ID - HasAnyDimension(): Check if any dimensions were loaded from server-provided data Made-with: Cursor
2026-03-19 00:13:22 +08:00
/// <summary>
/// VarInt ID → dimension name mapping, populated from RegistryData in 1.20.6+
/// </summary>
private static Dictionary<int, string> dimensionIdMap = new();
Dynamically parse minecraft:attribute registry from server RegistryData In 1.20.6+, EntityProperties packets reference attributes by VarInt registry IDs instead of string names. Previously, a hardcoded dictionary of 22 attribute entries (matching the vanilla 1.20.6 registry) was used to map these IDs back to names. This works for vanilla servers but would fail silently for modded servers that add custom attributes — any unknown ID would be reported as "unknown". This commit replaces the hardcoded attribute dictionary with dynamic registry parsing, following the same pattern already used for dimension_type and chat_type registries: - World.cs: Add static `attributeIdMap` field, `SetAttributeIdMap()` and `GetAttributeNameById()` methods for storing/querying attribute names by their VarInt registry IDs. - Protocol18.cs (RegistryData handler): When the server sends a `minecraft:attribute` registry during the Configuration phase, parse all entries and store the ID→name mapping. The `minecraft:` prefix is stripped from entry names to match the format used in EntityProperties packets (e.g. "minecraft:generic.armor" → "generic.armor"). - Protocol18.cs (EntityProperties handler): Remove the hardcoded 22-entry `attributeDictionary` and use `World.GetAttributeNameById()` instead. Unknown IDs still fall back to "unknown" for safety. Also closes issue #4 (Disconnect packet extra boolean) — verified that both Play and Configuration phase Disconnect handlers already use `ReadNextChat()` (NBT format since 1.20.4+), matching the 1.20.6 protocol spec. No code changes needed; updated tracking document to mark as closed. Made-with: Cursor
2026-03-19 01:12:18 +08:00
/// <summary>
/// VarInt ID → attribute name mapping, populated from RegistryData (minecraft:attribute) in 1.20.6+
/// </summary>
private static Dictionary<int, string> attributeIdMap = new();
2022-07-25 03:19:24 +08:00
/// <summary>
/// Chunk data parsing progress
/// </summary>
public int chunkCnt = 0;
public int chunkLoadNotCompleted = 0;
2022-07-25 03:19:24 +08:00
/// <summary>
/// Read, set or unload the specified chunk column
/// </summary>
/// <param name="chunkX">ChunkColumn X</param>
/// <param name="chunkZ">ChunkColumn Z</param>
/// <returns>chunk at the given location</returns>
public ChunkColumn? this[int chunkX, int chunkZ]
{
get
{
2026-03-25 23:57:02 +01:00
chunks.TryGetValue((chunkX, chunkZ), out ChunkColumn? chunkColumn);
2022-08-31 18:00:00 +08:00
return chunkColumn;
}
set
{
2026-03-25 23:57:02 +01:00
var chunkCoord = (chunkX, chunkZ);
if (value is null)
2022-08-31 19:50:11 +08:00
chunks.TryRemove(chunkCoord, out _);
2022-08-31 18:00:00 +08:00
else
chunks.AddOrUpdate(chunkCoord, value, (_, _) => value);
}
}
2022-08-28 22:27:21 +08:00
/// <summary>
2022-08-28 22:27:21 +08:00
/// Storage of all dimensional data - 1.19.1 and above
/// </summary>
/// <param name="registryCodec">Registry Codec nbt data</param>
public static void StoreDimensionList(Dictionary<string, object> registryCodec)
{
const string namespacedDimensionTypeKey = "minecraft:dimension_type";
const string legacyDimensionTypeKey = "dimension_type";
if (!registryCodec.TryGetValue(namespacedDimensionTypeKey, out var dimensionTypeRegistry)
&& !registryCodec.TryGetValue(legacyDimensionTypeKey, out dimensionTypeRegistry))
{
return;
}
var dimensionListNbt = (object[])(((Dictionary<string, object>)dimensionTypeRegistry)["value"]);
foreach (var (dimensionName, dimensionType) in from Dictionary<string, object> dimensionNbt in dimensionListNbt
let dimensionName = (string)dimensionNbt["name"]
let dimensionType = (Dictionary<string, object>)dimensionNbt["element"]
select (dimensionName, dimensionType))
2022-08-28 22:27:21 +08:00
{
2022-09-07 00:08:31 +08:00
StoreOneDimension(dimensionName, dimensionType);
2022-08-28 22:27:21 +08:00
}
}
public static void LoadDefaultDimensions1206Plus()
{
// TODO: Move this to a JSON file.
var defaultRegistryCodec = new Dictionary<string, object>
{
{ "minecraft:dimension_type", new Dictionary<string, object>
{
{ "value", new object[]
{
new Dictionary<string, object>
{
{ "name", "minecraft:overworld" },
{ "id", 0 },
{ "element", new Dictionary<string, object>
{
{ "piglin_safe", (byte)0 },
{ "natural", 1 },
{ "ambient_light", 0.0 },
{ "monster_spawn_block_light_limit", 0 },
{ "infiniburn", "#minecraft:infiniburn_overworld" },
{ "respawn_anchor_works", 0 },
{ "has_skylight", 1 },
{ "bed_works", 1 },
{ "effects", "minecraft:overworld" },
{ "has_raids", 1 },
{ "logical_height", 384 },
{ "coordinate_scale", 1.0 },
{ "monster_spawn_light_level", new Dictionary<string, object>
{
{ "min_inclusive", 0 },
{ "max_inclusive", 7 },
{ "type", "minecraft:uniform" }
}
},
{ "min_y", -64 },
{ "ultrawarm", 0 },
{ "has_ceiling", 0 },
{ "height", 384 }
}
}
},
new Dictionary<string, object>
{
{ "name", "minecraft:overworld_caves" },
{ "id", 1 },
{ "element", new Dictionary<string, object>
{
{ "piglin_safe", (byte)0 },
{ "natural", 1 },
{ "ambient_light", 0.0 },
{ "monster_spawn_block_light_limit", 0 },
{ "infiniburn", "#minecraft:infiniburn_overworld" },
{ "respawn_anchor_works", 0 },
{ "has_skylight", 1 },
{ "bed_works", 1 },
{ "effects", "minecraft:overworld" },
{ "has_raids", 1 },
{ "logical_height", 384 },
{ "coordinate_scale", 1.0 },
{ "monster_spawn_light_level", new Dictionary<string, object>
{
{ "min_inclusive", 0 },
{ "max_inclusive", 7 },
{ "type", "minecraft:uniform" }
}
},
{ "min_y", -64 },
{ "ultrawarm", 0 },
{ "has_ceiling", 1 },
{ "height", 384 }
}
}
},
new Dictionary<string, object>
{
{ "name", "minecraft:the_end" },
{ "id", 2 },
{ "element", new Dictionary<string, object>
{
{ "piglin_safe", (byte)0 },
{ "natural", 0 },
{ "ambient_light", 0.0 },
{ "monster_spawn_block_light_limit", 0 },
{ "infiniburn", "#minecraft:infiniburn_end" },
{ "respawn_anchor_works", 0 },
{ "has_skylight", 0 },
{ "bed_works", 0 },
{ "effects", "minecraft:the_end" },
{ "fixed_time", 6000 },
{ "has_raids", 1 },
{ "logical_height", 256 },
{ "coordinate_scale", 1.0 },
{ "monster_spawn_light_level", new Dictionary<string, object>
{
{ "min_inclusive", 0 },
{ "max_inclusive", 7 },
{ "type", "minecraft:uniform" }
}
},
{ "min_y", 0 },
{ "ultrawarm", 0 },
{ "has_ceiling", 0 },
{ "height", 256 }
}
}
},
new Dictionary<string, object>
{
{ "name", "minecraft:the_nether" },
{ "id", 3 },
{ "element", new Dictionary<string, object>
{
{ "piglin_safe", (byte)1 },
{ "natural", 0 },
{ "ambient_light", 0.1 },
{ "monster_spawn_block_light_limit", 15 },
{ "infiniburn", "#minecraft:infiniburn_nether" },
{ "respawn_anchor_works", 1 },
{ "has_skylight", 0 },
{ "bed_works", 0 },
{ "effects", "minecraft:the_nether" },
{ "fixed_time", 18000 },
{ "has_raids", 0 },
{ "logical_height", 128 },
{ "coordinate_scale", 8.0 },
{ "monster_spawn_light_level", 7 },
{ "min_y", 0 },
{ "ultrawarm", 1 },
{ "has_ceiling", 1 },
{ "height", 256 }
}
}
}
}
}
}
}
};
StoreDimensionList(defaultRegistryCodec);
}
Fix RegistryData parsing and KnownDataPacks negotiation for 1.20.6 Two critical issues in the 1.20.6 configuration phase that could cause connection instability and packet desync: 1. RegistryData: The handler used an early `break` when it encountered a registryId other than "minecraft:dimension_type" or "minecraft:chat_type". This skipped reading the remaining entries for that registry, leaving unconsumed data in the packet buffer. Subsequent packet reads would start at the wrong offset, causing cascading parse failures and eventual disconnection. Fix: Always read all entries (entryId + hasData + optional NBT) for every registry, regardless of whether we process it. For dimension_type entries, if the server sends inline NBT data (i.e. non-vanilla dimensions from mods/datapacks), parse and store the dimension directly via World.StoreOneDimension(). Only fall back to hardcoded defaults when no dimension data was received. 2. KnownDataPacks: The client echoed back ALL packs the server listed, including non-vanilla ones. This told the server "I have these packs cached" when the client actually did not, so the server would skip sending full registry data for those packs. The result: incomplete registries for modded/datapack content. Fix: Filter the response to only include packs with the "minecraft" namespace. Non-vanilla packs are omitted, forcing the server to send their full registry data inline. Also adds supporting methods to World.cs: - SetDimensionIdMap(): Store VarInt ID -> dimension name mapping from RegistryData entries (needed by JoinGame/Respawn) - GetDimensionNameById(): Look up dimension name by numeric ID - HasAnyDimension(): Check if any dimensions were loaded from server-provided data Made-with: Cursor
2026-03-19 00:13:22 +08:00
public static void SetDimensionIdMap(Dictionary<int, string> idMap)
{
dimensionIdMap = idMap;
}
public static string GetDimensionNameById(int id)
{
return dimensionIdMap.TryGetValue(id, out var name) ? name : "minecraft:overworld";
}
public static bool HasAnyDimension()
{
return dimensionList.Count > 0;
}
Dynamically parse minecraft:attribute registry from server RegistryData In 1.20.6+, EntityProperties packets reference attributes by VarInt registry IDs instead of string names. Previously, a hardcoded dictionary of 22 attribute entries (matching the vanilla 1.20.6 registry) was used to map these IDs back to names. This works for vanilla servers but would fail silently for modded servers that add custom attributes — any unknown ID would be reported as "unknown". This commit replaces the hardcoded attribute dictionary with dynamic registry parsing, following the same pattern already used for dimension_type and chat_type registries: - World.cs: Add static `attributeIdMap` field, `SetAttributeIdMap()` and `GetAttributeNameById()` methods for storing/querying attribute names by their VarInt registry IDs. - Protocol18.cs (RegistryData handler): When the server sends a `minecraft:attribute` registry during the Configuration phase, parse all entries and store the ID→name mapping. The `minecraft:` prefix is stripped from entry names to match the format used in EntityProperties packets (e.g. "minecraft:generic.armor" → "generic.armor"). - Protocol18.cs (EntityProperties handler): Remove the hardcoded 22-entry `attributeDictionary` and use `World.GetAttributeNameById()` instead. Unknown IDs still fall back to "unknown" for safety. Also closes issue #4 (Disconnect packet extra boolean) — verified that both Play and Configuration phase Disconnect handlers already use `ReadNextChat()` (NBT format since 1.20.4+), matching the 1.20.6 protocol spec. No code changes needed; updated tracking document to mark as closed. Made-with: Cursor
2026-03-19 01:12:18 +08:00
public static void SetAttributeIdMap(Dictionary<int, string> idMap)
{
attributeIdMap = idMap;
}
/// <summary>
/// Get attribute name by its registry VarInt ID. Returns null if the ID is unknown.
Fix EntityProperties crash and add default attribute registry fallback After the previous commit (99ac3d0) moved attribute lookup from a hardcoded dictionary to the dynamic RegistryData, MCC would crash immediately upon joining a vanilla 1.20.6 server with: System.ArgumentException: An item with the same key has already been added. Key: unknown Root cause: When KnownDataPacks negotiation tells the server that MCC already has the "minecraft" data pack, the server skips sending RegistryData for registries it considers "known" — including minecraft:attribute. This left the dynamic attribute map empty, so every VarInt attribute ID resolved to "unknown". The EntityProperties packet often contains multiple attributes (e.g. armor, max_health, movement_speed), and `keys.Add("unknown", ...)` on the second "unknown" attribute threw ArgumentException. Two fixes applied: 1. World.GetAttributeNameById(): When the dynamic attribute map is empty (server didn't send the registry), automatically load the vanilla 1.20.6 default attribute order (22 entries matching Attributes.java registration order). This mirrors the pattern used for dimensions where defaults are loaded when RegistryData is not sent. If a modded server sends a custom attribute registry, the dynamic map takes precedence. 2. Protocol18.cs EntityProperties handler: Change `keys.Add(propertyKey, propertyValue2)` to `keys[propertyKey] = propertyValue2` to tolerate duplicate keys defensively, in case an unknown attribute ID still appears. Tested: MCC now connects to a vanilla 1.20.6 offline-mode server, stays online for 6+ minutes with no crashes or disconnections. Verified: chat messages received, inventory listing (item names/counts correct), entity detection, TPS query, and health query all work correctly. Made-with: Cursor
2026-03-19 01:26:20 +08:00
/// When KnownDataPacks negotiation tells the server we already have vanilla data,
/// the server skips sending the attribute registry. In that case we fall back to
/// the built-in vanilla 1.20.6 attribute order (22 entries).
Dynamically parse minecraft:attribute registry from server RegistryData In 1.20.6+, EntityProperties packets reference attributes by VarInt registry IDs instead of string names. Previously, a hardcoded dictionary of 22 attribute entries (matching the vanilla 1.20.6 registry) was used to map these IDs back to names. This works for vanilla servers but would fail silently for modded servers that add custom attributes — any unknown ID would be reported as "unknown". This commit replaces the hardcoded attribute dictionary with dynamic registry parsing, following the same pattern already used for dimension_type and chat_type registries: - World.cs: Add static `attributeIdMap` field, `SetAttributeIdMap()` and `GetAttributeNameById()` methods for storing/querying attribute names by their VarInt registry IDs. - Protocol18.cs (RegistryData handler): When the server sends a `minecraft:attribute` registry during the Configuration phase, parse all entries and store the ID→name mapping. The `minecraft:` prefix is stripped from entry names to match the format used in EntityProperties packets (e.g. "minecraft:generic.armor" → "generic.armor"). - Protocol18.cs (EntityProperties handler): Remove the hardcoded 22-entry `attributeDictionary` and use `World.GetAttributeNameById()` instead. Unknown IDs still fall back to "unknown" for safety. Also closes issue #4 (Disconnect packet extra boolean) — verified that both Play and Configuration phase Disconnect handlers already use `ReadNextChat()` (NBT format since 1.20.4+), matching the 1.20.6 protocol spec. No code changes needed; updated tracking document to mark as closed. Made-with: Cursor
2026-03-19 01:12:18 +08:00
/// </summary>
public static string? GetAttributeNameById(int id)
{
Fix EntityProperties crash and add default attribute registry fallback After the previous commit (99ac3d0) moved attribute lookup from a hardcoded dictionary to the dynamic RegistryData, MCC would crash immediately upon joining a vanilla 1.20.6 server with: System.ArgumentException: An item with the same key has already been added. Key: unknown Root cause: When KnownDataPacks negotiation tells the server that MCC already has the "minecraft" data pack, the server skips sending RegistryData for registries it considers "known" — including minecraft:attribute. This left the dynamic attribute map empty, so every VarInt attribute ID resolved to "unknown". The EntityProperties packet often contains multiple attributes (e.g. armor, max_health, movement_speed), and `keys.Add("unknown", ...)` on the second "unknown" attribute threw ArgumentException. Two fixes applied: 1. World.GetAttributeNameById(): When the dynamic attribute map is empty (server didn't send the registry), automatically load the vanilla 1.20.6 default attribute order (22 entries matching Attributes.java registration order). This mirrors the pattern used for dimensions where defaults are loaded when RegistryData is not sent. If a modded server sends a custom attribute registry, the dynamic map takes precedence. 2. Protocol18.cs EntityProperties handler: Change `keys.Add(propertyKey, propertyValue2)` to `keys[propertyKey] = propertyValue2` to tolerate duplicate keys defensively, in case an unknown attribute ID still appears. Tested: MCC now connects to a vanilla 1.20.6 offline-mode server, stays online for 6+ minutes with no crashes or disconnections. Verified: chat messages received, inventory listing (item names/counts correct), entity detection, TPS query, and health query all work correctly. Made-with: Cursor
2026-03-19 01:26:20 +08:00
if (attributeIdMap.Count == 0)
LoadDefaultAttributes();
Dynamically parse minecraft:attribute registry from server RegistryData In 1.20.6+, EntityProperties packets reference attributes by VarInt registry IDs instead of string names. Previously, a hardcoded dictionary of 22 attribute entries (matching the vanilla 1.20.6 registry) was used to map these IDs back to names. This works for vanilla servers but would fail silently for modded servers that add custom attributes — any unknown ID would be reported as "unknown". This commit replaces the hardcoded attribute dictionary with dynamic registry parsing, following the same pattern already used for dimension_type and chat_type registries: - World.cs: Add static `attributeIdMap` field, `SetAttributeIdMap()` and `GetAttributeNameById()` methods for storing/querying attribute names by their VarInt registry IDs. - Protocol18.cs (RegistryData handler): When the server sends a `minecraft:attribute` registry during the Configuration phase, parse all entries and store the ID→name mapping. The `minecraft:` prefix is stripped from entry names to match the format used in EntityProperties packets (e.g. "minecraft:generic.armor" → "generic.armor"). - Protocol18.cs (EntityProperties handler): Remove the hardcoded 22-entry `attributeDictionary` and use `World.GetAttributeNameById()` instead. Unknown IDs still fall back to "unknown" for safety. Also closes issue #4 (Disconnect packet extra boolean) — verified that both Play and Configuration phase Disconnect handlers already use `ReadNextChat()` (NBT format since 1.20.4+), matching the 1.20.6 protocol spec. No code changes needed; updated tracking document to mark as closed. Made-with: Cursor
2026-03-19 01:12:18 +08:00
return attributeIdMap.TryGetValue(id, out var name) ? name : null;
}
Fix EntityProperties crash and add default attribute registry fallback After the previous commit (99ac3d0) moved attribute lookup from a hardcoded dictionary to the dynamic RegistryData, MCC would crash immediately upon joining a vanilla 1.20.6 server with: System.ArgumentException: An item with the same key has already been added. Key: unknown Root cause: When KnownDataPacks negotiation tells the server that MCC already has the "minecraft" data pack, the server skips sending RegistryData for registries it considers "known" — including minecraft:attribute. This left the dynamic attribute map empty, so every VarInt attribute ID resolved to "unknown". The EntityProperties packet often contains multiple attributes (e.g. armor, max_health, movement_speed), and `keys.Add("unknown", ...)` on the second "unknown" attribute threw ArgumentException. Two fixes applied: 1. World.GetAttributeNameById(): When the dynamic attribute map is empty (server didn't send the registry), automatically load the vanilla 1.20.6 default attribute order (22 entries matching Attributes.java registration order). This mirrors the pattern used for dimensions where defaults are loaded when RegistryData is not sent. If a modded server sends a custom attribute registry, the dynamic map takes precedence. 2. Protocol18.cs EntityProperties handler: Change `keys.Add(propertyKey, propertyValue2)` to `keys[propertyKey] = propertyValue2` to tolerate duplicate keys defensively, in case an unknown attribute ID still appears. Tested: MCC now connects to a vanilla 1.20.6 offline-mode server, stays online for 6+ minutes with no crashes or disconnections. Verified: chat messages received, inventory listing (item names/counts correct), entity detection, TPS query, and health query all work correctly. Made-with: Cursor
2026-03-19 01:26:20 +08:00
private static void LoadDefaultAttributes()
{
// Fallback for when the server doesn't send attribute registry via RegistryData.
// Matches 1.21.1 Attributes.java registration order.
// For 1.20.6+ servers, SetAttributeIdMap() overrides this with the actual registry.
Fix EntityProperties crash and add default attribute registry fallback After the previous commit (99ac3d0) moved attribute lookup from a hardcoded dictionary to the dynamic RegistryData, MCC would crash immediately upon joining a vanilla 1.20.6 server with: System.ArgumentException: An item with the same key has already been added. Key: unknown Root cause: When KnownDataPacks negotiation tells the server that MCC already has the "minecraft" data pack, the server skips sending RegistryData for registries it considers "known" — including minecraft:attribute. This left the dynamic attribute map empty, so every VarInt attribute ID resolved to "unknown". The EntityProperties packet often contains multiple attributes (e.g. armor, max_health, movement_speed), and `keys.Add("unknown", ...)` on the second "unknown" attribute threw ArgumentException. Two fixes applied: 1. World.GetAttributeNameById(): When the dynamic attribute map is empty (server didn't send the registry), automatically load the vanilla 1.20.6 default attribute order (22 entries matching Attributes.java registration order). This mirrors the pattern used for dimensions where defaults are loaded when RegistryData is not sent. If a modded server sends a custom attribute registry, the dynamic map takes precedence. 2. Protocol18.cs EntityProperties handler: Change `keys.Add(propertyKey, propertyValue2)` to `keys[propertyKey] = propertyValue2` to tolerate duplicate keys defensively, in case an unknown attribute ID still appears. Tested: MCC now connects to a vanilla 1.20.6 offline-mode server, stays online for 6+ minutes with no crashes or disconnections. Verified: chat messages received, inventory listing (item names/counts correct), entity detection, TPS query, and health query all work correctly. Made-with: Cursor
2026-03-19 01:26:20 +08:00
attributeIdMap = new Dictionary<int, string>
{
{ 0, "generic.armor" },
{ 1, "generic.armor_toughness" },
{ 2, "generic.attack_damage" },
{ 3, "generic.attack_knockback" },
{ 4, "generic.attack_speed" },
{ 5, "player.block_break_speed" },
{ 6, "player.block_interaction_range" },
{ 7, "generic.burning_time" },
{ 8, "generic.explosion_knockback_resistance" },
{ 9, "player.entity_interaction_range" },
{ 10, "generic.fall_damage_multiplier" },
{ 11, "generic.flying_speed" },
{ 12, "generic.follow_range" },
{ 13, "generic.gravity" },
{ 14, "generic.jump_strength" },
{ 15, "generic.knockback_resistance" },
{ 16, "generic.luck" },
{ 17, "generic.max_absorption" },
{ 18, "generic.max_health" },
{ 19, "player.mining_efficiency" },
{ 20, "generic.movement_efficiency" },
{ 21, "generic.movement_speed" },
{ 22, "generic.oxygen_bonus" },
{ 23, "generic.safe_fall_distance" },
{ 24, "generic.scale" },
{ 25, "player.sneaking_speed" },
{ 26, "zombie.spawn_reinforcements" },
{ 27, "generic.step_height" },
{ 28, "player.submerged_mining_speed" },
{ 29, "player.sweeping_damage_ratio" },
{ 30, "generic.water_movement_efficiency" }
Fix EntityProperties crash and add default attribute registry fallback After the previous commit (99ac3d0) moved attribute lookup from a hardcoded dictionary to the dynamic RegistryData, MCC would crash immediately upon joining a vanilla 1.20.6 server with: System.ArgumentException: An item with the same key has already been added. Key: unknown Root cause: When KnownDataPacks negotiation tells the server that MCC already has the "minecraft" data pack, the server skips sending RegistryData for registries it considers "known" — including minecraft:attribute. This left the dynamic attribute map empty, so every VarInt attribute ID resolved to "unknown". The EntityProperties packet often contains multiple attributes (e.g. armor, max_health, movement_speed), and `keys.Add("unknown", ...)` on the second "unknown" attribute threw ArgumentException. Two fixes applied: 1. World.GetAttributeNameById(): When the dynamic attribute map is empty (server didn't send the registry), automatically load the vanilla 1.20.6 default attribute order (22 entries matching Attributes.java registration order). This mirrors the pattern used for dimensions where defaults are loaded when RegistryData is not sent. If a modded server sends a custom attribute registry, the dynamic map takes precedence. 2. Protocol18.cs EntityProperties handler: Change `keys.Add(propertyKey, propertyValue2)` to `keys[propertyKey] = propertyValue2` to tolerate duplicate keys defensively, in case an unknown attribute ID still appears. Tested: MCC now connects to a vanilla 1.20.6 offline-mode server, stays online for 6+ minutes with no crashes or disconnections. Verified: chat messages received, inventory listing (item names/counts correct), entity detection, TPS query, and health query all work correctly. Made-with: Cursor
2026-03-19 01:26:20 +08:00
};
}
2022-09-07 00:02:09 +08:00
/// <summary>
2022-09-07 00:08:31 +08:00
/// Store one dimension - Directly used in 1.16.2 to 1.18.2
2022-09-07 00:02:09 +08:00
/// </summary>
/// <param name="dimensionName">Dimension name</param>
/// <param name="dimensionType">Dimension Type nbt data</param>
2022-09-07 00:08:31 +08:00
public static void StoreOneDimension(string dimensionName, Dictionary<string, object> dimensionType)
2022-09-07 00:02:09 +08:00
{
if (dimensionList.ContainsKey(dimensionName))
dimensionList.Remove(dimensionName);
2022-09-07 00:02:09 +08:00
dimensionList.Add(dimensionName, new Dimension(dimensionName, dimensionType));
}
2022-08-28 22:27:21 +08:00
/// <summary>
2022-08-28 22:27:21 +08:00
/// Set current dimension - 1.16 and above
/// </summary>
/// <param name="name"> The name of the dimension type</param>
/// <param name="nbt">The dimension type (NBT Tag Compound)</param>
public static void SetDimension(string name)
{
// Try to get the dimension using the name as is
if (dimensionList.TryGetValue(name, out Dimension? dimension))
{
curDimension = dimension;
return; // Dimension found
}
// If not found, check if name lacks 'minecraft:' prefix and try again
if (!name.StartsWith("minecraft:"))
{
string prefixedName = "minecraft:" + name;
if (dimensionList.TryGetValue(prefixedName, out dimension))
{
curDimension = dimension;
return; // Dimension found with prefixed name
}
}
else
{
string unprefixedName = name["minecraft:".Length..];
if (dimensionList.TryGetValue(unprefixedName, out dimension))
{
curDimension = dimension;
return;
}
}
if (TryStoreDefaultVanillaDimension(name)
&& dimensionList.TryGetValue(name, out dimension))
{
curDimension = dimension;
return;
}
// If still not found, dimension does not exist
throw new KeyNotFoundException($"Dimension '{name}' not found in dimensions dictionary.");
}
private static bool TryStoreDefaultVanillaDimension(string name)
{
var normalizedName = name.StartsWith("minecraft:")
? name
: "minecraft:" + name;
if (normalizedName is not ("minecraft:overworld" or "minecraft:the_nether" or "minecraft:the_end"))
return false;
StoreOneDimension(name, new Dictionary<string, object>());
return true;
}
/// <summary>
/// Get current dimension
/// </summary>
/// <returns>Current dimension</returns>
public static Dimension GetDimension()
{
2022-08-28 22:27:21 +08:00
return curDimension;
}
/// <summary>
/// Set chunk column at the specified location
/// </summary>
/// <param name="chunkX">ChunkColumn X</param>
/// <param name="chunkY">ChunkColumn Y</param>
/// <param name="chunkZ">ChunkColumn Z</param>
/// <param name="chunkColumnSize">ChunkColumn size</param>
/// <param name="chunk">Chunk data</param>
/// <param name="loadCompleted">Whether the ChunkColumn has been fully loaded</param>
2022-08-25 10:40:55 +08:00
public void StoreChunk(int chunkX, int chunkY, int chunkZ, int chunkColumnSize, Chunk? chunk, bool loadCompleted)
{
2026-03-25 23:57:02 +01:00
ChunkColumn chunkColumn = chunks.GetOrAdd((chunkX, chunkZ), (_) => new(chunkColumnSize));
2022-09-02 21:02:25 +08:00
chunkColumn[chunkY] = chunk;
if (loadCompleted)
chunkColumn.FullyLoaded = true;
}
/// <summary>
/// Get chunk column at the specified location
/// </summary>
/// <param name="location">Location to retrieve chunk column</param>
/// <returns>The chunk column</returns>
public ChunkColumn? GetChunkColumn(Location location)
{
return this[location.ChunkX, location.ChunkZ];
}
/// <summary>
/// Get block at the specified location
/// </summary>
/// <param name="location">Location to retrieve block from</param>
/// <returns>Block at specified location or Air if the location is not loaded</returns>
public Block GetBlock(Location location)
{
ChunkColumn? column = GetChunkColumn(location);
if (column is not null)
{
Chunk? chunk = column.GetChunk(location);
if (chunk is not null)
return chunk.GetBlock(location);
}
2022-10-02 13:49:36 +08:00
return Block.Air;
}
/// <summary>
/// Look for a block around the specified location
/// </summary>
/// <param name="from">Start location</param>
/// <param name="block">Block type</param>
/// <param name="radius">Search radius - larger is slower: O^3 complexity</param>
/// <returns>Block matching the specified block type</returns>
public List<Location> FindBlock(Location from, Material block, double radius)
{
return FindBlock(from, block, radius, radius, radius);
}
/// <summary>
/// Look for a block around the specified location
/// </summary>
/// <param name="from">Start location</param>
/// <param name="block">Block type</param>
/// <param name="radiusx">Search radius on the X axis</param>
/// <param name="radiusy">Search radius on the Y axis</param>
/// <param name="radiusz">Search radius on the Z axis</param>
/// <returns>Block matching the specified block type</returns>
public List<Location> FindBlock(Location from, Material block, double radiusx, double radiusy, double radiusz)
{
Location minPoint = new Location(from.X - radiusx, from.Y - radiusy, from.Z - radiusz);
Location maxPoint = new Location(from.X + radiusx, from.Y + radiusy, from.Z + radiusz);
List<int> xRange = Enumerable.Range(Convert.ToInt32(Math.Floor(minPoint.X)), Convert.ToInt32(Math.Floor(maxPoint.X - minPoint.X)) + 1).ToList();
List<int> yRange = Enumerable.Range(Convert.ToInt32(Math.Floor(minPoint.Y)), Convert.ToInt32(Math.Floor(maxPoint.Y - minPoint.Y)) + 1).ToList();
List<int> zRange = Enumerable.Range(Convert.ToInt32(Math.Floor(minPoint.Z)), Convert.ToInt32(Math.Floor(maxPoint.Z - minPoint.Z)) + 1).ToList();
List<Location> listOfBlocks = xRange.SelectMany(x => yRange.SelectMany(y => zRange.Select(z => new Location(x, y, z)))).ToList();
return listOfBlocks.Where(loc => GetBlock(loc).Type == block).ToList();
}
/// <summary>
/// Set block at the specified location
/// </summary>
/// <param name="location">Location to set block to</param>
/// <param name="block">Block to set</param>
public void SetBlock(Location location, Block block)
{
ChunkColumn? column = this[location.ChunkX, location.ChunkZ];
if (column is not null && location.ChunkY >= 0 && location.ChunkY < column.ColumnSize)
{
Chunk? chunk = column.GetChunk(location);
if (chunk is null)
column[location.ChunkY] = chunk = new Chunk();
chunk[location.ChunkBlockX, location.ChunkBlockY, location.ChunkBlockZ] = block;
}
}
/// <summary>
/// Clear all terrain data from the world
/// </summary>
public void Clear()
{
2022-08-31 18:00:00 +08:00
chunks = new();
chunkCnt = 0;
chunkLoadNotCompleted = 0;
}
public static string GetChunkLoadingStatus(World world)
{
double chunkLoadedRatio;
if (world.chunkCnt == 0)
chunkLoadedRatio = 0;
else
chunkLoadedRatio = (world.chunkCnt - world.chunkLoadNotCompleted) / (double)world.chunkCnt;
string status = string.Format(Translations.cmd_move_chunk_loading_status,
chunkLoadedRatio, world.chunkCnt - world.chunkLoadNotCompleted, world.chunkCnt);
return status;
}
}
}