diff --git a/.skills/mcc-integration-testing/SKILL.md b/.skills/mcc-integration-testing/SKILL.md index e7c0831d..d6685ff6 100644 --- a/.skills/mcc-integration-testing/SKILL.md +++ b/.skills/mcc-integration-testing/SKILL.md @@ -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 diff --git a/AGENTS.md b/AGENTS.md index b82cc5bf..ed3590b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/MinecraftClient/Inventory/BookContent.cs b/MinecraftClient/Inventory/BookContent.cs index 239e3813..262e4680 100644 --- a/MinecraftClient/Inventory/BookContent.cs +++ b/MinecraftClient/Inventory/BookContent.cs @@ -100,7 +100,7 @@ public static class BookContentHelper { if (item.Components is not null) { - var component = item.Components.OfType().FirstOrDefault(); + var component = item.Components.OfType().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().FirstOrDefault(); + var component = item.Components.OfType().FirstOrDefault(); if (component is not null) { content = new BookContent( diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 180b174a..c9b093de 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -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> ReadNextPacket() { var size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size - Queue packetData = new(socketWrapper.ReadDataRAW(size)); //Packet contents + var rawBytes = socketWrapper.ReadDataRAW(size); + Queue 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)); } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponent.cs similarity index 93% rename from MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs rename to MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponent.cs index eb5ed17d..f8679473 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponentComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/FoodComponent.cs @@ -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; } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/IntangibleProjectileComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/IntangibleProjectileComponent.cs index 2ecfe0f1..b6d24a5e 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/IntangibleProjectileComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/IntangibleProjectileComponent.cs @@ -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? Nbt { get; set; } = new(); - - public override void Parse(Queue data) - { - Nbt = DataTypes.ReadNextNbt(data); - } - - public override Queue Serialize() - { - var data = new List(); - data.AddRange(DataTypes.GetNbt(Nbt)); - return new Queue(data); - } -} \ No newline at end of file +} diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/OmniousBottleAmplifierComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/OminousBottleAmplifierComponent.cs similarity index 91% rename from MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/OmniousBottleAmplifierComponent.cs rename to MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/OminousBottleAmplifierComponent.cs index 1c35b944..ed92d953 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/OmniousBottleAmplifierComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/OminousBottleAmplifierComponent.cs @@ -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; } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/UnbreakableComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/UnbreakableComponent.cs index d7fc891a..b4f354f6 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/UnbreakableComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/UnbreakableComponent.cs @@ -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 data) { - Unbrekable = DataTypes.ReadNextBool(data); + Unbreakable = DataTypes.ReadNextBool(data); } public override Queue Serialize() { var data = new List(); - data.AddRange(DataTypes.GetBool(Unbrekable)); + data.AddRange(DataTypes.GetBool(Unbreakable)); return new Queue(data); } } \ No newline at end of file diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WritableBlookContentComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WritableBookContentComponent.cs similarity index 82% rename from MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WritableBlookContentComponent.cs rename to MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WritableBookContentComponent.cs index 9a2b9a30..59dc0409 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WritableBlookContentComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WritableBookContentComponent.cs @@ -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 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)); } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WrittenBlookContentComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WrittenBookContentComponent.cs similarity index 94% rename from MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WrittenBlookContentComponent.cs rename to MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WrittenBookContentComponent.cs index 79e58af8..413c94d8 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WrittenBlookContentComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_20_6/WrittenBookContentComponent.cs @@ -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; } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21/JukeBoxPlayableComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21/JukeBoxPlayableComponent.cs deleted file mode 100644 index e1e0a3e1..00000000 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21/JukeBoxPlayableComponent.cs +++ /dev/null @@ -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 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 Serialize() - { - var data = new List(); - - 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(data); - } -} \ No newline at end of file diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/KineticWeaponComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/KineticWeaponComponent.cs index 9bfeb283..4d676f51 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/KineticWeaponComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/KineticWeaponComponent.cs @@ -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 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 data) + private KineticWeaponConditionData? ReadOptionalCondition(Queue 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 data) + private void WriteOptionalCondition(List 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 Serialize() { - return new Queue(); + var data = new List(); + 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(data); } } + +public sealed record KineticWeaponConditionData(int MaxDurationTicks, float MinSpeed, float MinRelativeSpeed); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/PiercingWeaponComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/PiercingWeaponComponent.cs index 904044ef..a6af8bef 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/PiercingWeaponComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_11/PiercingWeaponComponent.cs @@ -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 data) { DealsKnockback = DataTypes.ReadNextBool(data); Dismounts = DataTypes.ReadNextBool(data); - ReadOptionalSoundEventHolder(data); - ReadOptionalSoundEventHolder(data); - } - - private void ReadOptionalSoundEventHolder(Queue 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 Serialize() { - return new Queue(); + var data = new List(); + data.AddRange(DataTypes.GetBool(DealsKnockback)); + data.AddRange(DataTypes.GetBool(Dismounts)); + StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, Sound); + StructuredComponentCodecHelpers.WriteOptionalSoundEventHolder(DataTypes, data, HitSound); + return new Queue(data); } } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_5/BlocksAttacksComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_5/BlocksAttacksComponent.cs index 030133c7..5c497bf4 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_5/BlocksAttacksComponent.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/1_21_5/BlocksAttacksComponent.cs @@ -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 RawDamageReductions { get; set; } = []; + public List 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 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 as ResourceLocation + BypassedBy = DataTypes.ReadNextString(data); // TagKey as ResourceLocation - var hasBlockSound = DataTypes.ReadNextBool(data); - if (hasBlockSound) - ReadSoundEventHolder(data); - - var hasDisableSound = DataTypes.ReadNextBool(data); - if (hasDisableSound) - ReadSoundEventHolder(data); - } - - private void ReadHolderSet(Queue 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 registry ids - } - } - - private void ReadSoundEventHolder(Queue 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 Serialize() { - return new Queue(); + var data = new List(); + 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(data); } } + +public sealed record DamageReductionData(float HorizontalBlockingAngle, HolderSetData? Type, float Base, float Factor); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/26_1/TypedEntityDataComponent261.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/26_1/TypedEntityDataComponent261.cs index 15940d06..d05b147a 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/26_1/TypedEntityDataComponent261.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/26_1/TypedEntityDataComponent261.cs @@ -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? Nbt { get; set; } - - public override void Parse(Queue data) - { - EntityTypeId = DataTypes.ReadNextVarInt(data); - Nbt = DataTypes.ReadNextNbt(data); - } - - public override Queue Serialize() - { - var data = new List(); - data.AddRange(DataTypes.GetVarInt(EntityTypeId)); - data.AddRange(DataTypes.GetNbt(Nbt)); - return new Queue(data); - } -} + : TypedEntityDataComponent(dataTypes, itemPalette, subComponentRegistry) +{ } public class BlockEntityDataComponent261(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) - : TypedEntityDataComponent261(dataTypes, itemPalette, subComponentRegistry) + : TypedBlockEntityDataComponent(dataTypes, itemPalette, subComponentRegistry) { } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/StructuredComponentCodecHelpers.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/StructuredComponentCodecHelpers.cs new file mode 100644 index 00000000..dc3fc218 --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/StructuredComponentCodecHelpers.cs @@ -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 data) + { + var sizeOrTag = dataTypes.ReadNextVarInt(data); + if (sizeOrTag == 0) + return new HolderSetData(dataTypes.ReadNextString(data), []); + + var count = sizeOrTag - 1; + var holderIds = new List(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 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 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 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 data) + { + return dataTypes.ReadNextBool(data) ? ReadSoundEventHolder(dataTypes, data) : null; + } + + public static void WriteOptionalSoundEventHolder(DataTypes dataTypes, List 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 HolderIds); + +public sealed record SoundEventHolderData(int HolderId, string? SoundLocation, bool HasFixedRange, float FixedRange); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/TypedEntityDataComponent.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/TypedEntityDataComponent.cs new file mode 100644 index 00000000..ab7ae279 --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Components/TypedEntityDataComponent.cs @@ -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? Nbt { get; set; } + + public override void Parse(Queue data) + { + EntityTypeId = DataTypes.ReadNextVarInt(data); + Nbt = DataTypes.ReadNextNbt(data); + } + + public override Queue Serialize() + { + var data = new List(); + data.AddRange(DataTypes.GetVarInt(EntityTypeId)); + data.AddRange(DataTypes.GetNbt(Nbt)); + return new Queue(data); + } +} + +public class TypedBlockEntityDataComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry) + : TypedEntityDataComponent(dataTypes, itemPalette, subComponentRegistry) +{ } diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1206.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1206.cs index 18ae967c..8f5dee2e 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1206.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1206.cs @@ -13,7 +13,7 @@ public class StructuredComponentsRegistry1206 : StructuredComponentRegistry RegisterComponent(1, "minecraft:max_stack_size"); RegisterComponent(2, "minecraft:max_damage"); RegisterComponent(3, "minecraft:damage"); - RegisterComponent(4, "minecraft:unbreakable"); + RegisterComponent(4, "minecraft:unbreakable"); RegisterComponent(5, "minecraft:custom_name"); RegisterComponent(6, "minecraft:item_name"); RegisterComponent(7, "minecraft:lore"); @@ -29,7 +29,7 @@ public class StructuredComponentsRegistry1206 : StructuredComponentRegistry RegisterComponent(17, "minecraft:creative_slot_lock"); RegisterComponent(18, "minecraft:enchantment_glint_override"); RegisterComponent(19, "minecraft:intangible_projectile"); - RegisterComponent(20, "minecraft:food"); + RegisterComponent(20, "minecraft:food"); RegisterComponent(21, "minecraft:fire_resistant"); RegisterComponent(22, "minecraft:tool"); RegisterComponent(23, "minecraft:stored_enchantments"); @@ -42,15 +42,15 @@ public class StructuredComponentsRegistry1206 : StructuredComponentRegistry RegisterComponent(30, "minecraft:bundle_contents"); RegisterComponent(31, "minecraft:potion_contents"); RegisterComponent(32, "minecraft:suspicious_stew_effects"); - RegisterComponent(33, "minecraft:writable_book_content"); - RegisterComponent(34, "minecraft:written_book_content"); + RegisterComponent(33, "minecraft:writable_book_content"); + RegisterComponent(34, "minecraft:written_book_content"); RegisterComponent(35, "minecraft:trim"); RegisterComponent(36, "minecraft:debug_stick_state"); RegisterComponent(37, "minecraft:entity_data"); RegisterComponent(38, "minecraft:bucket_entity_data"); RegisterComponent(39, "minecraft:block_entity_data"); RegisterComponent(40, "minecraft:instrument"); - RegisterComponent(41, "minecraft:ominous_bottle_amplifier"); + RegisterComponent(41, "minecraft:ominous_bottle_amplifier"); RegisterComponent(42, "minecraft:recipes"); RegisterComponent(43, "minecraft:lodestone_tracker"); RegisterComponent(44, "minecraft:firework_explosion"); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry121.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry121.cs index 7a2376f3..5e22bd78 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry121.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry121.cs @@ -14,7 +14,7 @@ public class StructuredComponentsRegistry121 : StructuredComponentRegistry RegisterComponent(1, "minecraft:max_stack_size"); RegisterComponent(2, "minecraft:max_damage"); RegisterComponent(3, "minecraft:damage"); - RegisterComponent(4, "minecraft:unbreakable"); + RegisterComponent(4, "minecraft:unbreakable"); RegisterComponent(5, "minecraft:custom_name"); RegisterComponent(6, "minecraft:item_name"); RegisterComponent(7, "minecraft:lore"); @@ -30,7 +30,7 @@ public class StructuredComponentsRegistry121 : StructuredComponentRegistry RegisterComponent(17, "minecraft:creative_slot_lock"); RegisterComponent(18, "minecraft:enchantment_glint_override"); RegisterComponent(19, "minecraft:intangible_projectile"); - RegisterComponent(20, "minecraft:food"); + RegisterComponent(20, "minecraft:food"); RegisterComponent(21, "minecraft:fire_resistant"); RegisterComponent(22, "minecraft:tool"); RegisterComponent(23, "minecraft:stored_enchantments"); @@ -43,15 +43,15 @@ public class StructuredComponentsRegistry121 : StructuredComponentRegistry RegisterComponent(30, "minecraft:bundle_contents"); RegisterComponent(31, "minecraft:potion_contents"); RegisterComponent(32, "minecraft:suspicious_stew_effects"); - RegisterComponent(33, "minecraft:writable_book_content"); - RegisterComponent(34, "minecraft:written_book_content"); + RegisterComponent(33, "minecraft:writable_book_content"); + RegisterComponent(34, "minecraft:written_book_content"); RegisterComponent(35, "minecraft:trim"); RegisterComponent(36, "minecraft:debug_stick_state"); RegisterComponent(37, "minecraft:entity_data"); RegisterComponent(38, "minecraft:bucket_entity_data"); RegisterComponent(39, "minecraft:block_entity_data"); RegisterComponent(40, "minecraft:instrument"); - RegisterComponent(41, "minecraft:ominous_bottle_amplifier"); + RegisterComponent(41, "minecraft:ominous_bottle_amplifier"); RegisterComponent(42, "minecraft:jukebox_playable"); RegisterComponent(43, "minecraft:recipes"); RegisterComponent(44, "minecraft:lodestone_tracker"); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry12111.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry12111.cs index de00bbe2..0be0c7d2 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry12111.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry12111.cs @@ -69,16 +69,16 @@ public class StructuredComponentsRegistry12111 : StructuredComponentRegistry RegisterComponent(49, "minecraft:potion_contents"); RegisterComponent(50, "minecraft:potion_duration_scale"); RegisterComponent(51, "minecraft:suspicious_stew_effects"); - RegisterComponent(52, "minecraft:writable_book_content"); - RegisterComponent(53, "minecraft:written_book_content"); + RegisterComponent(52, "minecraft:writable_book_content"); + RegisterComponent(53, "minecraft:written_book_content"); RegisterComponent(54, "minecraft:trim"); RegisterComponent(55, "minecraft:debug_stick_state"); - RegisterComponent(56, "minecraft:entity_data"); + RegisterComponent(56, "minecraft:entity_data"); RegisterComponent(57, "minecraft:bucket_entity_data"); - RegisterComponent(58, "minecraft:block_entity_data"); + RegisterComponent(58, "minecraft:block_entity_data"); RegisterComponent(59, "minecraft:instrument"); RegisterComponent(60, "minecraft:provides_trim_material"); - RegisterComponent(61, "minecraft:ominous_bottle_amplifier"); + RegisterComponent(61, "minecraft:ominous_bottle_amplifier"); RegisterComponent(62, "minecraft:jukebox_playable"); RegisterComponent(63, "minecraft:provides_banner_patterns"); RegisterComponent(64, "minecraft:recipes"); @@ -111,7 +111,7 @@ public class StructuredComponentsRegistry12111 : StructuredComponentRegistry RegisterComponent(90, "minecraft:rabbit/variant"); RegisterComponent(91, "minecraft:pig/variant"); RegisterComponent(92, "minecraft:cow/variant"); - RegisterComponent(93, "minecraft:chicken/variant"); + RegisterComponent(93, "minecraft:chicken/variant"); RegisterComponent(94, "minecraft:zombie_nautilus/variant"); RegisterComponent(95, "minecraft:frog/variant"); RegisterComponent(96, "minecraft:horse/variant"); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1212.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1212.cs index d6f519bb..ffe89d0f 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1212.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1212.cs @@ -15,7 +15,7 @@ public class StructuredComponentsRegistry1212 : StructuredComponentRegistry RegisterComponent(1, "minecraft:max_stack_size"); RegisterComponent(2, "minecraft:max_damage"); RegisterComponent(3, "minecraft:damage"); - RegisterComponent(4, "minecraft:unbreakable"); + RegisterComponent(4, "minecraft:unbreakable"); RegisterComponent(5, "minecraft:custom_name"); RegisterComponent(6, "minecraft:item_name"); RegisterComponent(7, "minecraft:item_model"); @@ -57,15 +57,15 @@ public class StructuredComponentsRegistry1212 : StructuredComponentRegistry RegisterComponent(40, "minecraft:bundle_contents"); RegisterComponent(41, "minecraft:potion_contents"); RegisterComponent(42, "minecraft:suspicious_stew_effects"); - RegisterComponent(43, "minecraft:writable_book_content"); - RegisterComponent(44, "minecraft:written_book_content"); + RegisterComponent(43, "minecraft:writable_book_content"); + RegisterComponent(44, "minecraft:written_book_content"); RegisterComponent(45, "minecraft:trim"); RegisterComponent(46, "minecraft:debug_stick_state"); RegisterComponent(47, "minecraft:entity_data"); RegisterComponent(48, "minecraft:bucket_entity_data"); RegisterComponent(49, "minecraft:block_entity_data"); RegisterComponent(50, "minecraft:instrument"); - RegisterComponent(51, "minecraft:ominous_bottle_amplifier"); + RegisterComponent(51, "minecraft:ominous_bottle_amplifier"); RegisterComponent(52, "minecraft:jukebox_playable"); RegisterComponent(53, "minecraft:recipes"); RegisterComponent(54, "minecraft:lodestone_tracker"); diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1215.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1215.cs index cca30281..d78d87b9 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1215.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry1215.cs @@ -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(0, "minecraft:custom_data"); RegisterComponent(1, "minecraft:max_stack_size"); @@ -31,7 +33,7 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry RegisterComponent(10, "minecraft:enchantments"); RegisterComponent(11, "minecraft:can_place_on"); RegisterComponent(12, "minecraft:can_break"); - if (uses1218AttributeAndEquippableFormats) + if (uses1216AttributeAndEquippableFormats) RegisterComponent(13, "minecraft:attribute_modifiers"); else RegisterComponent(13, "minecraft:attribute_modifiers"); @@ -50,7 +52,7 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry RegisterComponent(25, "minecraft:tool"); RegisterComponent(26, "minecraft:weapon"); // NEW RegisterComponent(27, "minecraft:enchantable"); - if (uses1218AttributeAndEquippableFormats) + if (uses1216AttributeAndEquippableFormats) RegisterComponent(28, "minecraft:equippable"); else RegisterComponent(28, "minecraft:equippable"); @@ -70,16 +72,22 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry RegisterComponent(42, "minecraft:potion_contents"); RegisterComponent(43, "minecraft:potion_duration_scale"); // NEW RegisterComponent(44, "minecraft:suspicious_stew_effects"); - RegisterComponent(45, "minecraft:writable_book_content"); - RegisterComponent(46, "minecraft:written_book_content"); + RegisterComponent(45, "minecraft:writable_book_content"); + RegisterComponent(46, "minecraft:written_book_content"); RegisterComponent(47, "minecraft:trim"); RegisterComponent(48, "minecraft:debug_stick_state"); - RegisterComponent(49, "minecraft:entity_data"); + if (usesTypedEntityDataFormat) + RegisterComponent(49, "minecraft:entity_data"); + else + RegisterComponent(49, "minecraft:entity_data"); RegisterComponent(50, "minecraft:bucket_entity_data"); - RegisterComponent(51, "minecraft:block_entity_data"); + if (usesTypedEntityDataFormat) + RegisterComponent(51, "minecraft:block_entity_data"); + else + RegisterComponent(51, "minecraft:block_entity_data"); RegisterComponent(52, "minecraft:instrument"); // Changed to EitherHolder in 1.21.5 RegisterComponent(53, "minecraft:provides_trim_material"); // NEW - RegisterComponent(54, "minecraft:ominous_bottle_amplifier"); + RegisterComponent(54, "minecraft:ominous_bottle_amplifier"); RegisterComponent(55, "minecraft:jukebox_playable"); RegisterComponent(56, "minecraft:provides_banner_patterns"); // NEW RegisterComponent(57, "minecraft:recipes"); @@ -116,7 +124,7 @@ public class StructuredComponentsRegistry1215 : StructuredComponentRegistry RegisterComponent(83, "minecraft:rabbit/variant"); RegisterComponent(84, "minecraft:pig/variant"); RegisterComponent(85, "minecraft:cow/variant"); - RegisterComponent(86, "minecraft:chicken/variant"); // EitherHolder + RegisterComponent(86, "minecraft:chicken/variant"); // EitherHolder RegisterComponent(87, "minecraft:frog/variant"); RegisterComponent(88, "minecraft:horse/variant"); RegisterComponent(89, "minecraft:painting/variant"); // Holder diff --git a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry261.cs b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry261.cs index c4d809fd..c0b4281d 100644 --- a/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry261.cs +++ b/MinecraftClient/Protocol/Handlers/StructuredComponents/Registries/StructuredComponentsRegistry261.cs @@ -71,16 +71,16 @@ public class StructuredComponentsRegistry261 : StructuredComponentRegistry RegisterComponent(51, "minecraft:potion_contents"); RegisterComponent(52, "minecraft:potion_duration_scale"); RegisterComponent(53, "minecraft:suspicious_stew_effects"); - RegisterComponent(54, "minecraft:writable_book_content"); - RegisterComponent(55, "minecraft:written_book_content"); + RegisterComponent(54, "minecraft:writable_book_content"); + RegisterComponent(55, "minecraft:written_book_content"); RegisterComponent(56, "minecraft:trim"); RegisterComponent(57, "minecraft:debug_stick_state"); - RegisterComponent(58, "minecraft:entity_data"); + RegisterComponent(58, "minecraft:entity_data"); RegisterComponent(59, "minecraft:bucket_entity_data"); - RegisterComponent(60, "minecraft:block_entity_data"); + RegisterComponent(60, "minecraft:block_entity_data"); RegisterComponent(61, "minecraft:instrument"); RegisterComponent(62, "minecraft:provides_trim_material"); - RegisterComponent(63, "minecraft:ominous_bottle_amplifier"); + RegisterComponent(63, "minecraft:ominous_bottle_amplifier"); RegisterComponent(64, "minecraft:jukebox_playable"); RegisterComponent(65, "minecraft:provides_banner_patterns"); RegisterComponent(66, "minecraft:recipes"); diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 4a0cc7fd..cdd5d4b1 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1541,6 +1541,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Packet types to exclude from packet debug logs, e.g. ["KeepAlive", "Ping"].. + /// + internal static string Logging_PacketDebugExclusions { + get { + return ResourceManager.GetString("Logging.PacketDebugExclusions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show error messages.. /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 5515ae24..d1ebbcb2 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -629,6 +629,9 @@ Want to upgrade to a newer version? See https://github.com/MCCTeam/Minecraft-Con Show low-level packet debug logs. + + Packet types to exclude from packet debug logs, e.g. ["KeepAlive", "Ping"]. + Show error messages. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 06a3837c..a5ce88af 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1080,6 +1080,9 @@ namespace MinecraftClient [TomlInlineComment("$Logging.PacketDebugMessages$")] public bool PacketDebugMessages = false; + [TomlInlineComment("$Logging.PacketDebugExclusions$")] + public List PacketDebugExclusions = new(); + [TomlInlineComment("$Logging.ChatMessages$")] public bool ChatMessages = true; diff --git a/tools/run-structured-components-test.sh b/tools/run-structured-components-test.sh new file mode 100755 index 00000000..a188e7d6 --- /dev/null +++ b/tools/run-structured-components-test.sh @@ -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 +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 "; 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