From 56f2426c1f618c031de0e4d9f96482a43a7d15b7 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Thu, 19 Mar 2026 01:37:56 +0800 Subject: [PATCH] Fix StructuredComponent serialization correctness for 1.20.6 Audited all 58 StructuredComponent subclasses against the official Minecraft 1.20.6 decompiled source to verify Parse()/Serialize() symmetry. Found and fixed four bugs across four components: 1. ContainerComponent: Parse() skipped null item slots (empty slots in a container) but Serialize() looped NumberOfItems times using Items[i], causing IndexOutOfRangeException when any slot was empty. The official ItemContainerContents uses OPTIONAL_STREAM_CODEC which serializes empty slots as VarInt(0). Fixed: Parse now stores all slots including nulls, Serialize uses Items.Count and iterates all entries. GetItemSlot(null) correctly writes VarInt(0) for empty slots. 2. ChargedProjectilesComponent: Used Items.OfType() in Serialize() which silently dropped null entries, causing the serialized count to differ from the written VarInt header. The official ChargedProjectiles uses STREAM_CODEC (non-optional, no empty slots allowed). Fixed: Items list is now List (non-nullable), Parse defensively skips nulls, Serialize writes Items.Count matching the actual list. 3. BundleContentsComponent: Same issue as ChargedProjectilesComponent. Applied the same fix pattern. 4. FoodComponentComponent: Two type mismatches vs the official FoodProperties.DIRECT_STREAM_CODEC: - Saturation was declared as bool and read with ReadNextBool (1 byte), but the protocol sends it as float (4 bytes). This caused all subsequent fields in the component to be read at wrong offsets, corrupting CanAlwaysEat, SecondsToEat, and the effects list. - NumberOfEffects was serialized with GetFloat() instead of GetVarInt(), writing 4 bytes of IEEE 754 float instead of a variable-length integer. Fixed both Parse and Serialize to use correct types. Also removed redundant NumberOfItems/NumberOfEffects fields from components where the count is derivable from the list length, and replaced ArgumentNullException with cleaner patterns. Tested end-to-end on vanilla 1.20.6 server: item receiving (diamond_sword, golden_apple, diamond_pickaxe), inventory slot movement (click to pick up and place), and inventory listing all work correctly with no server-side protocol errors. Made-with: Cursor --- .../1_20_6/BundleContentsComponent.cs | 24 +++++++++---------- .../1_20_6/ChargedProjectilesComponent.cs | 24 +++++++++---------- .../Components/1_20_6/ContainerComponent.cs | 19 +++++---------- .../1_20_6/FoodComponentComponent.cs | 23 +++++++----------- 4 files changed, 36 insertions(+), 54 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/BundleContentsComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/BundleContentsComponent.cs index 063d1d20..5c5044f7 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/BundleContentsComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/BundleContentsComponent.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using MinecraftClient.Inventory; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Protocol.Handlers.StructuredComponents.Core; @@ -10,28 +8,28 @@ namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_2 public class BundleContentsComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry) { - public int NumberOfItems { get; set; } - public List Items { get; set; } = []; + public List Items { get; set; } = []; public override void Parse(Queue data) { - NumberOfItems = dataTypes.ReadNextVarInt(data); + var count = dataTypes.ReadNextVarInt(data); - for (var i = 0; i < NumberOfItems; i++) - Items.Add(dataTypes.ReadNextItemSlot(data, itemPalette)); + for (var i = 0; i < count; i++) + { + var item = dataTypes.ReadNextItemSlot(data, itemPalette); + if (item != null) + Items.Add(item); + } } public override Queue Serialize() { var data = new List(); - data.AddRange(DataTypes.GetVarInt(NumberOfItems)); + data.AddRange(DataTypes.GetVarInt(Items.Count)); - if (NumberOfItems != Items.Count) - throw new ArgumentNullException($"Cannot serialize BundleContentsComponent1206 because NumberOfItems != Items.Count!"); - - foreach (var item in Items.OfType()) + foreach (var item in Items) data.AddRange(DataTypes.GetItemSlot(item, itemPalette)); return new Queue(data); } -} \ No newline at end of file +} diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ChargedProjectilesComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ChargedProjectilesComponent.cs index ef0875ef..7305ee8c 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ChargedProjectilesComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ChargedProjectilesComponent.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using MinecraftClient.Inventory; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Protocol.Handlers.StructuredComponents.Core; @@ -10,28 +8,28 @@ namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_2 public class ChargedProjectilesComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry) { - public int NumberOfItems { get; set; } - public List Items { get; set; } = []; + public List Items { get; set; } = []; public override void Parse(Queue data) { - NumberOfItems = dataTypes.ReadNextVarInt(data); + var count = dataTypes.ReadNextVarInt(data); - for (var i = 0; i < NumberOfItems; i++) - Items.Add(dataTypes.ReadNextItemSlot(data, itemPalette)); + for (var i = 0; i < count; i++) + { + var item = dataTypes.ReadNextItemSlot(data, itemPalette); + if (item != null) + Items.Add(item); + } } public override Queue Serialize() { var data = new List(); - data.AddRange(DataTypes.GetVarInt(NumberOfItems)); + data.AddRange(DataTypes.GetVarInt(Items.Count)); - if (NumberOfItems != Items.Count) - throw new ArgumentNullException($"Cannot serialize ChargedProjectilesComponent1206 because NumberOfItems != Items.Count!"); - - foreach (var item in Items.OfType()) + foreach (var item in Items) data.AddRange(DataTypes.GetItemSlot(item, itemPalette)); return new Queue(data); } -} \ No newline at end of file +} diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ContainerComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ContainerComponent.cs index 2b050aff..053fc918 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ContainerComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/ContainerComponent.cs @@ -9,29 +9,22 @@ public class ContainerComponent(DataTypes dataTypes, ItemPalette itemPalette, Su : StructuredComponent(dataTypes, itemPalette, subComponentRegistry) { public int NumberOfItems { get; set; } - public List Items { get; set; } = []; + public List Items { get; set; } = []; public override void Parse(Queue data) { NumberOfItems = dataTypes.ReadNextVarInt(data); for (var i = 0; i < NumberOfItems; i++) - { - var item = dataTypes.ReadNextItemSlot(data, ItemPalette); - - if (item is null) - continue; - - Items.Add(item); - } + Items.Add(dataTypes.ReadNextItemSlot(data, ItemPalette)); } public override Queue Serialize() { var data = new List(); - data.AddRange(DataTypes.GetVarInt(NumberOfItems)); - for (var i = 0; i < NumberOfItems; i++) - data.AddRange(DataTypes.GetItemSlot(Items[i], itemPalette)); + data.AddRange(DataTypes.GetVarInt(Items.Count)); + foreach (var item in Items) + data.AddRange(DataTypes.GetItemSlot(item, itemPalette)); return new Queue(data); } -} \ No newline at end of file +} diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs index f848750d..aac861c2 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs @@ -11,21 +11,20 @@ public class FoodComponentComponent(DataTypes dataTypes, ItemPalette itemPalette : StructuredComponent(dataTypes, itemPalette, subComponentRegistry) { public int Nutrition { get; set; } - public bool Saturation { get; set; } + public float Saturation { get; set; } public bool CanAlwaysEat { get; set; } public float SecondsToEat { get; set; } - public int NumberOfEffects { get; set; } public List Effects { get; set; } = new(); public override void Parse(Queue data) { Nutrition = dataTypes.ReadNextVarInt(data); - Saturation = dataTypes.ReadNextBool(data); + Saturation = dataTypes.ReadNextFloat(data); CanAlwaysEat = dataTypes.ReadNextBool(data); SecondsToEat = dataTypes.ReadNextFloat(data); - NumberOfEffects = dataTypes.ReadNextVarInt(data); + var numberOfEffects = dataTypes.ReadNextVarInt(data); - for(var i = 0; i < NumberOfEffects; i++) + for(var i = 0; i < numberOfEffects; i++) Effects.Add((EffectSubComponent)subComponentRegistry.ParseSubComponent(SubComponents.Effect, data)); } @@ -33,19 +32,13 @@ public class FoodComponentComponent(DataTypes dataTypes, ItemPalette itemPalette { var data = new List(); data.AddRange(DataTypes.GetVarInt(Nutrition)); - data.AddRange(DataTypes.GetBool(Saturation)); + data.AddRange(DataTypes.GetFloat(Saturation)); data.AddRange(DataTypes.GetBool(CanAlwaysEat)); data.AddRange(DataTypes.GetFloat(SecondsToEat)); - data.AddRange(DataTypes.GetFloat(NumberOfEffects)); + data.AddRange(DataTypes.GetVarInt(Effects.Count)); - if (NumberOfEffects > 0) - { - if(Effects.Count != NumberOfEffects) - throw new ArgumentNullException($"Can not serialize FoodComponent1206 due to NumberOfEffcets being different from the count of elements in the Effects list!"); - - foreach(var effect in Effects) - data.AddRange(effect.Serialize()); - } + foreach(var effect in Effects) + data.AddRange(effect.Serialize()); return new Queue(data); }