EnchantmentsComponent (used by both regular and stored enchantments)
was directly casting the registry VarInt ID to the Enchantments enum
via (Enchantments)id. However, the Enchantments enum is ordered
alphabetically (AquaAffinity=0, BaneOfArthropods=1, ..., Sharpness=32)
while the 1.20.6 registry uses a completely different order
(protection=0, fire_protection=1, ..., sharpness=13). This caused all
enchantment names to display incorrectly (e.g. Sharpness V shown as
"Unknown Enchantment with ID: 32").
Changes:
- Parse now uses EnchantmentMapping.GetEnchantmentByRegistryId1206()
to properly map registry IDs to enum values via the existing
1.20.6+ mapping table
- Serialize now uses EnchantmentMapping.GetRegistryId1206ByEnchantment()
to convert enum values back to registry IDs (reverse lookup)
- Fixed translation key prefix: "Enchantments.minecraft." (wrong) ->
"enchantment.minecraft." (matches en_us.json resource keys)
- Fixed 3 long-standing typos in the Enchantments enum that prevented
translation lookup from matching resource keys:
- DepthStrieder -> DepthStrider (depth_strieder vs depth_strider)
- Efficency -> Efficiency (efficency vs efficiency)
- Loyality -> Loyalty (loyality vs loyalty)
Verified on vanilla 1.20.6 server: items with sharpness, efficiency,
unbreaking, fortune, mending, and bane_of_arthropods all display
correct localized names (锋利, 效率, 耐久, 时运, 经验修补, 节肢杀手).
Made-with: Cursor
Both components had incorrect Parse/Serialize implementations that would
cause packet deserialization misalignment when encountered in-game.
PotionContentsComponent (3 bugs):
- Serialize unconditionally wrote VarInt(PotionId) and Int(CustomColor)
even when HasPotionId/HasCustomColor was false. The official format
(PotionContents.STREAM_CODEC) uses Optional encoding: Bool(hasValue)
followed by the value only when true. The extra bytes caused all
subsequent fields in the packet to be read at wrong offsets.
- Serialize omitted the VarInt(count) prefix for the custom effects list.
The official codec uses ByteBufCodecs.list() which always writes a
VarInt count header before the list elements.
- Also fixed typo: PotiononId -> PotionId.
InstrumentComponent (3 bugs):
- The official Instrument.STREAM_CODEC uses ByteBufCodecs.holder() which
encodes as VarInt(holderId): 0 = inline data, N>0 = registry ref (N-1).
The SoundEvent field inside uses the same holder pattern. The old code
unconditionally read SoundName (ResourceLocation) and HasFixedRange/
FixedRange even when SoundEventHolderId != 0 (registry reference case
has no inline data).
- UseDuration was read/written as Float, but the official codec uses
ByteBufCodecs.VAR_INT. This caused a 4-byte vs variable-length
mismatch that would shift all subsequent data.
- HasFixedRange was read unconditionally when SoundEventHolderId == 0,
but FixedRange was also read unconditionally. The official SoundEvent
DIRECT_STREAM_CODEC uses Optional<Float> encoding: Bool(hasValue)
followed by Float only when true.
These components are used for potion items and goat horns respectively.
Verified against official 1.20.6 decompiled source:
- net.minecraft.world.item.alchemy.PotionContents (STREAM_CODEC)
- net.minecraft.world.item.Instrument (STREAM_CODEC/DIRECT_STREAM_CODEC)
- net.minecraft.sounds.SoundEvent (STREAM_CODEC/DIRECT_STREAM_CODEC)
- net.minecraft.network.codec.ByteBufCodecs (holder/optional/list)
Made-with: Cursor
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