From 99ac3d028ab9711ba43c32a83b1a9fe494eafc27 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Thu, 19 Mar 2026 01:12:18 +0800 Subject: [PATCH] Dynamically parse minecraft:attribute registry from server RegistryData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- MinecraftClient/Mapping/World.cs | 18 +++++++ .../Protocol/Handlers/Protocol18.cs | 52 ++++++++----------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index 8add5c23..be666f4f 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -28,6 +28,11 @@ namespace MinecraftClient.Mapping /// private static Dictionary dimensionIdMap = new(); + /// + /// VarInt ID → attribute name mapping, populated from RegistryData (minecraft:attribute) in 1.20.6+ + /// + private static Dictionary attributeIdMap = new(); + /// /// Chunk data parsing progress /// @@ -232,6 +237,19 @@ namespace MinecraftClient.Mapping return dimensionList.Count > 0; } + public static void SetAttributeIdMap(Dictionary idMap) + { + attributeIdMap = idMap; + } + + /// + /// Get attribute name by its registry VarInt ID. Returns null if the ID is unknown. + /// + public static string? GetAttributeNameById(int id) + { + return attributeIdMap.TryGetValue(id, out var name) ? name : null; + } + /// /// Store one dimension - Directly used in 1.16.2 to 1.18.2 /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index dab9f2e0..7806af21 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -463,9 +463,11 @@ namespace MinecraftClient.Protocol.Handlers var isChat = registryId == "minecraft:chat_type"; var isDimension = registryId == "minecraft:dimension_type"; + var isAttribute = registryId == "minecraft:attribute"; var availableChats = isChat ? new Dictionary() : null; var dimensionIdMap = isDimension ? new Dictionary() : null; + var attributeIdMap = isAttribute ? new Dictionary() : null; for (var i = 0; i < entryCount; i++) { @@ -484,6 +486,14 @@ namespace MinecraftClient.Protocol.Handlers if (nbtData != null && handler.GetTerrainEnabled()) 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) @@ -494,6 +504,8 @@ namespace MinecraftClient.Protocol.Handlers if (!handler.GetTerrainEnabled() || !World.HasAnyDimension()) World.LoadDefaultDimensions1206Plus(); } + else if (isAttribute) + World.SetAttributeIdMap(attributeIdMap!); } break; @@ -2502,39 +2514,19 @@ namespace MinecraftClient.Protocol.Handlers ? dataTypes.ReadNextVarInt(packetData) : dataTypes.ReadNextInt(packetData); - var attributeDictionary = new Dictionary - { - { 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 keys = new(); for (var i = 0; i < numberOfProperties; i++) { - var propertyKey = protocolVersion < MC_1_20_6_Version - ? dataTypes.ReadNextString(packetData) - : (attributeDictionary.TryGetValue(dataTypes.ReadNextVarInt(packetData), out var attrName) - ? attrName : "unknown"); + string propertyKey; + if (protocolVersion < MC_1_20_6_Version) + { + propertyKey = dataTypes.ReadNextString(packetData); + } + else + { + var attrId = dataTypes.ReadNextVarInt(packetData); + propertyKey = World.GetAttributeNameById(attrId) ?? "unknown"; + } var propertyValue2 = dataTypes.ReadNextDouble(packetData); List op0 = new();