diff --git a/MinecraftClient/Inventory/Item.cs b/MinecraftClient/Inventory/Item.cs index b9d86755..247d45ea 100644 --- a/MinecraftClient/Inventory/Item.cs +++ b/MinecraftClient/Inventory/Item.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Core; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; using MinecraftClient.Protocol.Message; namespace MinecraftClient.Inventory @@ -32,6 +34,11 @@ namespace MinecraftClient.Inventory /// public Dictionary? NBT; + /// + /// 1.20.6+ structured components (raw list for round-trip serialization) + /// + public List? Components; + /// /// Create an item with ItemType, Count and Metadata /// @@ -50,6 +57,14 @@ namespace MinecraftClient.Inventory Data = data; } + /// + /// Create a shallow clone with a specific count (preserves NBT and Components). + /// + public Item CloneWithCount(int count) + { + return new Item(Type, count, Data, NBT) { Components = Components }; + } + /// /// Check if the item slot is empty /// @@ -60,12 +75,26 @@ namespace MinecraftClient.Inventory } /// - /// Retrieve item display name from NBT properties. NULL if no display name is defined. + /// Retrieve item display name. For 1.20.6+ reads from structured components + /// (CustomNameComponent, then ItemNameComponent as fallback); for older versions reads from NBT. /// public string? DisplayName { get { + if (Components != null) + { + var customName = Components.OfType().FirstOrDefault(); + if (customName != null && !string.IsNullOrEmpty(customName.CustomName)) + return customName.CustomName; + + var itemName = Components.OfType().FirstOrDefault(); + if (itemName != null && !string.IsNullOrEmpty(itemName.ItemName)) + return itemName.ItemName; + + return null; + } + if (NBT != null && NBT.ContainsKey("display")) { if (NBT["display"] is Dictionary displayProperties && @@ -82,12 +111,21 @@ namespace MinecraftClient.Inventory } /// - /// Retrieve item lores from NBT properties. Returns null if no lores is defined. + /// Retrieve item lores. For 1.20.6+ reads from LoreNameComponent1206; for older versions reads from NBT. /// public string[]? Lores { get { + if (Components != null) + { + var loreComponent = Components.OfType().FirstOrDefault(); + if (loreComponent != null && loreComponent.Lines.Count > 0) + return loreComponent.Lines.ToArray(); + + return null; + } + List lores = new(); if (NBT != null && NBT.ContainsKey("display")) { @@ -107,12 +145,21 @@ namespace MinecraftClient.Inventory } /// - /// Retrieve item damage from NBT properties. Returns 0 if no damage is defined. + /// Retrieve item damage. For 1.20.6+ reads from DamageComponent; for older versions reads from NBT. /// public int Damage { get { + if (Components != null) + { + var damageComponent = Components.OfType().FirstOrDefault(); + if (damageComponent != null) + return damageComponent.Damage; + + return 0; + } + if (NBT != null && NBT.ContainsKey("Damage")) { object damage = NBT["Damage"]; @@ -127,6 +174,26 @@ namespace MinecraftClient.Inventory } } + /// + /// Retrieve enchantments from structured components (1.20.6+). Returns null for older versions. + /// Both normal enchantments (EnchantmentsComponent) and stored enchantments + /// (StoredEnchantmentsComponent, e.g. enchanted books) are checked. + /// + public List? EnchantmentList + { + get + { + if (Components == null) + return null; + + var enchComp = Components.OfType().FirstOrDefault(); + if (enchComp != null && enchComp.Enchantments.Count > 0) + return enchComp.Enchantments; + + return null; + } + } + public static string GetTypeString(ItemType type) { string type_str = type.ToString(); @@ -152,8 +219,18 @@ namespace MinecraftClient.Inventory try { - if (NBT != null && (NBT.TryGetValue("Enchantments", out object? enchantments) || - NBT.TryGetValue("StoredEnchantments", out enchantments))) + var enchList = EnchantmentList; + if (enchList != null) + { + foreach (var ench in enchList) + { + string name = EnchantmentMapping.GetEnchantmentName(ench.Type); + string level = EnchantmentMapping.ConvertLevelToRomanNumbers(ench.Level); + sb.AppendFormat(" | {0} {1}", name, level); + } + } + else if (NBT != null && (NBT.TryGetValue("Enchantments", out object? enchantments) || + NBT.TryGetValue("StoredEnchantments", out enchantments))) { foreach (Dictionary enchantment in (object[])enchantments) { diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 96dcb615..e9b0b38c 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; @@ -1549,7 +1549,7 @@ namespace MinecraftClient /// Record changes private static void StoreInNewSlot(Container inventory, Item item, int slotId, int newSlotId, List> changedSlots) { - Item newItem = new(item.Type, item.Count, item.NBT); + Item newItem = item.CloneWithCount(item.Count); inventory.Items[newSlotId] = newItem; inventory.Items.Remove(slotId); @@ -1672,7 +1672,7 @@ namespace MinecraftClient { // Drop 1 item count from cursor Item itemTmp = playerInventory.Items[-1]; - Item itemClone = new(itemTmp.Type, 1, itemTmp.NBT); + Item itemClone = itemTmp.CloneWithCount(1); inventory.Items[slotId] = itemClone; playerInventory.Items[-1].Count--; } @@ -1701,14 +1701,14 @@ namespace MinecraftClient { // Can be evenly divided Item itemTmp = inventory.Items[slotId]; - playerInventory.Items[-1] = new Item(itemTmp.Type, itemTmp.Count / 2, itemTmp.NBT); + playerInventory.Items[-1] = itemTmp.CloneWithCount(itemTmp.Count / 2); inventory.Items[slotId].Count = itemTmp.Count / 2; } else { // Cannot be evenly divided. item count on cursor is always larger than item on inventory Item itemTmp = inventory.Items[slotId]; - playerInventory.Items[-1] = new Item(itemTmp.Type, (itemTmp.Count + 1) / 2, itemTmp.NBT); + playerInventory.Items[-1] = itemTmp.CloneWithCount((itemTmp.Count + 1) / 2); inventory.Items[slotId].Count = (itemTmp.Count - 1) / 2; } } diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index dc953c56..af09909c 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -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 /// Item slot representation public byte[] GetItemSlot(Item? item, ItemPalette itemPalette) { - // TODO: Wire up Structured components for 1.20.6 - List 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); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponent.cs index 7465740b..2c0b66e3 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponent.cs @@ -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; - + + /// + /// The registry type ID assigned during parsing, used for round-trip serialization. + /// + public int TypeId { get; set; } = -1; + public abstract void Parse(Queue data); public abstract Queue Serialize(); } \ No newline at end of file diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponentRegistry.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponentRegistry.cs index 11c48e1f..3ae07057 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponentRegistry.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Core/StructuredComponentRegistry.cs @@ -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; }