bugfix: Fixed and tested all structured components and issues with Empty packets sent by some servers

bugfix: Fixed and tested all structured components and issues with Empty packets sent by some servers
This commit is contained in:
Anon 2026-06-11 22:05:35 +02:00 committed by GitHub
commit 981a996344
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 818 additions and 262 deletions

View file

@ -115,7 +115,31 @@ Use this for TPS, movement-cadence, or packet-cadence work:
Run them against a real server with a temp config and summarize counts from the captured logs.
### 4. Full inventory regression sweep
### 4. Structured components test
Use this after touching any `StructuredComponents` code (registries, component
parsers, subcomponents, codec helpers) to prove every component type in a
version parses on the wire without error:
```bash
bash tools/run-structured-components-test.sh 1.21.11
```
Run a single version (fast, ~2 min) or a matrix:
```bash
for v in 1.20.6 1.21 1.21.2 1.21.5 1.21.11 26.1; do
bash tools/run-structured-components-test.sh "$v"
done
```
The script gives items with every registered component via RCON `/give`, reads
them back with `inventory player list`, and asserts no parse errors in the MCC
log. Version-gated components (v1212+, v1215+, v12111+, v261) are tested only
on the versions that support them. See `SC_Integration_Test_Report.md` for a
reference run across all 6 version groups.
### 5. Full inventory regression sweep
Use this when touching inventory snapshots, player/container slot sync, creative inventory, item-slot serialization, packet palettes, game-mode updates, or block-use paths that open containers:
@ -198,6 +222,8 @@ Optionally override the login name with the fourth argument to the config helper
- ordered creative-mode E2E regression scenario
- `tools/run-inventory-full-sweep.sh`
- full inventory command/API sweep across one or more versions
- `tools/run-structured-components-test.sh`
- exercises every structured component via RCON `/give` across versions 1.20.6-26.1
## Evidence Discipline

View file

@ -123,3 +123,4 @@ Read `docs/guide/ai-assisted-development.md` before starting development work on
- Don't trust older docs over current code for supported versions or feature gates. When AGENTS.md, skills, and older docs disagree, prefer current code and current tool behavior, then update the stale source.
- Don't hardcode user-facing strings (messages, labels, help text) directly in source code; always use `Translations.*` resources so the text can be localized.
- Never use "—" ("em dash"), unless specifically being instructed to do so!
- Never generate MCC config files (`.ini`) in the repo root. When `dotnet run --project MinecraftClient -- --help` is used to generate a config template, it writes `MinecraftClient.ini` to the current directory. Always run this command from a system temp directory (e.g. `mktemp -d`) or use `prepare_offline_mcc_config.sh` which already handles output routing.

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.Debug("Empty packet after decompress: size={0}, sizeUncompressed={1}, protocol={2}, state={3}, rawBytes=[{4}]", size, sizeUncompressed, protocolVersion, currentState, rawHex);
return new(-1, packetData);
}
var packetId = dataTypes.ReadNextVarInt(packetData); // Packet ID
LogIncomingPacket(packetId, packetData.Count, size, compressed, sizeUncompressed);
if (handler.GetNetworkPacketCaptureEnabled())
@ -4147,12 +4158,21 @@ namespace MinecraftClient.Protocol.Handlers
log.PacketDebug(string.Format(Translations.debug_packet_state_change, previousState, newState));
}
private static bool IsPacketExcluded(string packetType)
{
var exclusions = Settings.Config.Logging.PacketDebugExclusions;
return exclusions.Count > 0 && exclusions.Contains(packetType, StringComparer.OrdinalIgnoreCase);
}
private void LogIncomingPacket(int packetId, int payloadLength, int frameLength, bool compressed, int uncompressedLength)
{
if (!log.DebugEnabled)
return;
var packetType = ResolveIncomingPacketType(packetId);
if (IsPacketExcluded(packetType))
return;
var compressionInfo = compression_treshold < 0
? Translations.debug_packet_compression_disabled
: compressed
@ -4173,10 +4193,14 @@ namespace MinecraftClient.Protocol.Handlers
if (!log.DebugEnabled)
return;
var resolvedType = packetType ?? ResolveOutgoingPacketType(packetId);
if (IsPacketExcluded(resolvedType))
return;
log.PacketDebug(string.Format(Translations.debug_packet_outgoing,
currentState,
packetId,
packetType ?? ResolveOutgoingPacketType(packetId),
resolvedType,
payloadLength,
compression_treshold));
}

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

@ -1541,6 +1541,15 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to Packet types to exclude from packet debug logs, e.g. [&quot;KeepAlive&quot;, &quot;Ping&quot;]..
/// </summary>
internal static string Logging_PacketDebugExclusions {
get {
return ResourceManager.GetString("Logging.PacketDebugExclusions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show error messages..
/// </summary>

View file

@ -629,6 +629,9 @@ Want to upgrade to a newer version? See https://github.com/MCCTeam/Minecraft-Con
<data name="Logging.PacketDebugMessages" xml:space="preserve">
<value>Show low-level packet debug logs.</value>
</data>
<data name="Logging.PacketDebugExclusions" xml:space="preserve">
<value>Packet types to exclude from packet debug logs, e.g. ["KeepAlive", "Ping"].</value>
</data>
<data name="Logging.ErrorMessages" xml:space="preserve">
<value>Show error messages.</value>
</data>

View file

@ -1080,6 +1080,9 @@ namespace MinecraftClient
[TomlInlineComment("$Logging.PacketDebugMessages$")]
public bool PacketDebugMessages = false;
[TomlInlineComment("$Logging.PacketDebugExclusions$")]
public List<string> PacketDebugExclusions = new();
[TomlInlineComment("$Logging.ChatMessages$")]
public bool ChatMessages = true;

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