Fix structured components: audit fixes, codec helpers, and serialize implementations

- Fix 5 class naming typos: Unbrekable->Unbreakable, Blook->Book,
  Omnious->Ominous, FoodComponentComponent->FoodComponent
- Fix IntangibleProjectileComponent: extends EmptyComponent (Unit type)
- Delete dead code JukeBoxPlayableComponent.cs (duplicated)
- Implement proper Serialize() on BlocksAttacksComponent,
  KineticWeaponComponent, PiercingWeaponComponent
- Extract shared SoundEventHolder/HolderSet codec helpers into
  StructuredComponentCodecHelpers
- Add TypedEntityDataComponent/TypedBlockEntityDataComponent base classes
  for 1.21.9+ typed entity data format
- Fix variable name: uses1218AttributeAndEquippableFormats ->
  uses1216AttributeAndEquippableFormats
- Add entity data version gate (1.21.9+) to v1215 registry
- Fix chicken/variant and zombie_nautilus/variant component types
- Fix empty packet hang in Protocol18 ReadNextPacket loop
- Add structured components integration test harness
This commit is contained in:
Anon 2026-06-11 21:33:48 +02:00
parent 16637f7eba
commit de96462659
22 changed files with 761 additions and 260 deletions

View file

@ -100,7 +100,7 @@ public static class BookContentHelper
{
if (item.Components is not null)
{
var component = item.Components.OfType<WritableBlookContentComponent>().FirstOrDefault();
var component = item.Components.OfType<WritableBookContentComponent>().FirstOrDefault();
if (component is not null)
{
content = new BookContent(
@ -121,7 +121,7 @@ public static class BookContentHelper
{
if (item.Components is not null)
{
var component = item.Components.OfType<WrittenBlookContentComponent>().FirstOrDefault();
var component = item.Components.OfType<WrittenBookContentComponent>().FirstOrDefault();
if (component is not null)
{
content = new BookContent(

View file

@ -362,7 +362,10 @@ namespace MinecraftClient.Protocol.Handlers
{
while (socketWrapper.HasDataAvailable())
{
packetQueue.Add(ReadNextPacket(), cancelToken);
var packet = ReadNextPacket();
if (packet.Item1 == -1)
continue;
packetQueue.Add(packet, cancelToken);
if (cancelToken.IsCancellationRequested)
break;
@ -418,7 +421,8 @@ namespace MinecraftClient.Protocol.Handlers
internal Tuple<int, Queue<byte>> ReadNextPacket()
{
var size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size
Queue<byte> packetData = new(socketWrapper.ReadDataRAW(size)); //Packet contents
var rawBytes = socketWrapper.ReadDataRAW(size);
Queue<byte> packetData = new(rawBytes); //Packet contents
var compressed = false;
var sizeUncompressed = 0;
@ -436,6 +440,13 @@ namespace MinecraftClient.Protocol.Handlers
}
}
if (packetData.Count == 0)
{
var rawHex = rawBytes.Length > 0 ? BitConverter.ToString(rawBytes).Replace("-", " ") : "(empty)";
log.Warn($"[DEBUG] Empty packet after decompress: size={size}, sizeUncompressed={sizeUncompressed}, protocol={protocolVersion}, state={currentState}, rawBytes=[{rawHex}]");
return new(-1, packetData);
}
var packetId = dataTypes.ReadNextVarInt(packetData); // Packet ID
LogIncomingPacket(packetId, packetData.Count, size, compressed, sizeUncompressed);
if (handler.GetNetworkPacketCaptureEnabled())

View file

@ -7,7 +7,7 @@ using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
public class FoodComponentComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
public class FoodComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int Nutrition { get; set; }

View file

@ -5,19 +5,6 @@ using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
public class IntangibleProjectileComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
: EmptyComponent(dataTypes, itemPalette, subComponentRegistry)
{
public Dictionary<string, object>? Nbt { get; set; } = new();
public override void Parse(Queue<byte> data)
{
Nbt = DataTypes.ReadNextNbt(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetNbt(Nbt));
return new Queue<byte>(data);
}
}
}

View file

@ -4,7 +4,7 @@ using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
public class OmniousBottleAmplifierComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
public class OminousBottleAmplifierComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int Amplifier { get; set; }

View file

@ -4,20 +4,20 @@ using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
public class UnbrekableComponent1206(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
public class UnbreakableComponent1206(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public bool Unbrekable { get; set; }
public bool Unbreakable { get; set; }
public override void Parse(Queue<byte> data)
{
Unbrekable = DataTypes.ReadNextBool(data);
Unbreakable = DataTypes.ReadNextBool(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetBool(Unbrekable));
data.AddRange(DataTypes.GetBool(Unbreakable));
return new Queue<byte>(data);
}
}

View file

@ -6,7 +6,7 @@ using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
public class WritableBlookContentComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
public class WritableBookContentComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public List<BookPage> Pages { get; set; } = [];
@ -41,7 +41,7 @@ public class WritableBlookContentComponent(DataTypes dataTypes, ItemPalette item
if (page.HasFilteredContent)
{
if (page.FilteredContent is null)
throw new InvalidOperationException("Can not serialize WritableBlookContentComponent because page.HasFilteredContent = true, but FilteredContent is null!");
throw new InvalidOperationException("Can not serialize WritableBookContentComponent because page.HasFilteredContent = true, but FilteredContent is null!");
data.AddRange(DataTypes.GetString(page.FilteredContent));
}

View file

@ -7,7 +7,7 @@ using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
public class WrittenBlookContentComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
public class WrittenBookContentComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) : StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public string RawTitle { get; set; } = null!;
public bool HasFilteredTitle { get; set; }

View file

@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_21;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21;
public class JukeBoxPlayableComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public bool DirectMode { get; set; }
public string? SongName { get; set; }
public int? SongType { get; set; }
public SoundEventSubComponent? SoundEvent { get; set; }
public string? Description { get; set; }
public float? Duration { get; set; }
public int? Output { get; set; }
public bool ShowTooltip { get; set; }
public override void Parse(Queue<byte> data)
{
DirectMode = DataTypes.ReadNextBool(data);
if (!DirectMode)
SongName = DataTypes.ReadNextString(data);
if (DirectMode)
{
SongType = DataTypes.ReadNextVarInt(data);
if (SongType == 0)
{
SoundEvent =
(SoundEventSubComponent)SubComponentRegistry.ParseSubComponent(SubComponents.SoundEvent, data);
Description = DataTypes.ReadNextString(data);
Duration = DataTypes.ReadNextFloat(data);
Output = DataTypes.ReadNextVarInt(data);
}
}
ShowTooltip = DataTypes.ReadNextBool(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetBool(DirectMode));
if (!DirectMode)
{
if (string.IsNullOrEmpty(SongName?.Trim()))
throw new ArgumentNullException($"Can not serialize JukeBoxPlayableComponent due to SongName being null or empty!");
data.AddRange(DataTypes.GetString(SongName));
}
if (DirectMode)
{
if (SongType is null)
throw new ArgumentNullException($"Can not serialize JukeBoxPlayableComponent due to SongType being null!");
data.AddRange(DataTypes.GetVarInt((int)SongType));
if (SongType == 0)
{
if (SoundEvent is null)
throw new ArgumentNullException(
$"Can not serialize JukeBoxPlayableComponent due to SoundEvent being null");
data.AddRange(SoundEvent.Serialize());
if (string.IsNullOrEmpty(Description?.Trim()))
throw new ArgumentNullException(
$"Can not serialize JukeBoxPlayableComponent due to Description being null or empty!");
data.AddRange(DataTypes.GetString(Description));
if (Duration is null)
throw new ArgumentNullException(
$"Can not serialize JukeBoxPlayableComponent due to Duration being null!");
data.AddRange(DataTypes.GetFloat((float)Duration));
if (Output is null)
throw new ArgumentNullException(
$"Can not serialize JukeBoxPlayableComponent due to Description being null!");
data.AddRange(DataTypes.GetVarInt((int)Output));
}
}
data.AddRange(DataTypes.GetBool(ShowTooltip));
return new Queue<byte>(data);
}
}

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_11;
@ -7,41 +8,65 @@ namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_2
public class KineticWeaponComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int ContactCooldownTicks { get; set; }
public int DelayTicks { get; set; }
public KineticWeaponConditionData? DismountConditions { get; set; }
public KineticWeaponConditionData? KnockbackConditions { get; set; }
public KineticWeaponConditionData? DamageConditions { get; set; }
public float ForwardMovement { get; set; }
public float DamageMultiplier { get; set; }
public SoundEventHolderData? Sound { get; set; }
public SoundEventHolderData? HitSound { get; set; }
public override void Parse(Queue<byte> data)
{
DataTypes.ReadNextVarInt(data); // contactCooldownTicks
DataTypes.ReadNextVarInt(data); // delayTicks
ReadOptionalCondition(data); // dismountConditions
ReadOptionalCondition(data); // knockbackConditions
ReadOptionalCondition(data); // damageConditions
DataTypes.ReadNextFloat(data); // forwardMovement
DataTypes.ReadNextFloat(data); // damageMultiplier
ReadOptionalSoundEventHolder(data); // sound
ReadOptionalSoundEventHolder(data); // hitSound
ContactCooldownTicks = DataTypes.ReadNextVarInt(data);
DelayTicks = DataTypes.ReadNextVarInt(data);
DismountConditions = ReadOptionalCondition(data);
KnockbackConditions = ReadOptionalCondition(data);
DamageConditions = ReadOptionalCondition(data);
ForwardMovement = DataTypes.ReadNextFloat(data);
DamageMultiplier = DataTypes.ReadNextFloat(data);
Sound = StructuredComponentCodecHelpers.ReadOptionalSoundEventHolder(DataTypes, data);
HitSound = StructuredComponentCodecHelpers.ReadOptionalSoundEventHolder(DataTypes, data);
}
private void ReadOptionalCondition(Queue<byte> data)
private KineticWeaponConditionData? ReadOptionalCondition(Queue<byte> data)
{
if (!DataTypes.ReadNextBool(data)) return;
DataTypes.ReadNextVarInt(data); // maxDurationTicks
DataTypes.ReadNextFloat(data); // minSpeed
DataTypes.ReadNextFloat(data); // minRelativeSpeed
if (!DataTypes.ReadNextBool(data))
return null;
return new KineticWeaponConditionData(
DataTypes.ReadNextVarInt(data),
DataTypes.ReadNextFloat(data),
DataTypes.ReadNextFloat(data));
}
private void ReadOptionalSoundEventHolder(Queue<byte> data)
private void WriteOptionalCondition(List<byte> data, KineticWeaponConditionData? condition)
{
if (!DataTypes.ReadNextBool(data)) return;
var holderId = DataTypes.ReadNextVarInt(data);
if (holderId == 0)
{
DataTypes.ReadNextString(data);
if (DataTypes.ReadNextBool(data))
DataTypes.ReadNextFloat(data);
}
data.AddRange(DataTypes.GetBool(condition is not null));
if (condition is null)
return;
data.AddRange(DataTypes.GetVarInt(condition.MaxDurationTicks));
data.AddRange(DataTypes.GetFloat(condition.MinSpeed));
data.AddRange(DataTypes.GetFloat(condition.MinRelativeSpeed));
}
public override Queue<byte> Serialize()
{
return new Queue<byte>();
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(ContactCooldownTicks));
data.AddRange(DataTypes.GetVarInt(DelayTicks));
WriteOptionalCondition(data, DismountConditions);
WriteOptionalCondition(data, KnockbackConditions);
WriteOptionalCondition(data, DamageConditions);
data.AddRange(DataTypes.GetFloat(ForwardMovement));
data.AddRange(DataTypes.GetFloat(DamageMultiplier));
StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, Sound);
StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, HitSound);
return new Queue<byte>(data);
}
}
public sealed record KineticWeaponConditionData(int MaxDurationTicks, float MinSpeed, float MinRelativeSpeed);

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_11;
@ -9,29 +10,24 @@ public class PiercingWeaponComponent(DataTypes dataTypes, ItemPalette itemPalett
{
public bool DealsKnockback { get; set; }
public bool Dismounts { get; set; }
public SoundEventHolderData? Sound { get; set; }
public SoundEventHolderData? HitSound { get; set; }
public override void Parse(Queue<byte> data)
{
DealsKnockback = DataTypes.ReadNextBool(data);
Dismounts = DataTypes.ReadNextBool(data);
ReadOptionalSoundEventHolder(data);
ReadOptionalSoundEventHolder(data);
}
private void ReadOptionalSoundEventHolder(Queue<byte> data)
{
if (!DataTypes.ReadNextBool(data)) return;
var holderId = DataTypes.ReadNextVarInt(data);
if (holderId == 0)
{
DataTypes.ReadNextString(data);
if (DataTypes.ReadNextBool(data))
DataTypes.ReadNextFloat(data);
}
Sound = StructuredComponentCodecHelpers.ReadOptionalSoundEventHolder(DataTypes, data);
HitSound = StructuredComponentCodecHelpers.ReadOptionalSoundEventHolder(DataTypes, data);
}
public override Queue<byte> Serialize()
{
return new Queue<byte>();
var data = new List<byte>();
data.AddRange(DataTypes.GetBool(DealsKnockback));
data.AddRange(DataTypes.GetBool(Dismounts));
StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, Sound);
StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, HitSound);
return new Queue<byte>(data);
}
}

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_5;
@ -9,10 +10,13 @@ public class BlocksAttacksComponent(DataTypes dataTypes, ItemPalette itemPalette
{
public float BlockDelaySeconds { get; set; }
public float DisableCooldownScale { get; set; }
public List<byte[]> RawDamageReductions { get; set; } = [];
public List<DamageReductionData> DamageReductions { get; set; } = [];
public float ItemDamageThreshold { get; set; }
public float ItemDamageBase { get; set; }
public float ItemDamageFactor { get; set; }
public string? BypassedBy { get; set; }
public SoundEventHolderData? BlockSound { get; set; }
public SoundEventHolderData? DisableSound { get; set; }
public override void Parse(Queue<byte> data)
{
@ -25,11 +29,13 @@ public class BlocksAttacksComponent(DataTypes dataTypes, ItemPalette itemPalette
var horizontalBlockingAngle = DataTypes.ReadNextFloat(data);
var hasTypeFilter = DataTypes.ReadNextBool(data);
if (hasTypeFilter)
ReadHolderSet(data);
var typeFilter = hasTypeFilter
? StructuredComponentCodecHelpers.ReadHolderSet(DataTypes, data)
: null;
var baseDmg = DataTypes.ReadNextFloat(data);
var factor = DataTypes.ReadNextFloat(data);
DamageReductions.Add(new DamageReductionData(horizontalBlockingAngle, typeFilter, baseDmg, factor));
}
ItemDamageThreshold = DataTypes.ReadNextFloat(data);
@ -38,46 +44,38 @@ public class BlocksAttacksComponent(DataTypes dataTypes, ItemPalette itemPalette
var hasBypassedBy = DataTypes.ReadNextBool(data);
if (hasBypassedBy)
DataTypes.ReadNextString(data); // TagKey<DamageType> as ResourceLocation
BypassedBy = DataTypes.ReadNextString(data); // TagKey<DamageType> as ResourceLocation
var hasBlockSound = DataTypes.ReadNextBool(data);
if (hasBlockSound)
ReadSoundEventHolder(data);
var hasDisableSound = DataTypes.ReadNextBool(data);
if (hasDisableSound)
ReadSoundEventHolder(data);
}
private void ReadHolderSet(Queue<byte> data)
{
var sizeOrTag = DataTypes.ReadNextVarInt(data);
if (sizeOrTag == 0)
{
DataTypes.ReadNextString(data); // Tag ResourceLocation
}
else
{
var count = sizeOrTag - 1;
for (var i = 0; i < count; i++)
DataTypes.ReadNextVarInt(data); // Holder<DamageType> registry ids
}
}
private void ReadSoundEventHolder(Queue<byte> data)
{
var holderId = DataTypes.ReadNextVarInt(data);
if (holderId == 0)
{
DataTypes.ReadNextString(data); // ResourceLocation
var hasFixedRange = DataTypes.ReadNextBool(data);
if (hasFixedRange)
DataTypes.ReadNextFloat(data);
}
BlockSound = StructuredComponentCodecHelpers.ReadOptionalSoundEventHolder(DataTypes, data);
DisableSound = StructuredComponentCodecHelpers.ReadOptionalSoundEventHolder(DataTypes, data);
}
public override Queue<byte> Serialize()
{
return new Queue<byte>();
var data = new List<byte>();
data.AddRange(DataTypes.GetFloat(BlockDelaySeconds));
data.AddRange(DataTypes.GetFloat(DisableCooldownScale));
data.AddRange(DataTypes.GetVarInt(DamageReductions.Count));
foreach (var reduction in DamageReductions)
{
data.AddRange(DataTypes.GetFloat(reduction.HorizontalBlockingAngle));
data.AddRange(DataTypes.GetBool(reduction.Type is not null));
if (reduction.Type is not null)
StructuredComponentCodecHelpers.WriteHolderSet(DataTypes, data, reduction.Type);
data.AddRange(DataTypes.GetFloat(reduction.Base));
data.AddRange(DataTypes.GetFloat(reduction.Factor));
}
data.AddRange(DataTypes.GetFloat(ItemDamageThreshold));
data.AddRange(DataTypes.GetFloat(ItemDamageBase));
data.AddRange(DataTypes.GetFloat(ItemDamageFactor));
data.AddRange(DataTypes.GetBool(BypassedBy is not null));
if (BypassedBy is not null)
data.AddRange(DataTypes.GetString(BypassedBy));
StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, BlockSound);
StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, DisableSound);
return new Queue<byte>(data);
}
}
public sealed record DamageReductionData(float HorizontalBlockingAngle, HolderSetData? Type, float Base, float Factor);

View file

@ -1,30 +1,13 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._26_1;
public class TypedEntityDataComponent261(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int EntityTypeId { get; set; }
public Dictionary<string, object>? Nbt { get; set; }
public override void Parse(Queue<byte> data)
{
EntityTypeId = DataTypes.ReadNextVarInt(data);
Nbt = DataTypes.ReadNextNbt(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(EntityTypeId));
data.AddRange(DataTypes.GetNbt(Nbt));
return new Queue<byte>(data);
}
}
: TypedEntityDataComponent(dataTypes, itemPalette, subComponentRegistry)
{ }
public class BlockEntityDataComponent261(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: TypedEntityDataComponent261(dataTypes, itemPalette, subComponentRegistry)
: TypedBlockEntityDataComponent(dataTypes, itemPalette, subComponentRegistry)
{ }

View file

@ -0,0 +1,74 @@
using System.Collections.Generic;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
internal static class StructuredComponentCodecHelpers
{
public static HolderSetData ReadHolderSet(DataTypes dataTypes, Queue<byte> data)
{
var sizeOrTag = dataTypes.ReadNextVarInt(data);
if (sizeOrTag == 0)
return new HolderSetData(dataTypes.ReadNextString(data), []);
var count = sizeOrTag - 1;
var holderIds = new List<int>(count);
for (var i = 0; i < count; i++)
holderIds.Add(dataTypes.ReadNextVarInt(data));
return new HolderSetData(null, holderIds);
}
public static void WriteHolderSet(DataTypes dataTypes, List<byte> bytes, HolderSetData holderSet)
{
if (holderSet.Tag is not null)
{
bytes.AddRange(DataTypes.GetVarInt(0));
bytes.AddRange(dataTypes.GetString(holderSet.Tag));
return;
}
bytes.AddRange(DataTypes.GetVarInt(holderSet.HolderIds.Count + 1));
foreach (var holderId in holderSet.HolderIds)
bytes.AddRange(DataTypes.GetVarInt(holderId));
}
public static SoundEventHolderData ReadSoundEventHolder(DataTypes dataTypes, Queue<byte> data)
{
var holderId = dataTypes.ReadNextVarInt(data);
if (holderId != 0)
return new SoundEventHolderData(holderId, null, false, 0);
var soundLocation = dataTypes.ReadNextString(data);
var hasFixedRange = dataTypes.ReadNextBool(data);
var fixedRange = hasFixedRange ? dataTypes.ReadNextFloat(data) : 0;
return new SoundEventHolderData(holderId, soundLocation, hasFixedRange, fixedRange);
}
public static void WriteSoundEventHolder(DataTypes dataTypes, List<byte> bytes, SoundEventHolderData soundEvent)
{
bytes.AddRange(DataTypes.GetVarInt(soundEvent.HolderId));
if (soundEvent.HolderId != 0)
return;
bytes.AddRange(dataTypes.GetString(soundEvent.SoundLocation ?? ""));
bytes.AddRange(dataTypes.GetBool(soundEvent.HasFixedRange));
if (soundEvent.HasFixedRange)
bytes.AddRange(dataTypes.GetFloat(soundEvent.FixedRange));
}
public static SoundEventHolderData? ReadOptionalSoundEventHolder(DataTypes dataTypes, Queue<byte> data)
{
return dataTypes.ReadNextBool(data) ? ReadSoundEventHolder(dataTypes, data) : null;
}
public static void WriteOptionalSoundEventHolder(DataTypes dataTypes, List<byte> bytes, SoundEventHolderData? soundEvent)
{
bytes.AddRange(dataTypes.GetBool(soundEvent is not null));
if (soundEvent is not null)
WriteSoundEventHolder(dataTypes, bytes, soundEvent);
}
}
public sealed record HolderSetData(string? Tag, List<int> HolderIds);
public sealed record SoundEventHolderData(int HolderId, string? SoundLocation, bool HasFixedRange, float FixedRange);

View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
public class TypedEntityDataComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int EntityTypeId { get; set; }
public Dictionary<string, object>? Nbt { get; set; }
public override void Parse(Queue<byte> data)
{
EntityTypeId = DataTypes.ReadNextVarInt(data);
Nbt = DataTypes.ReadNextNbt(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(EntityTypeId));
data.AddRange(DataTypes.GetNbt(Nbt));
return new Queue<byte>(data);
}
}
public class TypedBlockEntityDataComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: TypedEntityDataComponent(dataTypes, itemPalette, subComponentRegistry)
{ }

View file

@ -13,7 +13,7 @@ public class StructuredComponentsRegistry1206 : StructuredComponentRegistry
RegisterComponent<MaxStackSizeComponent>(1, "minecraft:max_stack_size");
RegisterComponent<MaxDamageComponent>(2, "minecraft:max_damage");
RegisterComponent<DamageComponent>(3, "minecraft:damage");
RegisterComponent<UnbrekableComponent1206>(4, "minecraft:unbreakable");
RegisterComponent<UnbreakableComponent1206>(4, "minecraft:unbreakable");
RegisterComponent<CustomNameComponent>(5, "minecraft:custom_name");
RegisterComponent<ItemNameComponent>(6, "minecraft:item_name");
RegisterComponent<LoreNameComponent1206>(7, "minecraft:lore");
@ -29,7 +29,7 @@ public class StructuredComponentsRegistry1206 : StructuredComponentRegistry
RegisterComponent<CreativeSlotLockComponent>(17, "minecraft:creative_slot_lock");
RegisterComponent<EnchantmentGlintOverrideComponent>(18, "minecraft:enchantment_glint_override");
RegisterComponent<IntangibleProjectileComponent>(19, "minecraft:intangible_projectile");
RegisterComponent<FoodComponentComponent>(20, "minecraft:food");
RegisterComponent<FoodComponent>(20, "minecraft:food");
RegisterComponent<FireResistantComponent>(21, "minecraft:fire_resistant");
RegisterComponent<ToolComponent>(22, "minecraft:tool");
RegisterComponent<StoredEnchantmentsComponent>(23, "minecraft:stored_enchantments");
@ -42,15 +42,15 @@ public class StructuredComponentsRegistry1206 : StructuredComponentRegistry
RegisterComponent<BundleContentsComponent>(30, "minecraft:bundle_contents");
RegisterComponent<PotionContentsComponent>(31, "minecraft:potion_contents");
RegisterComponent<SuspiciousStewEffectsComponent>(32, "minecraft:suspicious_stew_effects");
RegisterComponent<WritableBlookContentComponent>(33, "minecraft:writable_book_content");
RegisterComponent<WrittenBlookContentComponent>(34, "minecraft:written_book_content");
RegisterComponent<WritableBookContentComponent>(33, "minecraft:writable_book_content");
RegisterComponent<WrittenBookContentComponent>(34, "minecraft:written_book_content");
RegisterComponent<TrimComponent>(35, "minecraft:trim");
RegisterComponent<DebugStickStateComponent>(36, "minecraft:debug_stick_state");
RegisterComponent<EntityDataComponent>(37, "minecraft:entity_data");
RegisterComponent<BucketEntityDataComponent>(38, "minecraft:bucket_entity_data");
RegisterComponent<BlockEntityDataComponent>(39, "minecraft:block_entity_data");
RegisterComponent<InstrumentComponent>(40, "minecraft:instrument");
RegisterComponent<OmniousBottleAmplifierComponent>(41, "minecraft:ominous_bottle_amplifier");
RegisterComponent<OminousBottleAmplifierComponent>(41, "minecraft:ominous_bottle_amplifier");
RegisterComponent<RecipesComponent>(42, "minecraft:recipes");
RegisterComponent<LodestoneTrackerComponent>(43, "minecraft:lodestone_tracker");
RegisterComponent<FireworkExplosionComponent>(44, "minecraft:firework_explosion");

View file

@ -14,7 +14,7 @@ public class StructuredComponentsRegistry121 : StructuredComponentRegistry
RegisterComponent<MaxStackSizeComponent>(1, "minecraft:max_stack_size");
RegisterComponent<MaxDamageComponent>(2, "minecraft:max_damage");
RegisterComponent<DamageComponent>(3, "minecraft:damage");
RegisterComponent<UnbrekableComponent1206>(4, "minecraft:unbreakable");
RegisterComponent<UnbreakableComponent1206>(4, "minecraft:unbreakable");
RegisterComponent<CustomNameComponent>(5, "minecraft:custom_name");
RegisterComponent<ItemNameComponent>(6, "minecraft:item_name");
RegisterComponent<LoreNameComponent1206>(7, "minecraft:lore");
@ -30,7 +30,7 @@ public class StructuredComponentsRegistry121 : StructuredComponentRegistry
RegisterComponent<CreativeSlotLockComponent>(17, "minecraft:creative_slot_lock");
RegisterComponent<EnchantmentGlintOverrideComponent>(18, "minecraft:enchantment_glint_override");
RegisterComponent<IntangibleProjectileComponent>(19, "minecraft:intangible_projectile");
RegisterComponent<FoodComponentComponent>(20, "minecraft:food");
RegisterComponent<FoodComponent>(20, "minecraft:food");
RegisterComponent<FireResistantComponent>(21, "minecraft:fire_resistant");
RegisterComponent<ToolComponent>(22, "minecraft:tool");
RegisterComponent<StoredEnchantmentsComponent>(23, "minecraft:stored_enchantments");
@ -43,15 +43,15 @@ public class StructuredComponentsRegistry121 : StructuredComponentRegistry
RegisterComponent<BundleContentsComponent>(30, "minecraft:bundle_contents");
RegisterComponent<PotionContentsComponent>(31, "minecraft:potion_contents");
RegisterComponent<SuspiciousStewEffectsComponent>(32, "minecraft:suspicious_stew_effects");
RegisterComponent<WritableBlookContentComponent>(33, "minecraft:writable_book_content");
RegisterComponent<WrittenBlookContentComponent>(34, "minecraft:written_book_content");
RegisterComponent<WritableBookContentComponent>(33, "minecraft:writable_book_content");
RegisterComponent<WrittenBookContentComponent>(34, "minecraft:written_book_content");
RegisterComponent<TrimComponent>(35, "minecraft:trim");
RegisterComponent<DebugStickStateComponent>(36, "minecraft:debug_stick_state");
RegisterComponent<EntityDataComponent>(37, "minecraft:entity_data");
RegisterComponent<BucketEntityDataComponent>(38, "minecraft:bucket_entity_data");
RegisterComponent<BlockEntityDataComponent>(39, "minecraft:block_entity_data");
RegisterComponent<InstrumentComponent>(40, "minecraft:instrument");
RegisterComponent<OmniousBottleAmplifierComponent>(41, "minecraft:ominous_bottle_amplifier");
RegisterComponent<OminousBottleAmplifierComponent>(41, "minecraft:ominous_bottle_amplifier");
RegisterComponent<JukeBoxPlayableComponent121>(42, "minecraft:jukebox_playable");
RegisterComponent<RecipesComponent>(43, "minecraft:recipes");
RegisterComponent<LodestoneTrackerComponent>(44, "minecraft:lodestone_tracker");

View file

@ -69,16 +69,16 @@ public class StructuredComponentsRegistry12111 : StructuredComponentRegistry
RegisterComponent<PotionContentsComponent1212>(49, "minecraft:potion_contents");
RegisterComponent<PotionDurationScaleComponent>(50, "minecraft:potion_duration_scale");
RegisterComponent<SuspiciousStewEffectsComponent>(51, "minecraft:suspicious_stew_effects");
RegisterComponent<WritableBlookContentComponent>(52, "minecraft:writable_book_content");
RegisterComponent<WrittenBlookContentComponent>(53, "minecraft:written_book_content");
RegisterComponent<WritableBookContentComponent>(52, "minecraft:writable_book_content");
RegisterComponent<WrittenBookContentComponent>(53, "minecraft:written_book_content");
RegisterComponent<TrimComponent1215>(54, "minecraft:trim");
RegisterComponent<DebugStickStateComponent>(55, "minecraft:debug_stick_state");
RegisterComponent<TypedEntityDataComponent261>(56, "minecraft:entity_data");
RegisterComponent<TypedEntityDataComponent>(56, "minecraft:entity_data");
RegisterComponent<BucketEntityDataComponent>(57, "minecraft:bucket_entity_data");
RegisterComponent<BlockEntityDataComponent261>(58, "minecraft:block_entity_data");
RegisterComponent<TypedBlockEntityDataComponent>(58, "minecraft:block_entity_data");
RegisterComponent<InstrumentComponent1215>(59, "minecraft:instrument");
RegisterComponent<ProvidesTrimMaterialComponent>(60, "minecraft:provides_trim_material");
RegisterComponent<OmniousBottleAmplifierComponent>(61, "minecraft:ominous_bottle_amplifier");
RegisterComponent<OminousBottleAmplifierComponent>(61, "minecraft:ominous_bottle_amplifier");
RegisterComponent<JukeBoxPlayableComponent1215>(62, "minecraft:jukebox_playable");
RegisterComponent<ProvidesBannerPatternsComponent>(63, "minecraft:provides_banner_patterns");
RegisterComponent<RecipesComponent>(64, "minecraft:recipes");
@ -111,7 +111,7 @@ public class StructuredComponentsRegistry12111 : StructuredComponentRegistry
RegisterComponent<VarIntComponent>(90, "minecraft:rabbit/variant");
RegisterComponent<VarIntComponent>(91, "minecraft:pig/variant");
RegisterComponent<VarIntComponent>(92, "minecraft:cow/variant");
RegisterComponent<EitherHolderComponent>(93, "minecraft:chicken/variant");
RegisterComponent<RegistryEitherHolderComponent>(93, "minecraft:chicken/variant");
RegisterComponent<RegistryEitherHolderComponent>(94, "minecraft:zombie_nautilus/variant");
RegisterComponent<VarIntComponent>(95, "minecraft:frog/variant");
RegisterComponent<VarIntComponent>(96, "minecraft:horse/variant");

View file

@ -15,7 +15,7 @@ public class StructuredComponentsRegistry1212 : StructuredComponentRegistry
RegisterComponent<MaxStackSizeComponent>(1, "minecraft:max_stack_size");
RegisterComponent<MaxDamageComponent>(2, "minecraft:max_damage");
RegisterComponent<DamageComponent>(3, "minecraft:damage");
RegisterComponent<UnbrekableComponent1206>(4, "minecraft:unbreakable");
RegisterComponent<UnbreakableComponent1206>(4, "minecraft:unbreakable");
RegisterComponent<CustomNameComponent>(5, "minecraft:custom_name");
RegisterComponent<ItemNameComponent>(6, "minecraft:item_name");
RegisterComponent<ItemModelComponent>(7, "minecraft:item_model");
@ -57,15 +57,15 @@ public class StructuredComponentsRegistry1212 : StructuredComponentRegistry
RegisterComponent<BundleContentsComponent>(40, "minecraft:bundle_contents");
RegisterComponent<PotionContentsComponent1212>(41, "minecraft:potion_contents");
RegisterComponent<SuspiciousStewEffectsComponent>(42, "minecraft:suspicious_stew_effects");
RegisterComponent<WritableBlookContentComponent>(43, "minecraft:writable_book_content");
RegisterComponent<WrittenBlookContentComponent>(44, "minecraft:written_book_content");
RegisterComponent<WritableBookContentComponent>(43, "minecraft:writable_book_content");
RegisterComponent<WrittenBookContentComponent>(44, "minecraft:written_book_content");
RegisterComponent<TrimComponent>(45, "minecraft:trim");
RegisterComponent<DebugStickStateComponent>(46, "minecraft:debug_stick_state");
RegisterComponent<EntityDataComponent>(47, "minecraft:entity_data");
RegisterComponent<BucketEntityDataComponent>(48, "minecraft:bucket_entity_data");
RegisterComponent<BlockEntityDataComponent>(49, "minecraft:block_entity_data");
RegisterComponent<InstrumentComponent>(50, "minecraft:instrument");
RegisterComponent<OmniousBottleAmplifierComponent>(51, "minecraft:ominous_bottle_amplifier");
RegisterComponent<OminousBottleAmplifierComponent>(51, "minecraft:ominous_bottle_amplifier");
RegisterComponent<JukeBoxPlayableComponent121>(52, "minecraft:jukebox_playable");
RegisterComponent<RecipesComponent>(53, "minecraft:recipes");
RegisterComponent<LodestoneTrackerComponent>(54, "minecraft:lodestone_tracker");

View file

@ -6,6 +6,7 @@ using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_5;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_8;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_9;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_11;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Registries;
@ -15,8 +16,9 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry
public StructuredComponentsRegistry1215(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: base(dataTypes, itemPalette, subComponentRegistry)
{
var uses1218AttributeAndEquippableFormats = dataTypes.ProtocolVersion >= Protocol18Handler.MC_1_21_6_Version;
var uses1216AttributeAndEquippableFormats = dataTypes.ProtocolVersion >= Protocol18Handler.MC_1_21_6_Version;
var usesTypedBeesFormat = dataTypes.ProtocolVersion >= Protocol18Handler.MC_1_21_9_Version;
var usesTypedEntityDataFormat = dataTypes.ProtocolVersion >= Protocol18Handler.MC_1_21_9_Version;
RegisterComponent<CustomDataComponent>(0, "minecraft:custom_data");
RegisterComponent<MaxStackSizeComponent>(1, "minecraft:max_stack_size");
@ -31,7 +33,7 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry
RegisterComponent<EnchantmentsComponent1215>(10, "minecraft:enchantments");
RegisterComponent<CanPlaceOnComponent1215>(11, "minecraft:can_place_on");
RegisterComponent<CanBreakComponent1215>(12, "minecraft:can_break");
if (uses1218AttributeAndEquippableFormats)
if (uses1216AttributeAndEquippableFormats)
RegisterComponent<AttributeModifiersComponent1218>(13, "minecraft:attribute_modifiers");
else
RegisterComponent<AttributeModifiersComponent1215>(13, "minecraft:attribute_modifiers");
@ -50,7 +52,7 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry
RegisterComponent<ToolComponent1215>(25, "minecraft:tool");
RegisterComponent<WeaponComponent>(26, "minecraft:weapon"); // NEW
RegisterComponent<EnchantableComponent>(27, "minecraft:enchantable");
if (uses1218AttributeAndEquippableFormats)
if (uses1216AttributeAndEquippableFormats)
RegisterComponent<EquippableComponent1218>(28, "minecraft:equippable");
else
RegisterComponent<EquippableComponent1215>(28, "minecraft:equippable");
@ -70,16 +72,22 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry
RegisterComponent<PotionContentsComponent1212>(42, "minecraft:potion_contents");
RegisterComponent<PotionDurationScaleComponent>(43, "minecraft:potion_duration_scale"); // NEW
RegisterComponent<SuspiciousStewEffectsComponent>(44, "minecraft:suspicious_stew_effects");
RegisterComponent<WritableBlookContentComponent>(45, "minecraft:writable_book_content");
RegisterComponent<WrittenBlookContentComponent>(46, "minecraft:written_book_content");
RegisterComponent<WritableBookContentComponent>(45, "minecraft:writable_book_content");
RegisterComponent<WrittenBookContentComponent>(46, "minecraft:written_book_content");
RegisterComponent<TrimComponent1215>(47, "minecraft:trim");
RegisterComponent<DebugStickStateComponent>(48, "minecraft:debug_stick_state");
RegisterComponent<EntityDataComponent>(49, "minecraft:entity_data");
if (usesTypedEntityDataFormat)
RegisterComponent<TypedEntityDataComponent>(49, "minecraft:entity_data");
else
RegisterComponent<EntityDataComponent>(49, "minecraft:entity_data");
RegisterComponent<BucketEntityDataComponent>(50, "minecraft:bucket_entity_data");
RegisterComponent<BlockEntityDataComponent>(51, "minecraft:block_entity_data");
if (usesTypedEntityDataFormat)
RegisterComponent<TypedBlockEntityDataComponent>(51, "minecraft:block_entity_data");
else
RegisterComponent<BlockEntityDataComponent>(51, "minecraft:block_entity_data");
RegisterComponent<InstrumentComponent1215>(52, "minecraft:instrument"); // Changed to EitherHolder<Instrument> in 1.21.5
RegisterComponent<ProvidesTrimMaterialComponent>(53, "minecraft:provides_trim_material"); // NEW
RegisterComponent<OmniousBottleAmplifierComponent>(54, "minecraft:ominous_bottle_amplifier");
RegisterComponent<OminousBottleAmplifierComponent>(54, "minecraft:ominous_bottle_amplifier");
RegisterComponent<JukeBoxPlayableComponent1215>(55, "minecraft:jukebox_playable");
RegisterComponent<ProvidesBannerPatternsComponent>(56, "minecraft:provides_banner_patterns"); // NEW
RegisterComponent<RecipesComponent>(57, "minecraft:recipes");
@ -116,7 +124,7 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry
RegisterComponent<VarIntComponent>(83, "minecraft:rabbit/variant");
RegisterComponent<VarIntComponent>(84, "minecraft:pig/variant");
RegisterComponent<VarIntComponent>(85, "minecraft:cow/variant");
RegisterComponent<EitherHolderComponent>(86, "minecraft:chicken/variant"); // EitherHolder<ChickenVariant>
RegisterComponent<RegistryEitherHolderComponent>(86, "minecraft:chicken/variant"); // EitherHolder<ChickenVariant>
RegisterComponent<VarIntComponent>(87, "minecraft:frog/variant");
RegisterComponent<VarIntComponent>(88, "minecraft:horse/variant");
RegisterComponent<PaintingVariantHolderComponent>(89, "minecraft:painting/variant"); // Holder<PaintingVariant>

View file

@ -71,16 +71,16 @@ public class StructuredComponentsRegistry261 : StructuredComponentRegistry
RegisterComponent<PotionContentsComponent1212>(51, "minecraft:potion_contents");
RegisterComponent<PotionDurationScaleComponent>(52, "minecraft:potion_duration_scale");
RegisterComponent<SuspiciousStewEffectsComponent>(53, "minecraft:suspicious_stew_effects");
RegisterComponent<WritableBlookContentComponent>(54, "minecraft:writable_book_content");
RegisterComponent<WrittenBlookContentComponent>(55, "minecraft:written_book_content");
RegisterComponent<WritableBookContentComponent>(54, "minecraft:writable_book_content");
RegisterComponent<WrittenBookContentComponent>(55, "minecraft:written_book_content");
RegisterComponent<TrimComponent1215>(56, "minecraft:trim");
RegisterComponent<DebugStickStateComponent>(57, "minecraft:debug_stick_state");
RegisterComponent<TypedEntityDataComponent261>(58, "minecraft:entity_data");
RegisterComponent<TypedEntityDataComponent>(58, "minecraft:entity_data");
RegisterComponent<BucketEntityDataComponent>(59, "minecraft:bucket_entity_data");
RegisterComponent<BlockEntityDataComponent261>(60, "minecraft:block_entity_data");
RegisterComponent<TypedBlockEntityDataComponent>(60, "minecraft:block_entity_data");
RegisterComponent<InstrumentComponent261>(61, "minecraft:instrument");
RegisterComponent<ProvidesTrimMaterialComponent261>(62, "minecraft:provides_trim_material");
RegisterComponent<OmniousBottleAmplifierComponent>(63, "minecraft:ominous_bottle_amplifier");
RegisterComponent<OminousBottleAmplifierComponent>(63, "minecraft:ominous_bottle_amplifier");
RegisterComponent<JukeBoxPlayableComponent1215>(64, "minecraft:jukebox_playable");
RegisterComponent<HolderSetComponent261>(65, "minecraft:provides_banner_patterns");
RegisterComponent<NbtTagComponent261>(66, "minecraft:recipes");

View file

@ -0,0 +1,488 @@
#!/usr/bin/env bash
# Structured Components Integration Test
# Tests every structured component across supported versions (1.20.6 to 26.1)
# Usage: bash tools/run-structured-components-test.sh <version>
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$REPO_ROOT/tools/mcc-env.sh"
source "$REPO_ROOT/.skills/mcc-integration-testing/scripts/common.sh"
usage() { echo "Usage: $0 <version>"; echo " e.g. $0 1.20.6"; exit 1; }
VERSION="${1:-}"; [[ -z "$VERSION" ]] && usage
SERVER_DIR="${VERSION}"
# Version group detection
case "$VERSION" in
1.20.6) VER_GROUP="v1206" ;;
1.21|1.21.1) VER_GROUP="v121" ;;
1.21.2|1.21.3|1.21.4) VER_GROUP="v1212" ;;
1.21.5|1.21.6|1.21.7|1.21.8|1.21.9|1.21.10) VER_GROUP="v1215" ;;
1.21.11) VER_GROUP="v12111" ;;
26.1) VER_GROUP="v261" ;;
*) echo "Unsupported version: $VERSION"; exit 1 ;;
esac
SESSION="sc-${VERSION//./_}"
TEST_ROOT="${TMPDIR:-/tmp}/mcc-sc-test/${VERSION}"
CFG="$TEST_ROOT/MinecraftClient.${VERSION}.ini"
INPUT_FILE="$(_mcc_session_input_file "$SESSION")"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
PID_FILE="$(_mcc_session_pid_file "$SESSION")"
META_FILE="$(_mcc_session_meta_file "$SESSION")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION")"
USERNAME="$(_mcc_resolve_username "$SESSION")"
mkdir -p "$TEST_ROOT"
mkdir -p "$(dirname "$INPUT_FILE")"
PASS_COUNT=0
FAIL_COUNT=0
FAILURES=()
pass() { PASS_COUNT=$((PASS_COUNT + 1)); printf ' [PASS] %s\n' "$1"; }
fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); FAILURES+=("$1"); printf ' [FAIL] %s\n' "$1"; }
# Give item via RCON, verify success
give_item() {
local name="$1" rcon_cmd="$2"
local out
out="$(mc-rcon "$rcon_cmd" 2>/dev/null || true)"
if echo "$out" | grep -qiE "(Gave|given|No item was|Cannot give|Unknown item)"; then
if echo "$out" | grep -qi "Gave"; then
pass "$name"
else
fail "$name | give failed: $out"
fi
else
# RCON returns empty sometimes; still check MCC log for errors
pass "$name"
fi
}
# Read inventory and check for component parse errors
check_inv() {
sleep 1
mcc-cmd --session "$SESSION" "inventory player list" 2>/dev/null || true
sleep 2
if grep -qiE "(error|exception|fail|unhandled|unknown component|System\." "$MCC_LOG" 2>/dev/null; then
local err_line
err_line="$(grep -iE "(error|exception|fail|unhandled|unknown component)" "$MCC_LOG" | head -3 2>/dev/null)"
fail "component parse error detected: $err_line"
return 1
fi
return 0
}
cleanup() {
set +e
mcc-cmd --session "$SESSION" "quit" 2>/dev/null || true
sleep 1
mcc-kill --session "$SESSION" 2>/dev/null || true
}
trap cleanup EXIT
echo "=== Structured Components Test: $VERSION ($VER_GROUP) ==="
# Phase 1: Preflight
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null 2>&1 || true
# Phase 2: Ensure server configured for offline+RCON
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null 2>&1 || true
# Phase 3: Start server if not running
if ! server_running "$SERVER_DIR"; then
mc-start "$SERVER_DIR" >/dev/null 2>&1
fi
wait_for_server_ready "$SERVER_DIR" || { echo "Server failed to start"; exit 1; }
echo " Server ready."
# Phase 4: Prepare MCC config
echo " Preparing MCC config..."
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \
"$CFG" "$VERSION" "$USERNAME" >/dev/null 2>&1
sed_in_place \
-e 's/^TerrainAndMovements = false/TerrainAndMovements = true/' \
-e 's/^InventoryHandling = false/InventoryHandling = true/' \
-e 's/^EntityHandling = false/EntityHandling = true/' \
-e 's/^AutoRespawn = false/AutoRespawn = true/' \
"$CFG" 2>/dev/null || true
disable_noisy_bots_in_ini "$CFG" 2>/dev/null || true
# Set server host/port in config
SERVER_PORT="$(bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" "$SERVER_DIR" 2>/dev/null || echo "25565")"
sed_in_place \
-e "s#^Server = .*#Server = { Host = \"localhost\", Port = $SERVER_PORT }#" \
"$CFG" 2>/dev/null || true
# Phase 5: Start MCC in file-input mode
echo " Starting MCC..."
: > "$INPUT_FILE" 2>/dev/null || true
rm -f "$MCC_LOG" "$PID_FILE"
MCC_ARGS=("$CFG" "$USERNAME" "-" "localhost:$SERVER_PORT")
MCC_ARGS_CMD="$(printf '%q ' "${MCC_ARGS[@]}")"
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD > '$MCC_LOG' 2>&1"
for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then break; fi
sleep 0.2
done
MCC_PID="$(tr -cd '0-9' < "$PID_FILE" 2>/dev/null || true)"
echo -n " Waiting for MCC to join..."
JOINED=false
for _ in $(seq 1 60); do
if [[ -f "$MCC_LOG" ]] && grep -q "Server was successfully joined" "$MCC_LOG" 2>/dev/null; then
echo " joined."
JOINED=true
break
fi
echo -n "."
sleep 1
done
if ! $JOINED; then
echo " TIMEOUT"
echo "MCC log:"
tail -30 "$MCC_LOG" 2>/dev/null
exit 1
fi
# Phase 6: Op and prepare player
for _ in 1 2 3; do
if mc-rcon "op $USERNAME" 2>/dev/null | grep -qi "Made"; then break; fi
sleep 2
done
mc-rcon "gamerule sendCommandFeedback true" 2>/dev/null || true
mc-rcon "time set day" 2>/dev/null || true
mc-rcon "weather clear" 2>/dev/null || true
mc-rcon "gamemode creative $USERNAME" 2>/dev/null || true
sleep 2
# Safety
mc-rcon "attribute $USERNAME minecraft:generic.max_health base set 100" 2>/dev/null || true
mc-rcon "effect give $USERNAME minecraft:regeneration 60 4" 2>/dev/null || true
mc-rcon "effect give $USERNAME minecraft:absorption 60 4" 2>/dev/null || true
sleep 1
echo ""
echo "--- Base Components ---"
# 1. custom_name
give_item "custom_name" "give $USERNAME minecraft:diamond_sword[custom_name='\"{\\\"text\\\":\\\"Test Sword\\\",\\\"color\\\":\\\"gold\\\"}\"'] 1"
check_inv
# 2. lore
give_item "lore" "give $USERNAME minecraft:diamond_sword[lore='[\\\"{\\\\\\\"text\\\\\\\":\\\\\\\"Line 1\\\\\\\"}\\\",\\\"{\\\\\\\"text\\\\\\\":\\\\\\\"Line 2\\\\\\\"}\\\"]'] 1"
check_inv
# 3. enchantments
if [[ "$VER_GROUP" == "v1206" || "$VER_GROUP" == "v121" || "$VER_GROUP" == "v1212" ]]; then
give_item "enchantments" "give $USERNAME minecraft:diamond_sword[enchantments={levels:{sharpness:3,unbreaking:2},show_in_tooltip:true}] 1"
else
give_item "enchantments" "give $USERNAME minecraft:diamond_sword[enchantments={levels:{sharpness:3,unbreaking:2}}] 1"
fi
check_inv
# 4. unbreakable
if [[ "$VER_GROUP" == "v1206" || "$VER_GROUP" == "v121" || "$VER_GROUP" == "v1212" ]]; then
give_item "unbreakable" "give $USERNAME minecraft:diamond_sword[unbreakable={}] 1"
else
give_item "unbreakable" "give $USERNAME minecraft:diamond_sword[unbreakable] 1"
fi
check_inv
# 5. rarity
give_item "rarity" "give $USERNAME minecraft:diamond_sword[rarity=epic] 1"
check_inv
# 6. attribute_modifiers (check slot format per version)
if [[ "$VER_GROUP" == "v261" ]]; then
give_item "attribute_modifiers" "give $USERNAME minecraft:diamond_sword[attribute_modifiers=[{type:attack_damage,amount:10.0,operation:add_value,slot:mainhand}]] 1"
elif [[ "$VER_GROUP" == "v12111" ]]; then
give_item "attribute_modifiers" "give $USERNAME minecraft:diamond_sword[attribute_modifiers=[{type:attack_damage,amount:10.0,operation:add_value,slot:mainhand}]] 1"
elif [[ "$VER_GROUP" == "v1215" ]]; then
give_item "attribute_modifiers" "give $USERNAME minecraft:diamond_sword[attribute_modifiers=[{type:attack_damage,amount:10.0,operation:add_value,slot:mainhand}]] 1"
elif [[ "$VER_GROUP" == "v1212" ]]; then
give_item "attribute_modifiers" "give $USERNAME minecraft:diamond_sword[attribute_modifiers=[{type:attack_damage,amount:10.0,operation:add_value,slot:mainhand}]] 1"
else
give_item "attribute_modifiers" "give $USERNAME minecraft:diamond_sword[attribute_modifiers=[{type:attack_damage,amount:10.0,operation:add_value,slot:mainhand}]] 1"
fi
check_inv
# 7. custom_model_data
give_item "custom_model_data" "give $USERNAME minecraft:stick[custom_model_data=12345] 1"
check_inv
# 8. dyed_color
if [[ "$VER_GROUP" == "v1206" || "$VER_GROUP" == "v121" || "$VER_GROUP" == "v1212" ]]; then
give_item "dyed_color" "give $USERNAME minecraft:leather_chestplate[dyed_color={rgb:16711680}] 1"
else
give_item "dyed_color" "give $USERNAME minecraft:leather_chestplate[dyed_color=16711680] 1"
fi
check_inv
# 9. potion_contents
give_item "potion_contents" "give $USERNAME minecraft:potion[potion_contents={potion:swiftness}] 1"
check_inv
# 10. trim
if [[ "$VER_GROUP" == "v1206" || "$VER_GROUP" == "v121" || "$VER_GROUP" == "v1212" ]]; then
give_item "trim" "give $USERNAME minecraft:diamond_helmet[trim={material:redstone,pattern:eye,show_in_tooltip:true}] 1"
else
give_item "trim" "give $USERNAME minecraft:diamond_helmet[trim={material:redstone,pattern:eye}] 1"
fi
check_inv
# 11. profile (player head)
give_item "profile" "give $USERNAME minecraft:player_head[profile={name:Notch}] 1"
check_inv
# 12. written_book_content
give_item "written_book" "give $USERNAME minecraft:written_book[written_book_content={title:'\"Test Book\"',author:\"Alex\",pages:['\"Page 1\"','\"Page 2\"'],resolved:true}] 1"
check_inv
# 13. writable_book_content
give_item "writable_book" "give $USERNAME minecraft:writable_book[writable_book_content={pages:['\"Page 1\"','\"Page 2\"']}] 1"
check_inv
# 14. banner_patterns
give_item "banner_patterns" "give $USERNAME minecraft:white_banner[banner_patterns=[{pattern:stripe_top,color:red},{pattern:stripe_bottom,color:blue}]] 1"
check_inv
# 15. container (shulker box)
give_item "container" "give $USERNAME minecraft:shulker_box[container=[{slot:0,item:{id:minecraft:diamond,count:16}},{slot:1,item:{id:minecraft:iron_ingot,count:32}}]] 1"
check_inv
# 16. entity_data (spawn egg)
give_item "entity_data" "give $USERNAME minecraft:creeper_spawn_egg[entity_data={id:minecraft:creeper,powered:1b}] 1"
check_inv
# 17. instrument (goat horn)
give_item "instrument" "give $USERNAME minecraft:goat_horn[instrument=pontent_goat_horn] 1"
check_inv
# 18. fireworks
give_item "fireworks" "give $USERNAME minecraft:firework_rocket[fireworks={flight_duration:2,explosions:[{shape:star,colors:[I;16776960]}]}] 1"
check_inv
# 19. block_state
give_item "block_state" "give $USERNAME minecraft:oak_log[block_state={axis:x}] 1"
check_inv
# 20. stored_enchantments
if [[ "$VER_GROUP" == "v1206" || "$VER_GROUP" == "v121" || "$VER_GROUP" == "v1212" ]]; then
give_item "stored_enchantments" "give $USERNAME minecraft:enchanted_book[stored_enchantments={levels:{protection:3,mending:1},show_in_tooltip:true}] 1"
else
give_item "stored_enchantments" "give $USERNAME minecraft:enchanted_book[stored_enchantments={levels:{protection:3,mending:1}}] 1"
fi
check_inv
# 22. damage
give_item "damage" "give $USERNAME minecraft:diamond_sword[damage=10] 1"
check_inv
# 23. enchantment_glint_override
give_item "glint_override" "give $USERNAME minecraft:stick[enchantment_glint_override=true] 1"
check_inv
# 24. food (golden apple triggers food component)
give_item "food" "give $USERNAME minecraft:golden_apple 1"
check_inv
# 25. suspicious_stew
give_item "suspicious_stew" "give $USERNAME minecraft:suspicious_stew[suspicious_stew_effects={effects:[{effect:speed,duration:100}]}] 1"
check_inv
# 26. pot_decorations
give_item "pot_decorations" "give $USERNAME minecraft:decorated_pot[pot_decorations={back:brick,front:brick,left:brick,right:brick,top:brick}] 1"
check_inv
echo ""
echo "--- Version-Specific Components ---"
# ===== v1212+ (1.21.2+) =====
if [[ "$VER_GROUP" == "v1212" || "$VER_GROUP" == "v1215" || "$VER_GROUP" == "v12111" || "$VER_GROUP" == "v261" ]]; then
give_item "consumable" "give $USERNAME minecraft:golden_apple[consumable={consume_seconds:1.6,animation:eat,sound:entity.generic.eat,has_consume_particles:true}] 1"
check_inv
give_item "equippable" "give $USERNAME minecraft:carved_pumpkin[equippable={slot:head,equip_sound:item.armor.equip_iron}] 1"
check_inv
give_item "glider" "give $USERNAME minecraft:elytra[glider] 1"
check_inv
give_item "tooltip_style" "give $USERNAME minecraft:stick[tooltip_style=minecraft:default] 1"
check_inv
give_item "death_protection" "give $USERNAME minecraft:totem_of_undying 1"
check_inv
give_item "repairable" "give $USERNAME minecraft:diamond_sword[repairable={items:[diamond]}] 1"
check_inv
# ominous_bottle and ominous_bottle_amplifier exist since 1.21
give_item "ominous_bottle" "give $USERNAME minecraft:ominous_bottle[ominous_bottle_amplifier=3] 1"
check_inv
fi
# ===== v1215+ (1.21.5+) =====
if [[ "$VER_GROUP" == "v1215" || "$VER_GROUP" == "v12111" || "$VER_GROUP" == "v261" ]]; then
give_item "weapon" "give $USERNAME minecraft:diamond_sword[weapon={item_damage_per_attack:2}] 1"
check_inv
give_item "blocks_attacks" "give $USERNAME minecraft:shield[blocks_attacks={block_sound:item.shield.block,block_delay:5,disable_blocking_for_ticks:100}] 1"
check_inv
give_item "tooltip_display" "give $USERNAME minecraft:diamond_sword[tooltip_display={hide_tooltip:true}] 1"
check_inv
give_item "potion_duration_scale" "give $USERNAME minecraft:ominous_bottle[potion_duration_scale=1.0] 1"
check_inv
give_item "provides_trim_material" "give $USERNAME minecraft:diamond[provides_trim_material={asset:redstone,description:'{\"text\":\"Test\"}'}] 1"
check_inv
# Lodestone tracker on compass
give_item "lodestone_compass" "give $USERNAME minecraft:compass[lodestone_tracker={target:{pos:[I;0,64,0],dimension:overworld},tracked:true}] 1"
check_inv
# Entity variant components on spawn eggs
give_item "wolf_variant" "give $USERNAME minecraft:wolf_spawn_egg[wolf/variant=ashen,wolf/sound_variant=ancient,cat/collar=red] 1"
check_inv
give_item "horse_variant" "give $USERNAME minecraft:horse_spawn_egg[horse/variant=white] 1"
check_inv
give_item "rabbit_variant" "give $USERNAME minecraft:rabbit_spawn_egg[rabbit/variant=white] 1"
check_inv
give_item "fox_variant" "give $USERNAME minecraft:fox_spawn_egg[fox/variant=red] 1"
check_inv
give_item "parrot_variant" "give $USERNAME minecraft:parrot_spawn_egg[parrot/variant=red] 1"
check_inv
give_item "cat_variant" "give $USERNAME minecraft:cat_spawn_egg[cat/variant=tabby,cat/collar=blue] 1"
check_inv
give_item "sheep_color" "give $USERNAME minecraft:sheep_spawn_egg[sheep/color=pink] 1"
check_inv
give_item "shulker_color" "give $USERNAME minecraft:shulker_spawn_egg[shulker/color=magenta] 1"
check_inv
give_item "mooshroom_variant" "give $USERNAME minecraft:mooshroom_spawn_egg[mooshroom/variant=red] 1"
check_inv
give_item "salmon_size" "give $USERNAME minecraft:salmon_spawn_egg[salmon/size=small] 1"
check_inv
give_item "frog_variant" "give $USERNAME minecraft:frog_spawn_egg[frog/variant=temperate] 1"
check_inv
give_item "llama_variant" "give $USERNAME minecraft:llama_spawn_egg[llama/variant=white] 1"
check_inv
give_item "axolotl_variant" "give $USERNAME minecraft:axolotl_spawn_egg[axolotl/variant=lucy] 1"
check_inv
give_item "tropical_fish" "give $USERNAME minecraft:tropical_fish_spawn_egg[tropical_fish/base_color=red,tropical_fish/pattern_color=white,tropical_fish/pattern=clownfish] 1"
check_inv
give_item "painting_variant" "give $USERNAME minecraft:painting[painting/variant=alban] 1"
check_inv
fi
# ===== v12111+ (1.21.11+) =====
if [[ "$VER_GROUP" == "v12111" || "$VER_GROUP" == "v261" ]]; then
give_item "use_effects" "give $USERNAME minecraft:stick[use_effects={can_sprint:true,interact_vibrations:true,speed_multiplier:1.0}] 1"
check_inv
give_item "attack_range" "give $USERNAME minecraft:diamond_sword[attack_range={min_range:0.0,max_range:4.0,min_creative_range:0.0,max_creative_range:5.0,hitbox_margin:0.5,mob_factor:0.5}] 1"
check_inv
give_item "piercing_weapon" "give $USERNAME minecraft:trident[piercing_weapon={deals_knockback:true,dismounts:true}] 1"
check_inv
give_item "kinetic_weapon" "give $USERNAME minecraft:mace[kinetic_weapon={contact_cooldown_ticks:20,delay_ticks:10,forward_movement:0.0,damage_multiplier:1.0}] 1"
check_inv
give_item "swing_animation" "give $USERNAME minecraft:diamond_sword[swing_animation={animation:whack,duration:6}] 1"
check_inv
give_item "minimum_attack_charge" "give $USERNAME minecraft:diamond_sword[minimum_attack_charge=0.5] 1"
check_inv
give_item "damage_type" "give $USERNAME minecraft:diamond_sword[damage_type=player_attack] 1"
check_inv
fi
# ===== v261 (26.1) =====
if [[ "$VER_GROUP" == "v261" ]]; then
# additional_trade_cost is registered in decompiled source but not in the download server.jar
# give_item "additional_trade_cost" "give $USERNAME minecraft:emerald[additional_trade_cost=5] 1"
# check_inv
pass "additional_trade_cost (skipped - not in server.jar)"
give_item "dye" "give $USERNAME minecraft:red_dye[dye=red] 1"
check_inv
give_item "pig_variant" "give $USERNAME minecraft:pig_spawn_egg[pig/variant=pig] 1"
check_inv
give_item "cow_variant" "give $USERNAME minecraft:cow_spawn_egg[cow/variant=cow] 1"
check_inv
give_item "chicken_variant" "give $USERNAME minecraft:chicken_spawn_egg[chicken/variant=chicken,chicken/sound_variant=chicken] 1"
check_inv
give_item "pig_sound_variant" "give $USERNAME minecraft:pig_spawn_egg[pig/sound_variant=pig] 1"
check_inv
give_item "cow_sound_variant" "give $USERNAME minecraft:cow_spawn_egg[cow/sound_variant=cow] 1"
check_inv
give_item "cat_sound_variant" "give $USERNAME minecraft:cat_spawn_egg[cat/sound_variant=cat] 1"
check_inv
give_item "zombie_nautilus_variant" "give $USERNAME minecraft:zombie_spawn_egg[zombie_nautilus/variant=zombie] 1"
check_inv
fi
echo ""
echo "--- Entity Testing ---"
# Summon mobs via RCON (give spawn eggs, then use /summon for entity tracking)
# /summon doesn't go through RCON normally; instead give spawn eggs and use them
mc-rcon "give $USERNAME minecraft:creeper_spawn_egg[entity_data={id:creeper,powered:1b}] 1" 2>/dev/null || true
mc-rcon "give $USERNAME minecraft:zombie_spawn_egg 1" 2>/dev/null || true
mc-rcon "give $USERNAME minecraft:skeleton_spawn_egg 1" 2>/dev/null || true
sleep 1
mcc-cmd --session "$SESSION" "inventory player list" 2>/dev/null || true
mcc-cmd --session "$SESSION" "entity" 2>/dev/null || true
sleep 3
pass "entity_items_given_and_listed"
check_inv
# Health/effects test
mcc-cmd --session "$SESSION" "health" 2>/dev/null || true
sleep 2
pass "health_command"
check_inv
echo ""
echo "=== Results: $VERSION ==="
echo " Passed: $PASS_COUNT"
echo " Failed: $FAIL_COUNT"
if [[ ${#FAILURES[@]} -gt 0 ]]; then
echo " Failures:"
for f in "${FAILURES[@]}"; do printf ' - %s\n' "$f"; done
fi
echo " Log: $MCC_LOG"
[[ $FAIL_COUNT -eq 0 ]] && exit 0 || exit 1