- Add AttributeSubComponent121 that uses ResourceLocation(string) instead
of UUID+Name, matching the 1.21 attribute modifier wire format change.
Register it in SubComponentRegistry121 via new ReplaceSubComponent method.
- Add ProjectilePower packet handler: reads 1 double (accelerationPower)
for 1.21+, or 3 doubles (xPower/yPower/zPower) for 1.20.6.
- Add CustomReportDetails and ServerLinks packet handlers in both Play
and Configuration phases, consuming all fields to prevent byte offset
errors on 1.21 servers.
Made-with: Cursor
Audited batch 5 components (ChargedProjectiles, BundleContents, Container,
WritableBookContent, BlockState, PotDecorations) against official 1.20.6
decompiled STREAM_CODEC definitions. All network encodings were correct.
Removed redundant NumberOfItems/NumberOfPages/NumberOfProperties fields from
ContainerComponent, WritableBlookContentComponent, BlockStateComponent, and
PotDecorationsComponent. Serialize now uses the actual collection .Count
instead of a potentially stale cached value, matching the pattern already
used by ContainerComponent's Serialize and other components.
Also modernized loop style (foreach with deconstruction where applicable)
and fixed a typo in an exception message ("setialize" -> "serialize").
Made-with: Cursor
In 1.20.6+, ComponentSerialization.STREAM_CODEC uses
ByteBufCodecs.fromCodecWithRegistries (NBT tag format), not plain string.
The previous implementation incorrectly used ReadNextString/GetString
for custom_name (5), item_name (6), and lore (7) components.
Fixed all three to use ReadNextNbt/GetNbt, preserving raw NBT data for
round-trip serialization while still extracting readable text via
ChatParser.ParseText(Dictionary).
Other batch 4 components (custom_data, entity_data, bucket_entity_data,
block_entity_data, debug_stick_state, map_decorations, recipes, lock,
container_loot, intangible_projectile — all NBT; hide_additional_tooltip,
hide_tooltip, fire_resistant, creative_slot_lock — all Unit/Empty;
note_block_sound — ResourceLocation string) were verified correct.
Made-with: Cursor
Audited all 14 simple binary components (batch 3): max_stack_size,
max_damage, damage, unbreakable, rarity, custom_model_data, repair_cost,
enchantment_glint_override, ominous_bottle_amplifier, dyed_color,
map_color, map_id, map_post_processing, base_color.
Found and fixed 1 bug:
- EnchantmentGlintOverrideComponent: was reading/writing VarInt but the
official STREAM_CODEC uses ByteBufCodecs.BOOL (single byte boolean).
Changed property type from int to bool, Parse from ReadNextVarInt to
ReadNextBool, and Serialize from GetVarInt to GetBool.
All other 13 components matched the official 1.20.6 STREAM_CODEC
definitions exactly.
Verified in-game: connected to 1.20.6 vanilla server, received items
with enchantment_glint_override=true/false, dyed_color, map_color,
map_id, base_color, unbreakable, rarity, custom_model_data, repair_cost,
damage, max_damage. All parsed and serialized correctly with no errors.
Made-with: Cursor
Audited all 8 batch-2 components (enchantments, stored_enchantments,
can_place_on, can_break, lodestone_tracker, firework_explosion, fireworks,
banner_patterns, suspicious_stew_effects, bees) against official 1.20.6
decompiled STREAM_CODEC definitions.
Found and fixed 3 bugs in BlockPredicate/PropertySubComponent:
1. BlockPredicateSubcomponent.Serialize(): missing HasNbt bool write.
Parse reads the bool but Serialize skipped writing it, causing all
subsequent fields to be offset by one byte.
2. BlockPredicateSubcomponent.Serialize(): missing Properties list count
VarInt write. Parse reads VarInt count before iterating, but Serialize
only wrote the elements without the preceding count.
3. PropertySubComponent: RangedMatcher min/max values must use Optional
encoding (Bool prefix + conditional String), matching the official
ByteBufCodecs.either(ExactMatcher, RangedMatcher) where RangedMatcher
uses ByteBufCodecs.optional(STRING_UTF8) for both min and max fields.
Previously read/wrote plain Strings unconditionally.
Remaining 6 components (enchantments, stored_enchantments, lodestone_tracker,
firework_explosion, fireworks, banner_patterns, suspicious_stew_effects, bees)
verified correct — no changes needed.
Made-with: Cursor
Audited all 8 high-complexity structured components against official 1.20.6
decompiled STREAM_CODEC definitions. Found and fixed bugs in 3 components
plus a systemic NBT serialization issue:
TrimComponent (ID 35):
- Serialize had TrimPatternType and ShowInTooltip incorrectly nested inside
the TrimMaterialType==0 branch; moved them outside to match Parse logic
- Description fields (TrimMaterial.description, TrimPattern.description) were
read/written as String but official codec uses ComponentSerialization
(NBT Tag format); changed to ReadNextNbt/GetNbt
ProfileComponent (ID 46):
- Serialize was missing the HasUniqueId Bool prefix before UUID
- Serialize only wrote properties when count > 0 but omitted the VarInt count
prefix entirely when empty; now always writes VarInt count
WrittenBookContentComponent (ID 34):
- Page content uses Filterable<Component> where Component is NBT-encoded via
ComponentSerialization.STREAM_CODEC, not plain String; changed Parse to use
ReadNextNbt and Serialize to use GetNbt
- Added RawContentNbt/FilteredContentNbt fields to BookPage record for
round-trip NBT preservation
- Removed unnecessary ChatParser.ParseText on title (it's a plain string)
DataTypes.GetNbt:
- Added TAG_String root support for 1.20.4+ (chat components like "Page 1"
are encoded as TAG_String, not TAG_Compound)
- Fixed root name handling: versions >= 1.20.2 omit the root compound name,
but GetNbt was unconditionally writing it
Components confirmed correct (no changes needed):
- FoodComponentComponent (ID 20), ToolComponent (ID 22),
InstrumentComponent (ID 40), PotionContentsComponent (ID 31),
AttributeModifiersComponent (ID 12)
Made-with: Cursor
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