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
This commit is contained in:
BruceChen 2026-03-19 01:12:18 +08:00
parent 967f67190c
commit 99ac3d028a
2 changed files with 40 additions and 30 deletions

View file

@ -28,6 +28,11 @@ namespace MinecraftClient.Mapping
/// </summary> /// </summary>
private static Dictionary<int, string> dimensionIdMap = new(); private static Dictionary<int, string> dimensionIdMap = new();
/// <summary>
/// VarInt ID → attribute name mapping, populated from RegistryData (minecraft:attribute) in 1.20.6+
/// </summary>
private static Dictionary<int, string> attributeIdMap = new();
/// <summary> /// <summary>
/// Chunk data parsing progress /// Chunk data parsing progress
/// </summary> /// </summary>
@ -232,6 +237,19 @@ namespace MinecraftClient.Mapping
return dimensionList.Count > 0; return dimensionList.Count > 0;
} }
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.
/// </summary>
public static string? GetAttributeNameById(int id)
{
return attributeIdMap.TryGetValue(id, out var name) ? name : null;
}
/// <summary> /// <summary>
/// Store one dimension - Directly used in 1.16.2 to 1.18.2 /// Store one dimension - Directly used in 1.16.2 to 1.18.2
/// </summary> /// </summary>

View file

@ -463,9 +463,11 @@ namespace MinecraftClient.Protocol.Handlers
var isChat = registryId == "minecraft:chat_type"; var isChat = registryId == "minecraft:chat_type";
var isDimension = registryId == "minecraft:dimension_type"; var isDimension = registryId == "minecraft:dimension_type";
var isAttribute = registryId == "minecraft:attribute";
var availableChats = isChat ? new Dictionary<int, string>() : null; var availableChats = isChat ? new Dictionary<int, string>() : null;
var dimensionIdMap = isDimension ? new Dictionary<int, string>() : null; var dimensionIdMap = isDimension ? new Dictionary<int, string>() : null;
var attributeIdMap = isAttribute ? new Dictionary<int, string>() : null;
for (var i = 0; i < entryCount; i++) for (var i = 0; i < entryCount; i++)
{ {
@ -484,6 +486,14 @@ namespace MinecraftClient.Protocol.Handlers
if (nbtData != null && handler.GetTerrainEnabled()) if (nbtData != null && handler.GetTerrainEnabled())
World.StoreOneDimension(entryId, nbtData); World.StoreOneDimension(entryId, nbtData);
} }
else if (isAttribute)
{
// Strip "minecraft:" prefix to match the format used in EntityProperties packets
var attrName = entryId.StartsWith("minecraft:")
? entryId.Substring("minecraft:".Length)
: entryId;
attributeIdMap!.Add(i, attrName);
}
} }
if (isChat) if (isChat)
@ -494,6 +504,8 @@ namespace MinecraftClient.Protocol.Handlers
if (!handler.GetTerrainEnabled() || !World.HasAnyDimension()) if (!handler.GetTerrainEnabled() || !World.HasAnyDimension())
World.LoadDefaultDimensions1206Plus(); World.LoadDefaultDimensions1206Plus();
} }
else if (isAttribute)
World.SetAttributeIdMap(attributeIdMap!);
} }
break; break;
@ -2502,39 +2514,19 @@ namespace MinecraftClient.Protocol.Handlers
? dataTypes.ReadNextVarInt(packetData) ? dataTypes.ReadNextVarInt(packetData)
: dataTypes.ReadNextInt(packetData); : dataTypes.ReadNextInt(packetData);
var attributeDictionary = 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, "player.entity_interaction_range" },
{ 8, "generic.fall_damage_multiplier" },
{ 9, "generic.flying_speed" },
{ 10, "generic.follow_range" },
{ 11, "generic.gravity" },
{ 12, "generic.jump_strength" },
{ 13, "generic.knockback_resistance" },
{ 14, "generic.luck" },
{ 15, "generic.max_absorption" },
{ 16, "generic.max_health" },
{ 17, "generic.movement_speed" },
{ 18, "generic.safe_fall_distance" },
{ 19, "generic.scale" },
{ 20, "zombie.spawn_reinforcements" },
{ 21, "generic.step_height" }
};
Dictionary<string, double> keys = new(); Dictionary<string, double> keys = new();
for (var i = 0; i < numberOfProperties; i++) for (var i = 0; i < numberOfProperties; i++)
{ {
var propertyKey = protocolVersion < MC_1_20_6_Version string propertyKey;
? dataTypes.ReadNextString(packetData) if (protocolVersion < MC_1_20_6_Version)
: (attributeDictionary.TryGetValue(dataTypes.ReadNextVarInt(packetData), out var attrName) {
? attrName : "unknown"); propertyKey = dataTypes.ReadNextString(packetData);
}
else
{
var attrId = dataTypes.ReadNextVarInt(packetData);
propertyKey = World.GetAttributeNameById(attrId) ?? "unknown";
}
var propertyValue2 = dataTypes.ReadNextDouble(packetData); var propertyValue2 = dataTypes.ReadNextDouble(packetData);
List<double> op0 = new(); List<double> op0 = new();