From 1e2b853b14e241089f9637f5ad99efe9aa29384f Mon Sep 17 00:00:00 2001 From: BruceChen Date: Thu, 19 Mar 2026 01:44:23 +0800 Subject: [PATCH] Fix PotionContentsComponent and InstrumentComponent serialization for 1.20.6 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 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 --- .../Components/1_20_6/InstrumentComponent.cs | 57 ++++++++++--------- .../1_20_6/PotionContentsComponent.cs | 39 +++++++------ 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/InstrumentComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/InstrumentComponent.cs index 87bb17be..ccfcf915 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/InstrumentComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/InstrumentComponent.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Protocol.Handlers.StructuredComponents.Core; @@ -8,60 +7,62 @@ namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_2 public class InstrumentComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry) { - public int InstrumentType { get; set; } - public int SoundEventType { get; set; } - public string? SoundName { get; set; } = null!; + // holder ID: 0 = inline instrument data, N>0 = registry reference (id = N-1) + public int InstrumentHolderId { get; set; } + + // Inline instrument fields (only when InstrumentHolderId == 0): + // holder ID for SoundEvent: 0 = inline sound, N>0 = registry reference (id = N-1) + public int SoundEventHolderId { get; set; } + // Inline SoundEvent fields (only when SoundEventHolderId == 0): + public string? SoundLocation { get; set; } public bool HasFixedRange { get; set; } public float FixedRange { get; set; } - public float UseDuration { get; set; } + + public int UseDuration { get; set; } public float Range { get; set; } public override void Parse(Queue data) { - InstrumentType = dataTypes.ReadNextVarInt(data); + InstrumentHolderId = dataTypes.ReadNextVarInt(data); - if (InstrumentType == 0) + if (InstrumentHolderId == 0) { - SoundEventType = dataTypes.ReadNextVarInt(data); - SoundName = dataTypes.ReadNextString(data); + SoundEventHolderId = dataTypes.ReadNextVarInt(data); - if (SoundEventType == 0) + if (SoundEventHolderId == 0) { + SoundLocation = dataTypes.ReadNextString(data); HasFixedRange = dataTypes.ReadNextBool(data); - FixedRange = dataTypes.ReadNextFloat(data); + if (HasFixedRange) + FixedRange = dataTypes.ReadNextFloat(data); } - UseDuration = dataTypes.ReadNextFloat(data); + UseDuration = dataTypes.ReadNextVarInt(data); Range = dataTypes.ReadNextFloat(data); } - - // TODO: Check, if we need to load in defaults from a registry } public override Queue Serialize() { var data = new List(); - data.AddRange(DataTypes.GetVarInt(InstrumentType)); + data.AddRange(DataTypes.GetVarInt(InstrumentHolderId)); - if (InstrumentType == 0) + if (InstrumentHolderId == 0) { - data.AddRange(DataTypes.GetVarInt(SoundEventType)); + data.AddRange(DataTypes.GetVarInt(SoundEventHolderId)); - if (string.IsNullOrEmpty(SoundName)) - throw new NullReferenceException("Can't serialize InstrumentComponent because SoundName is empty!"); - - data.AddRange(DataTypes.GetString(SoundName)); - if (SoundEventType == 0) + if (SoundEventHolderId == 0) { + data.AddRange(DataTypes.GetString(SoundLocation ?? "")); data.AddRange(DataTypes.GetBool(HasFixedRange)); - data.AddRange(DataTypes.GetFloat(FixedRange)); + if (HasFixedRange) + data.AddRange(DataTypes.GetFloat(FixedRange)); } - - data.AddRange(DataTypes.GetFloat(UseDuration)); + + data.AddRange(DataTypes.GetVarInt(UseDuration)); data.AddRange(DataTypes.GetFloat(Range)); } - - // TODO: Check, if we need to load in defaults from a registry if InstrumentType != 0 and send them + return new Queue(data); } -} \ No newline at end of file +} diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/PotionContentsComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/PotionContentsComponent.cs index 715cacd3..e8b3443e 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/PotionContentsComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/PotionContentsComponent.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents; @@ -11,21 +10,23 @@ public class PotionContentsComponent(DataTypes dataTypes, ItemPalette itemPalett : StructuredComponent(dataTypes, itemPalette, subComponentRegistry) { public bool HasPotionId { get; set; } - public int PotiononId { get; set; } + public int PotionId { get; set; } public bool HasCustomColor { get; set; } public int CustomColor { get; set; } - public int NumberOfCustomEffects { get; set; } public List Effects { get; set; } = new(); public override void Parse(Queue data) { HasPotionId = dataTypes.ReadNextBool(data); - PotiononId = HasPotionId ? dataTypes.ReadNextVarInt(data) : 0; // TODO: Find from the registry + if (HasPotionId) + PotionId = dataTypes.ReadNextVarInt(data); + HasCustomColor = dataTypes.ReadNextBool(data); - CustomColor = HasCustomColor ? dataTypes.ReadNextInt(data) : 0; // TODO: Find from the registry - NumberOfCustomEffects = dataTypes.ReadNextVarInt(data); - - for(var i = 0; i < NumberOfCustomEffects; i++) + if (HasCustomColor) + CustomColor = dataTypes.ReadNextInt(data); + + var numberOfEffects = dataTypes.ReadNextVarInt(data); + for (var i = 0; i < numberOfEffects; i++) Effects.Add((PotionEffectSubComponent)subComponentRegistry.ParseSubComponent(SubComponents.PotionEffect, data)); } @@ -33,19 +34,17 @@ public class PotionContentsComponent(DataTypes dataTypes, ItemPalette itemPalett { var data = new List(); data.AddRange(DataTypes.GetBool(HasPotionId)); - data.AddRange(DataTypes.GetVarInt(PotiononId)); - data.AddRange(DataTypes.GetBool(HasCustomColor)); - data.AddRange(DataTypes.GetInt(CustomColor)); + if (HasPotionId) + data.AddRange(DataTypes.GetVarInt(PotionId)); - if (NumberOfCustomEffects > 0) - { - if(Effects.Count != NumberOfCustomEffects) - throw new ArgumentNullException($"Can not serialize PotionContentsComponentComponent1206 due to NumberOfCustomEffects being different from the count of elements in the Effects list!"); - - foreach(var effect in Effects) - data.AddRange(effect.Serialize()); - } + data.AddRange(DataTypes.GetBool(HasCustomColor)); + if (HasCustomColor) + data.AddRange(DataTypes.GetInt(CustomColor)); + + data.AddRange(DataTypes.GetVarInt(Effects.Count)); + foreach (var effect in Effects) + data.AddRange(effect.Serialize()); return new Queue(data); } -} \ No newline at end of file +}