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<Item>() 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<Item> (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
This commit is contained in:
BruceChen 2026-03-19 01:37:56 +08:00
parent b692b13bbc
commit 56f2426c1f
4 changed files with 36 additions and 54 deletions

View file

@ -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<Item?> Items { get; set; } = [];
public List<Item> Items { get; set; } = [];
public override void Parse(Queue<byte> 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<byte> Serialize()
{
var data = new List<byte>();
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<Item>())
foreach (var item in Items)
data.AddRange(DataTypes.GetItemSlot(item, itemPalette));
return new Queue<byte>(data);
}
}
}

View file

@ -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<Item?> Items { get; set; } = [];
public List<Item> Items { get; set; } = [];
public override void Parse(Queue<byte> 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<byte> Serialize()
{
var data = new List<byte>();
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<Item>())
foreach (var item in Items)
data.AddRange(DataTypes.GetItemSlot(item, itemPalette));
return new Queue<byte>(data);
}
}
}

View file

@ -9,29 +9,22 @@ public class ContainerComponent(DataTypes dataTypes, ItemPalette itemPalette, Su
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int NumberOfItems { get; set; }
public List<Item> Items { get; set; } = [];
public List<Item?> Items { get; set; } = [];
public override void Parse(Queue<byte> 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<byte> Serialize()
{
var data = new List<byte>();
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<byte>(data);
}
}
}

View file

@ -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<EffectSubComponent> Effects { get; set; } = new();
public override void Parse(Queue<byte> 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<byte>();
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<byte>(data);
}