Wire up 1.20.6 structured components to Item and fix GetItemSlot serialization

In 1.20.6+, items use structured components instead of NBT for metadata.
Previously, ReadNextItemSlot parsed the components but never stored them
on the Item instance, leaving DisplayName/Lores/Damage/Enchantments all
empty. GetItemSlot also still used the pre-1.20.6 format (bool + VarInt +
byte + NBT), causing the server to reject any item operation packets.

Changes:

Item.cs:
- Add List<StructuredComponent>? Components field to hold the raw
  component list for round-trip serialization
- DisplayName property: read from CustomNameComponent (with
  ItemNameComponent as fallback) when Components is present
- Lores property: read from LoreNameComponent1206 when Components is
  present
- Damage property: read from DamageComponent when Components is present
- Add EnchantmentList property: read from EnchantmentsComponent (covers
  both normal and StoredEnchantmentsComponent for enchanted books)
- ToFullString(): use EnchantmentList with EnchantmentMapping for display
  when available, fall back to NBT path for older versions
- Add CloneWithCount() method that preserves both NBT and Components

DataTypes.cs - ReadNextItemSlot:
- Assign parsed strcturedComponentsToAdd to item.Components

DataTypes.cs - GetItemSlot:
- Add 1.20.6+ branch: write VarInt(count) + VarInt(itemId) + component
  counts + serialized components (using each component's TypeId and
  Serialize() method)
- Empty slot sends VarInt(0) per the 1.20.6 protocol spec

StructuredComponent.cs:
- Add int TypeId property (default -1) to store the registry type ID
  assigned during parsing, enabling round-trip serialization

StructuredComponentRegistry.cs:
- Set component.TypeId = id after instantiation in ParseComponent()

McClient.cs:
- Replace manual Item constructor calls (new Item(type, count, nbt))
  with Item.CloneWithCount() to preserve Components during inventory
  operations like slot moves, stack splits, and right-click placement

Made-with: Cursor
This commit is contained in:
BruceChen 2026-03-19 00:26:24 +08:00
parent bb18399523
commit 8eac21b4a4
5 changed files with 135 additions and 32 deletions

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
@ -450,15 +450,11 @@ namespace MinecraftClient.Protocol.Handlers
}
for (var i = 0; i < numberofComponentsToRemove; i++)
{
// TODO: Check what this does exactly
ReadNextVarInt(cache); // The type of component to remove
}
// TODO: Wire up the strctured components in the Item class (extract info, update fields, etc..)
// Use strcturedComponentsToAdd
// Look at: https://wiki.vg/index.php?title=Slot_Data&oldid=19350#Structured_components
ReadNextVarInt(cache);
if (strcturedComponentsToAdd.Count > 0)
item.Components = strcturedComponentsToAdd;
return item;
case >= Protocol18Handler.MC_1_13_Version:
{
@ -1561,17 +1557,44 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>Item slot representation</returns>
public byte[] GetItemSlot(Item? item, ItemPalette itemPalette)
{
// TODO: Wire up Structured components for 1.20.6
List<byte> slotData = new();
if (protocolversion > Protocol18Handler.MC_1_13_Version)
if (protocolversion >= Protocol18Handler.MC_1_20_6_Version)
{
// MC 1.13 and greater
if (item == null || item.IsEmpty)
slotData.AddRange(GetBool(false)); // No item
{
slotData.AddRange(GetVarInt(0));
}
else
{
slotData.AddRange(GetBool(true)); // Item is present
slotData.AddRange(GetVarInt(item.Count));
slotData.AddRange(GetVarInt(itemPalette.ToId(item.Type)));
if (item.Components != null && item.Components.Count > 0)
{
slotData.AddRange(GetVarInt(item.Components.Count));
slotData.AddRange(GetVarInt(0)); // components to remove
foreach (var component in item.Components)
{
slotData.AddRange(GetVarInt(component.TypeId));
var serialized = component.Serialize();
slotData.AddRange(serialized);
}
}
else
{
slotData.AddRange(GetVarInt(0)); // no components to add
slotData.AddRange(GetVarInt(0)); // no components to remove
}
}
}
else if (protocolversion > Protocol18Handler.MC_1_13_Version)
{
if (item == null || item.IsEmpty)
slotData.AddRange(GetBool(false));
else
{
slotData.AddRange(GetBool(true));
slotData.AddRange(GetVarInt(itemPalette.ToId(item.Type)));
slotData.Add((byte)item.Count);
slotData.AddRange(GetNbt(item.NBT));
@ -1579,13 +1602,10 @@ namespace MinecraftClient.Protocol.Handlers
}
else
{
// MC 1.12.2 and lower
if (item == null || item.IsEmpty)
slotData.AddRange(GetShort(-1));
else
{
// For 1.8 - 1.12.2 we combine Item Id and Item Data to a single value using: (id << 16) | data
// Thus to get an ID we do a right shift by 16 bits
slotData.AddRange(GetShort((short)(itemPalette.ToId(item.Type) >> 16)));
slotData.Add((byte)item.Count);
slotData.Add((byte)item.Data);

View file

@ -8,7 +8,12 @@ public abstract class StructuredComponent(DataTypes dataTypes, ItemPalette itemP
protected DataTypes DataTypes { get; private set; } = dataTypes;
protected SubComponentRegistry SubComponentRegistry { get; private set; } = subComponentRegistry;
protected ItemPalette ItemPalette { get; private set; } = itemPalette;
/// <summary>
/// The registry type ID assigned during parsing, used for round-trip serialization.
/// </summary>
public int TypeId { get; set; } = -1;
public abstract void Parse(Queue<byte> data);
public abstract Queue<byte> Serialize();
}

View file

@ -35,7 +35,8 @@ public abstract class StructuredComponentRegistry(DataTypes dataTypes, ItemPalet
var component =
Activator.CreateInstance(type, dataTypes, itemPalette, subComponentRegistry) as StructuredComponent
?? throw new InvalidOperationException($"Could not instantiate a parser for a structured component type {name}");
component.TypeId = id;
component.Parse(data);
return component;
}