2026-03-19 00:13:22 +08:00
|
|
|
using System;
|
2022-08-31 18:00:00 +08:00
|
|
|
using System.Collections.Concurrent;
|
2015-11-30 15:30:49 +01:00
|
|
|
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
|
2015-11-30 15:30:49 +01:00
|
|
|
/// </summary>
|
2026-03-25 23:57:02 +01:00
|
|
|
private ConcurrentDictionary<(int ChunkX, int ChunkZ), ChunkColumn> chunks = new();
|
2015-11-30 15:30:49 +01:00
|
|
|
|
2022-07-24 21:41:56 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// The dimension info of the world
|
|
|
|
|
/// </summary>
|
2024-12-05 02:12:39 +00:00
|
|
|
private static Dimension curDimension= new();
|
2022-08-28 22:27:21 +08:00
|
|
|
|
2022-10-02 18:31:08 +08:00
|
|
|
private static readonly Dictionary<string, Dimension> dimensionList = new();
|
2022-07-24 21:41:56 +08:00
|
|
|
|
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();
|
|
|
|
|
|
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>
|
2022-08-18 20:58:49 +02:00
|
|
|
public int chunkCnt = 0;
|
|
|
|
|
public int chunkLoadNotCompleted = 0;
|
2022-07-25 03:19:24 +08:00
|
|
|
|
2015-11-30 15:30:49 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Read, set or unload the specified chunk column
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="chunkX">ChunkColumn X</param>
|
2022-08-24 18:16:16 +08:00
|
|
|
/// <param name="chunkZ">ChunkColumn Z</param>
|
2015-11-30 15:30:49 +01:00
|
|
|
/// <returns>chunk at the given location</returns>
|
2022-08-24 18:16:16 +08:00
|
|
|
public ChunkColumn? this[int chunkX, int chunkZ]
|
2015-11-30 15:30:49 +01:00
|
|
|
{
|
|
|
|
|
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;
|
2015-11-30 15:30:49 +01:00
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
2026-03-25 23:57:02 +01:00
|
|
|
var chunkCoord = (chunkX, chunkZ);
|
2026-03-24 00:27:07 +00:00
|
|
|
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);
|
2015-11-30 15:30:49 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-28 22:27:21 +08:00
|
|
|
|
2022-07-24 21:41:56 +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)
|
|
|
|
|
{
|
|
|
|
|
var dimensionListNbt = (object[])(((Dictionary<string, object>)registryCodec["minecraft:dimension_type"])["value"]);
|
2022-10-02 18:31:08 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-30 11:26:41 +02: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);
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
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).
|
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();
|
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()
|
|
|
|
|
{
|
fix: Explosion packet parsing and update attribute fallback for 1.21
Fix the Explosion packet handler that was truncating reads at the
knockback fields, leaving BlockInteraction, particles, and SoundEvent
bytes unconsumed for 1.20.4+. The old commented-out code had three bugs:
conditional particle read (should always read both small and large),
reading SoundEvent as a plain string (it's a Holder<SoundEvent> encoded
as VarInt id + optional inline DIRECT_STREAM_CODEC), and an incorrect
fixedRange version gate. Verified against decompiled ClientboundExplodePacket
from both 1.20.6 and 1.21.1 — the wire format is identical across versions.
Update LoadDefaultAttributes() fallback to match the 1.21.1 registry
order (31 attributes), adding 9 new entries: burning_time,
explosion_knockback_resistance, mining_efficiency, movement_efficiency,
oxygen_bonus, sneaking_speed, submerged_mining_speed,
sweeping_damage_ratio, and water_movement_efficiency. This fallback is
only used when the server omits the attribute RegistryData packet.
Made-with: Cursor
2026-03-20 01:29:18 +08:00
|
|
|
// 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" },
|
fix: Explosion packet parsing and update attribute fallback for 1.21
Fix the Explosion packet handler that was truncating reads at the
knockback fields, leaving BlockInteraction, particles, and SoundEvent
bytes unconsumed for 1.20.4+. The old commented-out code had three bugs:
conditional particle read (should always read both small and large),
reading SoundEvent as a plain string (it's a Holder<SoundEvent> encoded
as VarInt id + optional inline DIRECT_STREAM_CODEC), and an incorrect
fixedRange version gate. Verified against decompiled ClientboundExplodePacket
from both 1.20.6 and 1.21.1 — the wire format is identical across versions.
Update LoadDefaultAttributes() fallback to match the 1.21.1 registry
order (31 attributes), adding 9 new entries: burning_time,
explosion_knockback_resistance, mining_efficiency, movement_efficiency,
oxygen_bonus, sneaking_speed, submerged_mining_speed,
sweeping_damage_ratio, and water_movement_efficiency. This fallback is
only used when the server omits the attribute RegistryData packet.
Made-with: Cursor
2026-03-20 01:29:18 +08:00
|
|
|
{ 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))
|
2022-10-02 18:31:08 +08:00
|
|
|
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
|
|
|
|
2022-07-24 21:41:56 +08:00
|
|
|
/// <summary>
|
2022-08-28 22:27:21 +08:00
|
|
|
/// Set current dimension - 1.16 and above
|
2022-07-24 21:41:56 +08:00
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="name"> The name of the dimension type</param>
|
|
|
|
|
/// <param name="nbt">The dimension type (NBT Tag Compound)</param>
|
2024-12-05 02:12:39 +00:00
|
|
|
public static void SetDimension(string name)
|
|
|
|
|
{
|
|
|
|
|
// Try to get the dimension using the name as is
|
2026-03-24 01:25:50 +00:00
|
|
|
if (dimensionList.TryGetValue(name, out Dimension? dimension))
|
2024-12-05 02:12:39 +00:00
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If still not found, dimension does not exist
|
|
|
|
|
throw new KeyNotFoundException($"Dimension '{name}' not found in dimensions dictionary.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-07-24 21:41:56 +08:00
|
|
|
|
|
|
|
|
/// <summary>
|
2022-08-18 20:58:49 +02:00
|
|
|
/// Get current dimension
|
2022-07-24 21:41:56 +08:00
|
|
|
/// </summary>
|
2022-08-18 20:58:49 +02:00
|
|
|
/// <returns>Current dimension</returns>
|
|
|
|
|
public static Dimension GetDimension()
|
2022-07-24 21:41:56 +08:00
|
|
|
{
|
2022-08-28 22:27:21 +08:00
|
|
|
return curDimension;
|
2022-07-24 21:41:56 +08:00
|
|
|
}
|
|
|
|
|
|
2022-08-25 01:34:07 +08:00
|
|
|
/// <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)
|
2022-08-25 01:34:07 +08:00
|
|
|
{
|
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;
|
2022-08-25 01:34:07 +08:00
|
|
|
}
|
|
|
|
|
|
2015-11-30 15:30:49 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Get chunk column at the specified location
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="location">Location to retrieve chunk column</param>
|
|
|
|
|
/// <returns>The chunk column</returns>
|
2022-08-24 18:16:16 +08:00
|
|
|
public ChunkColumn? GetChunkColumn(Location location)
|
2015-11-30 15:30:49 +01:00
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
|
{
|
2022-08-24 18:16:16 +08:00
|
|
|
ChunkColumn? column = GetChunkColumn(location);
|
2026-03-24 00:27:07 +00:00
|
|
|
if (column is not null)
|
2015-11-30 15:30:49 +01:00
|
|
|
{
|
2022-08-24 18:16:16 +08:00
|
|
|
Chunk? chunk = column.GetChunk(location);
|
2026-03-24 00:27:07 +00:00
|
|
|
if (chunk is not null)
|
2015-11-30 15:30:49 +01:00
|
|
|
return chunk.GetBlock(location);
|
|
|
|
|
}
|
2022-10-02 13:49:36 +08:00
|
|
|
return Block.Air;
|
2015-11-30 15:30:49 +01:00
|
|
|
}
|
|
|
|
|
|
2020-07-30 00:49:16 +05:00
|
|
|
/// <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>
|
2022-12-06 15:50:17 +08:00
|
|
|
public List<Location> FindBlock(Location from, Material block, double radius)
|
2020-07-30 00:49:16 +05:00
|
|
|
{
|
|
|
|
|
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>
|
2022-12-06 15:50:17 +08:00
|
|
|
public List<Location> FindBlock(Location from, Material block, double radiusx, double radiusy, double radiusz)
|
2020-07-30 00:49:16 +05:00
|
|
|
{
|
2022-10-18 22:39:48 +02:00
|
|
|
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();
|
2020-07-30 00:49:16 +05:00
|
|
|
}
|
|
|
|
|
|
2015-11-30 15:30:49 +01:00
|
|
|
/// <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)
|
|
|
|
|
{
|
2022-08-24 18:16:16 +08:00
|
|
|
ChunkColumn? column = this[location.ChunkX, location.ChunkZ];
|
2026-03-24 00:27:07 +00:00
|
|
|
if (column is not null && column.ColumnSize >= location.ChunkY)
|
2015-11-30 15:30:49 +01:00
|
|
|
{
|
2022-08-24 18:16:16 +08:00
|
|
|
Chunk? chunk = column.GetChunk(location);
|
2026-03-24 00:27:07 +00:00
|
|
|
if (chunk is null)
|
2015-11-30 15:30:49 +01:00
|
|
|
column[location.ChunkY] = chunk = new Chunk();
|
|
|
|
|
chunk[location.ChunkBlockX, location.ChunkBlockY, location.ChunkBlockZ] = block;
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-04-28 21:32:03 +02:00
|
|
|
|
|
|
|
|
/// <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;
|
2019-04-28 21:32:03 +02:00
|
|
|
}
|
2022-10-02 18:31:08 +08:00
|
|
|
|
|
|
|
|
public static string GetChunkLoadingStatus(World world)
|
|
|
|
|
{
|
|
|
|
|
double chunkLoadedRatio;
|
|
|
|
|
if (world.chunkCnt == 0)
|
|
|
|
|
chunkLoadedRatio = 0;
|
|
|
|
|
else
|
|
|
|
|
chunkLoadedRatio = (world.chunkCnt - world.chunkLoadNotCompleted) / (double)world.chunkCnt;
|
|
|
|
|
|
2022-10-28 11:13:20 +08:00
|
|
|
string status = string.Format(Translations.cmd_move_chunk_loading_status,
|
2022-10-02 18:31:08 +08:00
|
|
|
chunkLoadedRatio, world.chunkCnt - world.chunkLoadNotCompleted, world.chunkCnt);
|
|
|
|
|
|
|
|
|
|
return status;
|
|
|
|
|
}
|
2015-11-30 15:30:49 +01:00
|
|
|
}
|
|
|
|
|
}
|