From 52286701310cdbed3a392d1089f2d0f06d21330d Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 5 Jun 2026 20:26:29 +0200 Subject: [PATCH 1/4] Fix inventory handling across protocol versions --- MinecraftClient/Commands/Useblock.cs | 22 ++- MinecraftClient/Mapping/World.cs | 64 +++++++-- MinecraftClient/McClient.cs | 106 ++++++++++++++- MinecraftClient/MinecraftClient.csproj | 27 +--- .../PacketPalettes/PacketPalette119.cs | 43 +++--- .../PacketPalettes/PacketPalette19.cs | 127 ++++++++++++++++++ .../Protocol/Handlers/PacketType18Handler.cs | 1 + .../Protocol/Handlers/PacketTypesIn.cs | 2 +- .../Protocol/Handlers/Protocol18.cs | 61 +++++---- 9 files changed, 364 insertions(+), 89 deletions(-) create mode 100644 MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette19.cs diff --git a/MinecraftClient/Commands/Useblock.cs b/MinecraftClient/Commands/Useblock.cs index 7e482ba4..d4f964a6 100644 --- a/MinecraftClient/Commands/Useblock.cs +++ b/MinecraftClient/Commands/Useblock.cs @@ -1,3 +1,4 @@ +using System; using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; @@ -53,8 +54,27 @@ namespace MinecraftClient.Commands Location current = handler.GetCurrentLocation(); block = block.ToAbsolute(current).ToFloor(); Location blockCenter = block.ToCenter(); - bool res = handler.PlaceBlock(block, Direction.Down, hand, lookAtBlock: true); + bool res = handler.PlaceBlock(block, GetFaceNearestPlayer(current, blockCenter), hand, lookAtBlock: true); return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res); } + + private static Direction GetFaceNearestPlayer(Location playerLocation, Location blockCenter) + { + double dx = playerLocation.X - blockCenter.X; + double dy = playerLocation.Y - blockCenter.Y; + double dz = playerLocation.Z - blockCenter.Z; + + double absX = Math.Abs(dx); + double absY = Math.Abs(dy); + double absZ = Math.Abs(dz); + + if (absX >= absY && absX >= absZ) + return dx >= 0 ? Direction.East : Direction.West; + + if (absY >= absZ) + return dy >= 0 ? Direction.Up : Direction.Down; + + return dz >= 0 ? Direction.South : Direction.North; + } } } diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index aacfe1ec..0b6330ce 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -69,7 +69,16 @@ namespace MinecraftClient.Mapping /// Registry Codec nbt data public static void StoreDimensionList(Dictionary registryCodec) { - var dimensionListNbt = (object[])(((Dictionary)registryCodec["minecraft:dimension_type"])["value"]); + const string namespacedDimensionTypeKey = "minecraft:dimension_type"; + const string legacyDimensionTypeKey = "dimension_type"; + + if (!registryCodec.TryGetValue(namespacedDimensionTypeKey, out var dimensionTypeRegistry) + && !registryCodec.TryGetValue(legacyDimensionTypeKey, out dimensionTypeRegistry)) + { + return; + } + + var dimensionListNbt = (object[])(((Dictionary)dimensionTypeRegistry)["value"]); foreach (var (dimensionName, dimensionType) in from Dictionary dimensionNbt in dimensionListNbt let dimensionName = (string)dimensionNbt["name"] let dimensionType = (Dictionary)dimensionNbt["element"] @@ -324,19 +333,48 @@ namespace MinecraftClient.Mapping } // If not found, check if name lacks 'minecraft:' prefix and try again - if (!name.StartsWith("minecraft:")) - { - string prefixedName = "minecraft:" + name; - if (dimensionList.TryGetValue(prefixedName, out dimension)) - { - curDimension = dimension; - return; // Dimension found with prefixed name - } - } + if (!name.StartsWith("minecraft:")) + { + string prefixedName = "minecraft:" + name; + if (dimensionList.TryGetValue(prefixedName, out dimension)) + { + curDimension = dimension; + return; // Dimension found with prefixed name + } + } + else + { + string unprefixedName = name["minecraft:".Length..]; + if (dimensionList.TryGetValue(unprefixedName, out dimension)) + { + curDimension = dimension; + return; + } + } - // If still not found, dimension does not exist - throw new KeyNotFoundException($"Dimension '{name}' not found in dimensions dictionary."); - } + if (TryStoreDefaultVanillaDimension(name) + && dimensionList.TryGetValue(name, out dimension)) + { + curDimension = dimension; + return; + } + + // If still not found, dimension does not exist + throw new KeyNotFoundException($"Dimension '{name}' not found in dimensions dictionary."); + } + + private static bool TryStoreDefaultVanillaDimension(string name) + { + var normalizedName = name.StartsWith("minecraft:") + ? name + : "minecraft:" + name; + + if (normalizedName is not ("minecraft:overworld" or "minecraft:the_nether" or "minecraft:the_end")) + return false; + + StoreOneDimension(name, new Dictionary()); + return true; + } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index cde3ad55..b02e3269 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -2022,6 +2022,90 @@ namespace MinecraftClient }; } + private static bool TryGetMirroredPlayerInventoryRange(Container inventory, out int firstWindowSlot, out int lastWindowSlot) + { + firstWindowSlot = -1; + lastWindowSlot = -1; + + if (inventory.Type == ContainerType.PlayerInventory) + return false; + + const int mirroredPlayerInventorySlotCount = 36; + int slotCount = inventory.Type.SlotCount(); + if (slotCount < mirroredPlayerInventorySlotCount) + return false; + + firstWindowSlot = slotCount - mirroredPlayerInventorySlotCount; + lastWindowSlot = slotCount - 1; + return true; + } + + private static bool TryGetMirroredPlayerInventorySlot(Container inventory, int windowSlot, out int playerInventorySlot) + { + playerInventorySlot = -1; + + if (!TryGetMirroredPlayerInventoryRange(inventory, out int firstWindowSlot, out int lastWindowSlot)) + return false; + + if (windowSlot < firstWindowSlot || windowSlot > lastWindowSlot) + return false; + + playerInventorySlot = windowSlot - firstWindowSlot + 9; + return true; + } + + private static bool AreSameInventorySlot(Item? left, Item? right) + { + if (left is null || left.IsEmpty) + return right is null || right.IsEmpty; + if (right is null || right.IsEmpty) + return false; + + return left.Type == right.Type + && left.Count == right.Count + && left.Data == right.Data + && ReferenceEquals(left.NBT, right.NBT) + && ReferenceEquals(left.Components, right.Components); + } + + private bool SetPlayerInventorySlot(int playerInventorySlot, Item? item) + { + if (!inventories.TryGetValue(0, out Container? playerInventory)) + return false; + + playerInventory.Items.TryGetValue(playerInventorySlot, out Item? previousItem); + if (AreSameInventorySlot(previousItem, item)) + return false; + + if (item is null || item.IsEmpty) + playerInventory.Items.Remove(playerInventorySlot); + else + playerInventory.Items[playerInventorySlot] = item; + + return true; + } + + private bool SyncPlayerInventorySlotFromWindow(Container inventory, int windowSlot) + { + if (!TryGetMirroredPlayerInventorySlot(inventory, windowSlot, out int playerInventorySlot)) + return false; + + inventory.Items.TryGetValue(windowSlot, out Item? item); + return SetPlayerInventorySlot(playerInventorySlot, item); + } + + private bool SyncPlayerInventorySlotsFromWindow(Container inventory) + { + if (!TryGetMirroredPlayerInventoryRange(inventory, out int firstWindowSlot, out int lastWindowSlot)) + return false; + + bool changed = false; + for (int windowSlot = firstWindowSlot; windowSlot <= lastWindowSlot; windowSlot++) + changed |= SyncPlayerInventorySlotFromWindow(inventory, windowSlot); + + return changed; + } + /// /// Click a slot in the specified window /// @@ -2748,6 +2832,8 @@ namespace MinecraftClient changedSlots.Add(new Tuple((short)slotId, null)); break; } + + SyncPlayerInventorySlotsFromWindow(inventory); } return handler.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID); @@ -2764,7 +2850,16 @@ namespace MinecraftClient /// TRUE if item given successfully public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary? nbt = null) { - return InvokeOnMainThread(() => handler.SendCreativeInventoryAction(slot, itemType, count, nbt)); + return InvokeOnMainThread(() => + { + if (!handler.SendCreativeInventoryAction(slot, itemType, count, nbt)) + return false; + + if (slot is >= 1 and <= 45) + SetPlayerInventorySlot(slot, new Item(itemType, count, nbt)); + + return true; + }); } /// @@ -3780,6 +3875,9 @@ namespace MinecraftClient { inventories[inventoryID].Items = itemList; inventories[inventoryID].StateID = stateId; + bool playerInventoryChanged = SyncPlayerInventorySlotsFromWindow(inventories[inventoryID]); + if (playerInventoryChanged) + DispatchBotEvent(bot => bot.OnInventoryUpdate(0)); DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); } } @@ -3820,6 +3918,9 @@ namespace MinecraftClient inventories[inventoryID].Items.Remove(slotID); } else inventories[inventoryID].Items[slotID] = item; + + if (SyncPlayerInventorySlotFromWindow(inventories[inventoryID], slotID)) + DispatchBotEvent(bot => bot.OnInventoryUpdate(0)); } } DispatchBotEvent(bot => bot.OnInventoryUpdate(inventoryID)); @@ -4676,6 +4777,9 @@ namespace MinecraftClient { switch (reason) { + case 3: + OnGamemodeUpdate(Guid.Empty, (int)value); + break; case 7: DispatchBotEvent(bot => bot.OnRainLevelChange(value)); break; diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 9a9c8baa..4307393e 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -59,32 +59,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette119.cs b/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette119.cs index 0757dcb8..7a7e9eb6 100644 --- a/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette119.cs +++ b/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette119.cs @@ -118,9 +118,9 @@ namespace MinecraftClient.Protocol.Handlers.PacketPalettes { 0x00, PacketTypesOut.TeleportConfirm }, // (Wiki name: Confirm Teleportation) { 0x01, PacketTypesOut.QueryBlockNBT }, // (Wiki name: Query Block Entity Tag) { 0x02, PacketTypesOut.SetDifficulty }, // (Wiki name: Change Difficutly) - { 0x03, PacketTypesOut.MessageAcknowledgment }, // - { 0x04, PacketTypesOut.ChatCommand }, // Added in 1.19 - { 0x05, PacketTypesOut.ChatMessage }, // Changed in 1.19 (Completely changed) (Wiki name: Chat) + { 0x03, PacketTypesOut.ChatCommand }, // Added in 1.19 + { 0x04, PacketTypesOut.ChatMessage }, // Changed in 1.19 (Completely changed) (Wiki name: Chat) + { 0x05, PacketTypesOut.ChatPreview }, // Added in 1.19 (Wiki name: Chat Preview (serverbound)) { 0x06, PacketTypesOut.ClientStatus }, // (Wiki name: Client Command) { 0x07, PacketTypesOut.ClientSettings }, // (Wiki name: Client Information) { 0x08, PacketTypesOut.TabComplete }, // (Wiki name: Command Suggestions Request) @@ -147,25 +147,24 @@ namespace MinecraftClient.Protocol.Handlers.PacketPalettes { 0x1D, PacketTypesOut.EntityAction }, // (Wiki name: Player Command) { 0x1E, PacketTypesOut.SteerVehicle }, // (Wiki name: Player Input) { 0x1F, PacketTypesOut.Pong }, // (Wiki name: Pong (play)) - { 0x20, PacketTypesOut.PlayerSession }, // Added in 1.19.3 - { 0x21, PacketTypesOut.SetDisplayedRecipe }, // (Wiki name: Recipe Book Change Settings) - { 0x22, PacketTypesOut.SetRecipeBookState }, // (Wiki name: Recipe Book Seen Recipe) - { 0x23, PacketTypesOut.NameItem }, // (Wiki name: Rename Item) - { 0x24, PacketTypesOut.ResourcePackStatus }, // (Wiki name: Resource Pack (serverbound)) - { 0x25, PacketTypesOut.AdvancementTab }, // (Wiki name: Seen Advancements) - { 0x26, PacketTypesOut.SelectTrade }, // - { 0x27, PacketTypesOut.SetBeaconEffect }, // Changed in 1.19 (Added a "Secondary Effect Present" and "Secondary Effect" fields) (Wiki name: Set Beacon) - (No need to be implemented) - { 0x28, PacketTypesOut.HeldItemChange }, // (Wiki name: Set Carried Item (serverbound)) - { 0x29, PacketTypesOut.UpdateCommandBlock }, // (Wiki name: Set Command Block) - { 0x2A, PacketTypesOut.UpdateCommandBlockMinecart }, // - { 0x2B, PacketTypesOut.CreativeInventoryAction }, // (Wiki name: Set Creative Mode Slot) - { 0x2C, PacketTypesOut.UpdateJigsawBlock }, // (Wiki name: Set Jigsaw Block) - { 0x2D, PacketTypesOut.UpdateStructureBlock }, // (Wiki name: Set Structure Block) - { 0x2E, PacketTypesOut.UpdateSign }, // (Wiki name: Sign Update) - { 0x2F, PacketTypesOut.Animation }, // (Wiki name: Swing) - { 0x30, PacketTypesOut.Spectate }, // (Wiki name: Teleport To Entity) - { 0x31, PacketTypesOut.PlayerBlockPlacement }, // Changed in 1.19 (Added a "Sequence" field) (Wiki name: Use Item On) - { 0x32, PacketTypesOut.UseItem }, // Changed in 1.19 (Added a "Sequence" field) (Wiki name: Use Item) + { 0x20, PacketTypesOut.SetDisplayedRecipe }, // (Wiki name: Recipe Book Change Settings) + { 0x21, PacketTypesOut.SetRecipeBookState }, // (Wiki name: Recipe Book Seen Recipe) + { 0x22, PacketTypesOut.NameItem }, // (Wiki name: Rename Item) + { 0x23, PacketTypesOut.ResourcePackStatus }, // (Wiki name: Resource Pack (serverbound)) + { 0x24, PacketTypesOut.AdvancementTab }, // (Wiki name: Seen Advancements) + { 0x25, PacketTypesOut.SelectTrade }, // + { 0x26, PacketTypesOut.SetBeaconEffect }, // Changed in 1.19 (Added a "Secondary Effect Present" and "Secondary Effect" fields) (Wiki name: Set Beacon) - (No need to be implemented) + { 0x27, PacketTypesOut.HeldItemChange }, // (Wiki name: Set Carried Item (serverbound)) + { 0x28, PacketTypesOut.UpdateCommandBlock }, // (Wiki name: Set Command Block) + { 0x29, PacketTypesOut.UpdateCommandBlockMinecart }, // + { 0x2A, PacketTypesOut.CreativeInventoryAction }, // (Wiki name: Set Creative Mode Slot) + { 0x2B, PacketTypesOut.UpdateJigsawBlock }, // (Wiki name: Set Jigsaw Block) + { 0x2C, PacketTypesOut.UpdateStructureBlock }, // (Wiki name: Set Structure Block) + { 0x2D, PacketTypesOut.UpdateSign }, // (Wiki name: Sign Update) + { 0x2E, PacketTypesOut.Animation }, // (Wiki name: Swing) + { 0x2F, PacketTypesOut.Spectate }, // (Wiki name: Teleport To Entity) + { 0x30, PacketTypesOut.PlayerBlockPlacement }, // Changed in 1.19 (Added a "Sequence" field) (Wiki name: Use Item On) + { 0x31, PacketTypesOut.UseItem }, // Changed in 1.19 (Added a "Sequence" field) (Wiki name: Use Item) }; protected override Dictionary GetListIn() => typeIn; diff --git a/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette19.cs b/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette19.cs new file mode 100644 index 00000000..ebffc31a --- /dev/null +++ b/MinecraftClient/Protocol/Handlers/PacketPalettes/PacketPalette19.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; + +namespace MinecraftClient.Protocol.Handlers.PacketPalettes +{ + public class PacketPalette19 : PacketTypePalette + { + private readonly Dictionary typeIn = new() + { + { 0x00, PacketTypesIn.SpawnEntity }, + { 0x01, PacketTypesIn.SpawnExperienceOrb }, + { 0x02, PacketTypesIn.SpawnWeatherEntity }, + { 0x03, PacketTypesIn.SpawnLivingEntity }, + { 0x04, PacketTypesIn.SpawnPainting }, + { 0x05, PacketTypesIn.SpawnPlayer }, + { 0x06, PacketTypesIn.EntityAnimation }, + { 0x07, PacketTypesIn.Statistics }, + { 0x08, PacketTypesIn.BlockBreakAnimation }, + { 0x09, PacketTypesIn.BlockEntityData }, + { 0x0A, PacketTypesIn.BlockAction }, + { 0x0B, PacketTypesIn.BlockChange }, + { 0x0C, PacketTypesIn.BossBar }, + { 0x0D, PacketTypesIn.ServerDifficulty }, + { 0x0E, PacketTypesIn.TabComplete }, + { 0x0F, PacketTypesIn.ChatMessage }, + { 0x10, PacketTypesIn.MultiBlockChange }, + { 0x11, PacketTypesIn.WindowConfirmation }, + { 0x12, PacketTypesIn.CloseWindow }, + { 0x13, PacketTypesIn.OpenWindow }, + { 0x14, PacketTypesIn.WindowItems }, + { 0x15, PacketTypesIn.WindowProperty }, + { 0x16, PacketTypesIn.SetSlot }, + { 0x17, PacketTypesIn.SetCooldown }, + { 0x18, PacketTypesIn.PluginMessage }, + { 0x19, PacketTypesIn.NamedSoundEffect }, + { 0x1A, PacketTypesIn.Disconnect }, + { 0x1B, PacketTypesIn.EntityStatus }, + { 0x1C, PacketTypesIn.Explosion }, + { 0x1D, PacketTypesIn.UnloadChunk }, + { 0x1E, PacketTypesIn.ChangeGameState }, + { 0x1F, PacketTypesIn.KeepAlive }, + { 0x20, PacketTypesIn.ChunkData }, + { 0x21, PacketTypesIn.Effect }, + { 0x22, PacketTypesIn.Particle }, + { 0x23, PacketTypesIn.JoinGame }, + { 0x24, PacketTypesIn.MapData }, + { 0x25, PacketTypesIn.EntityPosition }, + { 0x26, PacketTypesIn.EntityPositionAndRotation }, + { 0x27, PacketTypesIn.EntityRotation }, + { 0x28, PacketTypesIn.EntityMovement }, + { 0x29, PacketTypesIn.VehicleMove }, + { 0x2A, PacketTypesIn.OpenSignEditor }, + { 0x2B, PacketTypesIn.PlayerAbilities }, + { 0x2C, PacketTypesIn.CombatEvent }, + { 0x2D, PacketTypesIn.PlayerInfo }, + { 0x2E, PacketTypesIn.PlayerPositionAndLook }, + { 0x2F, PacketTypesIn.UseBed }, + { 0x30, PacketTypesIn.DestroyEntities }, + { 0x31, PacketTypesIn.RemoveEntityEffect }, + { 0x32, PacketTypesIn.ResourcePackSend }, + { 0x33, PacketTypesIn.Respawn }, + { 0x34, PacketTypesIn.EntityHeadLook }, + { 0x35, PacketTypesIn.WorldBorder }, + { 0x36, PacketTypesIn.Camera }, + { 0x37, PacketTypesIn.HeldItemChange }, + { 0x38, PacketTypesIn.DisplayScoreboard }, + { 0x39, PacketTypesIn.EntityMetadata }, + { 0x3A, PacketTypesIn.AttachEntity }, + { 0x3B, PacketTypesIn.EntityVelocity }, + { 0x3C, PacketTypesIn.EntityEquipment }, + { 0x3D, PacketTypesIn.SetExperience }, + { 0x3E, PacketTypesIn.UpdateHealth }, + { 0x3F, PacketTypesIn.ScoreboardObjective }, + { 0x40, PacketTypesIn.SetPassengers }, + { 0x41, PacketTypesIn.Teams }, + { 0x42, PacketTypesIn.UpdateScore }, + { 0x43, PacketTypesIn.SpawnPosition }, + { 0x44, PacketTypesIn.TimeUpdate }, + { 0x45, PacketTypesIn.Title }, + { 0x46, PacketTypesIn.UpdateSign }, + { 0x47, PacketTypesIn.SoundEffect }, + { 0x48, PacketTypesIn.PlayerListHeaderAndFooter }, + { 0x49, PacketTypesIn.CollectItem }, + { 0x4A, PacketTypesIn.EntityTeleport }, + { 0x4B, PacketTypesIn.EntityProperties }, + { 0x4C, PacketTypesIn.EntityEffect }, + }; + + private readonly Dictionary typeOut = new() + { + { 0x00, PacketTypesOut.TeleportConfirm }, + { 0x01, PacketTypesOut.TabComplete }, + { 0x02, PacketTypesOut.ChatMessage }, + { 0x03, PacketTypesOut.ClientStatus }, + { 0x04, PacketTypesOut.ClientSettings }, + { 0x05, PacketTypesOut.WindowConfirmation }, + { 0x06, PacketTypesOut.EnchantItem }, + { 0x07, PacketTypesOut.ClickWindow }, + { 0x08, PacketTypesOut.CloseWindow }, + { 0x09, PacketTypesOut.PluginMessage }, + { 0x0A, PacketTypesOut.InteractEntity }, + { 0x0B, PacketTypesOut.KeepAlive }, + { 0x0C, PacketTypesOut.PlayerPosition }, + { 0x0D, PacketTypesOut.PlayerPositionAndRotation }, + { 0x0E, PacketTypesOut.PlayerRotation }, + { 0x0F, PacketTypesOut.PlayerMovement }, + { 0x10, PacketTypesOut.VehicleMove }, + { 0x11, PacketTypesOut.SteerBoat }, + { 0x12, PacketTypesOut.PlayerAbilities }, + { 0x13, PacketTypesOut.PlayerDigging }, + { 0x14, PacketTypesOut.EntityAction }, + { 0x15, PacketTypesOut.SteerVehicle }, + { 0x16, PacketTypesOut.ResourcePackStatus }, + { 0x17, PacketTypesOut.HeldItemChange }, + { 0x18, PacketTypesOut.CreativeInventoryAction }, + { 0x19, PacketTypesOut.UpdateSign }, + { 0x1A, PacketTypesOut.Animation }, + { 0x1B, PacketTypesOut.Spectate }, + { 0x1C, PacketTypesOut.PlayerBlockPlacement }, + { 0x1D, PacketTypesOut.UseItem }, + }; + + protected override Dictionary GetListIn() => typeIn; + protected override Dictionary GetListOut() => typeOut; + protected override Dictionary GetConfigurationListIn() => new(); + protected override Dictionary GetConfigurationListOut() => new(); + } +} diff --git a/MinecraftClient/Protocol/Handlers/PacketType18Handler.cs b/MinecraftClient/Protocol/Handlers/PacketType18Handler.cs index 21515782..82ee6e69 100644 --- a/MinecraftClient/Protocol/Handlers/PacketType18Handler.cs +++ b/MinecraftClient/Protocol/Handlers/PacketType18Handler.cs @@ -56,6 +56,7 @@ namespace MinecraftClient.Protocol.Handlers <= Protocol18Handler.MC_1_21_5_Version and > Protocol18Handler.MC_1_21_4_Version => new PacketPalette1215(), <= Protocol18Handler.MC_1_21_4_Version and > Protocol18Handler.MC_1_21_2_Version => new PacketPalette1214(), <= Protocol18Handler.MC_1_8_Version => new PacketPalette17(), + <= Protocol18Handler.MC_1_9_2_Version => new PacketPalette19(), <= Protocol18Handler.MC_1_11_2_Version => new PacketPalette110(), <= Protocol18Handler.MC_1_12_Version => new PacketPalette112(), <= Protocol18Handler.MC_1_12_2_Version => new PacketPalette1122(), diff --git a/MinecraftClient/Protocol/Handlers/PacketTypesIn.cs b/MinecraftClient/Protocol/Handlers/PacketTypesIn.cs index a36bc4ba..8aee524a 100644 --- a/MinecraftClient/Protocol/Handlers/PacketTypesIn.cs +++ b/MinecraftClient/Protocol/Handlers/PacketTypesIn.cs @@ -146,7 +146,7 @@ namespace MinecraftClient.Protocol.Handlers UpdateHealth, // UpdateLight, // UpdateScore, // - UpdateSign, // For 1.8 or below + UpdateSign, // For 1.8 or below, and 1.9-1.9.2 UpdateSimulationDistance, // UpdateViewDistance, // UpdateViewPosition, // diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 9f56a622..5f71b9af 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -45,6 +45,7 @@ namespace MinecraftClient.Protocol.Handlers internal const int MC_1_8_Version = 47; internal const int MC_1_9_Version = 107; internal const int MC_1_9_1_Version = 108; + internal const int MC_1_9_2_Version = 109; internal const int MC_1_10_Version = 210; internal const int MC_1_11_Version = 315; internal const int MC_1_11_2_Version = 316; @@ -835,15 +836,15 @@ namespace MinecraftClient.Protocol.Handlers dimensionTypeName = dataTypes.ReadNextString(packetData); // Dimension Type: Identifier break; - case >= MC_1_16_2_Version: - dimensionType = - dataTypes.ReadNextNbt( - packetData); // Dimension Type: NBT Tag Compound - break; - default: - dataTypes.ReadNextString(packetData); - break; - } + case >= MC_1_16_2_Version: + dimensionType = + dataTypes.ReadNextNbt( + packetData); // Dimension Type: NBT Tag Compound + break; + default: + dimensionTypeName = dataTypes.ReadNextString(packetData); + break; + } currentDimension = 0; break; @@ -1408,14 +1409,14 @@ namespace MinecraftClient.Protocol.Handlers dimensionTypeNameRespawn = dataTypes.ReadNextString(packetData); // Dimension Type: Identifier break; - case >= MC_1_16_2_Version: - dimensionTypeRespawn = - dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound - break; - default: - dataTypes.ReadNextString(packetData); - break; - } + case >= MC_1_16_2_Version: + dimensionTypeRespawn = + dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound + break; + default: + dimensionTypeNameRespawn = dataTypes.ReadNextString(packetData); + break; + } currentDimension = 0; } @@ -4027,6 +4028,21 @@ namespace MinecraftClient.Protocol.Handlers int blockEntityCount = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < blockEntityCount; i++) { + if (protocolVersion < MC_1_18_1_Version) + { + Dictionary? blockEntityNbt = dataTypes.ReadNextNbt(packetData); + if (blockEntityNbt.TryGetValue("x", out var nbtX) + && blockEntityNbt.TryGetValue("y", out var nbtY) + && blockEntityNbt.TryGetValue("z", out var nbtZ)) + { + handler.OnBlockEntityData( + new Location(Convert.ToInt32(nbtX), Convert.ToInt32(nbtY), Convert.ToInt32(nbtZ)), + blockEntityNbt); + } + + continue; + } + int packedXZ = dataTypes.ReadNextByte(packetData); int y = dataTypes.ReadNextShort(packetData); dataTypes.ReadNextVarInt(packetData); // Block entity type registry id @@ -5616,7 +5632,7 @@ namespace MinecraftClient.Protocol.Handlers private static byte ToLegacyBlockPlacementCursor(float cursor) { - return (byte)Math.Clamp((int)(cursor * 16.0f), 0, byte.MaxValue); + return (byte)Math.Clamp((int)(cursor * 16.0f), 0, 15); } public bool SendHeldItemChange(short slot) @@ -5752,16 +5768,11 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { - // 1.18+ - case >= MC_1_18_1_Version: + // 1.17.1+ + case >= MC_1_17_1_Version: packet.AddRange(DataTypes.GetVarInt(stateId)); // State ID packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID break; - // 1.17.1 - case MC_1_17_1_Version: - packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID - packet.AddRange(DataTypes.GetVarInt(stateId)); // State ID - break; // Older default: packet.AddRange(dataTypes.GetShort((short)slotId)); // Slot ID From 5545aabb60dbb0a8d51bba06af1d726fef6f2a53 Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 5 Jun 2026 20:57:49 +0200 Subject: [PATCH 2/4] Add reusable inventory sweep tooling --- .skills/mcc-integration-testing/SKILL.md | 43 ++ .skills/mcc-version-adaptation/SKILL.md | 55 ++ tools/README.md | 26 + tools/run-inventory-full-sweep.sh | 673 +++++++++++++++++++++++ 4 files changed, 797 insertions(+) create mode 100755 tools/run-inventory-full-sweep.sh diff --git a/.skills/mcc-integration-testing/SKILL.md b/.skills/mcc-integration-testing/SKILL.md index 168b545f..6316e0b8 100644 --- a/.skills/mcc-integration-testing/SKILL.md +++ b/.skills/mcc-integration-testing/SKILL.md @@ -115,6 +115,43 @@ 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 + +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: + +```bash +tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11" +``` + +Default coverage includes: + +- player inventory listing and inventory discovery +- creative give/delete +- inventory search +- player right/left click stack split and merge +- player drop one and drop all +- chest open via `useblock` +- container listing and close +- mirrored player slots in container windows +- shift-click and shift-right-click transfer +- container right/left click, cursor stack, drop one, and drop all +- creative middle-click command path +- log scan for packet parse failures, queue-empty crashes, unhandled exceptions, and disconnects + +Run the Issue #3112 repro after a passing sweep: + +```bash +tools/run-inventory-full-sweep.sh --versions "1.20.4" --run-issue-script +``` + +The script writes `summary.tsv` under `RUN_ROOT` and per-version logs under `/tmp/mcc-debug/inventory-full-/mcc-debug.log`. + +When a matrix has existing PASS rows, do not rerun them unless a later code change affects that row or the user asks for a full rerun. Derive remaining rows from summaries: + +```bash +awk 'FNR>1 && $2=="PASS" {print $1}' /tmp/mcc-inventory-full-sweep/*/summary.tsv | sort -V | uniq +``` + ## Preconditions Before running any scenario: @@ -165,6 +202,8 @@ Optionally override the login name with the fourth argument to the config helper - summarize the latest full-spectrum run - `tools/run-creative-e2e.sh` - ordered creative-mode E2E regression scenario +- `tools/run-inventory-full-sweep.sh` + - full inventory command/API sweep across one or more versions, with optional Issue #3112 MCCScript repro ## Evidence Discipline @@ -224,3 +263,7 @@ Always summarize: - If a test assertion fails, inspect the real MCC output before changing the code or weakening the assertion. - If an older server behaves oddly on Linux, check `use-native-transport=false` in `server.properties`. - If a matrix row fails before producing `mcc.log` or a command transcript, treat it as a harness failure, fix the environment, and rerun that row before drawing product conclusions. +- If creative inventory commands report "You must be in Creative gamemode" after RCON switched the player, inspect game-mode update parsing before assuming creative inventory is broken. Modern servers can update local game mode through game event reason `3`. +- If an inventory row crashes with `Queue empty` or `Failed to process incoming packet`, inspect packet palette routing before changing inventory code. A single shifted packet ID can make a healthy inventory feature look broken. +- For chest-open failures, separate product and harness causes. The player may be standing inside the chest or suffocating on older servers. Stand beside the chest, put a floor under the player, and retry `useblock`. +- For shared local servers, a `Done` log line does not prove RCON is ready. Retry setup commands and verify the actual RCON port from `server.properties`. diff --git a/.skills/mcc-version-adaptation/SKILL.md b/.skills/mcc-version-adaptation/SKILL.md index 706399cb..c3be7c12 100644 --- a/.skills/mcc-version-adaptation/SKILL.md +++ b/.skills/mcc-version-adaptation/SKILL.md @@ -157,6 +157,50 @@ When packet changes are detected: 2. Create new `PacketPaletteXXX.cs` based on the previous one, adjusting IDs 3. Update `PacketType18Handler.cs` routing +Use scriptable comparisons instead of eyeballing long packet tables. The packet ID is the registration index in `GameProtocols.java`: + +```bash +python3 - <<'PY' +import re +for ver in ["1.21.10", "1.21.11", "26.1"]: + path=f"MinecraftOfficial/{ver}-decompiled/net/minecraft/network/protocol/game/GameProtocols.java" + text=open(path).read() + start=text.index("CLIENTBOUND_TEMPLATE") + names=[m.group(1) for m in re.finditer(r"\.addPacket\(([^,]+),", text[start:])] + print("==", ver, len(names)) + for i, name in enumerate(names): + print(f"0x{i:02X}", name) +PY +``` + +For focused diffs: + +```bash +python3 - <<'PY' +import re +def packets(ver, marker): + text=open(f"MinecraftOfficial/{ver}-decompiled/net/minecraft/network/protocol/game/GameProtocols.java").read() + start=text.index(marker) + return [m.group(1) for m in re.finditer(r"\.addPacket\(([^,]+),", text[start:])] +left, right = "1.21.10", "1.21.11" +a, b = packets(left, "CLIENTBOUND_TEMPLATE"), packets(right, "CLIENTBOUND_TEMPLATE") +for i in range(max(len(a), len(b))): + x = a[i] if i < len(a) else "" + y = b[i] if i < len(b) else "" + if x != y: + print(f"0x{i:02X}: {left}={x} | {right}={y}") +PY +``` + +Do the same for `SERVERBOUND_TEMPLATE`. Clientbound and serverbound can change independently. Do not inherit a newer palette just because one side looks similar. For example, `1.21.11` used the same play packet order as `1.21.9/1.21.10` for the tested inventory path, while `26.1` had additional shifts. + +Known packet lessons: + +- `1.9`, `1.9.1`, and `1.9.2` need their own packet palette. They are not safe to route through the later 1.9.x palette. +- Pure `1.19` serverbound IDs differ from later 1.19.x. Do not put `MessageAcknowledgment` at `0x03`; pure 1.19 has `ChatCommand` at `0x03`, `ChatMessage` at `0x04`, and `ChatPreview` at `0x05`. +- A wrong packet palette often appears as unrelated inventory failure: creative give/delete disconnects, `Queue empty`, or `Failed to process incoming packet`. +- Game event reason `3` is `CHANGE_GAME_MODE`. If RCON changed the player to creative but MCC still refuses creative inventory commands, inspect `ChangeGameState` handling. + ## Step 5: Check Variant Encoding Changes For entity types that use variant serializers (Cat, Wolf, Frog, Painting), check if the codec changed between versions by inspecting: @@ -186,6 +230,17 @@ Compare key packet codec classes between versions. Known changes: When in doubt, compare the relevant packet class (e.g. `ClientboundAddEntityPacket.java`) between versions. +## Step 7.1: Check JoinGame and Respawn Formats + +JoinGame and Respawn are high-risk because dimension fields changed several times: + +- `1.16` and `1.16.1`: dimension type/name handling uses string identifiers in places where later versions do not. +- `1.16.2` through `1.18.2`: dimension type can be an NBT compound in JoinGame/Respawn. +- `1.19+`: dimension type commonly moves back to identifiers. +- `1.20.6+`: registry-driven IDs appear in more fields. + +When a version joins but terrain, inventory, or later packets look misaligned, inspect JoinGame/Respawn first. A single wrong dimension-field read leaves unread bytes in the packet and can make the next packet look broken. + ## Step 8: Update Block Collision Shapes (Physics Engine) MCC's physics engine uses block collision shape data from PrismarineJS `minecraft-data` to perform accurate AABB collision detection (stored in `MinecraftClient/Physics/BlockShapeData.json`, embedded as a resource). diff --git a/tools/README.md b/tools/README.md index be83c64d..87a65480 100644 --- a/tools/README.md +++ b/tools/README.md @@ -26,6 +26,32 @@ mcc-publish --rid linux-x64 Keep shared servers running by default. Do not stop or reset them unless the user explicitly asks for that, or you need to switch server versions. +### Full inventory regression sweep + +Use `tools/run-inventory-full-sweep.sh` when changing inventory, container, item-slot serialization, packet palettes, game-mode handling, or block-use behavior. It runs MCC against real local servers with temporary configs and checks player inventory, creative give/delete, search, click, drop, chest container, mirrored player slots, and crash markers. + +```bash +# Focused retest +tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11" + +# Full default major-version sweep +tools/run-inventory-full-sweep.sh + +# Run the Issue #3112 mirrored-player-inventory script after a passing sweep +tools/run-inventory-full-sweep.sh --versions "1.20.4" --run-issue-script +``` + +Useful environment overrides: + +```bash +RUN_ROOT=/tmp/my-inventory-run \ +MCC_SERVERS=/path/to/servers \ +STOP_ON_FAIL=1 \ +tools/run-inventory-full-sweep.sh --versions "1.19 1.20.4" +``` + +The script writes `summary.tsv` under `RUN_ROOT`. Per-version MCC logs are under `/tmp/mcc-debug/inventory-full-/mcc-debug.log`, and command output blocks are saved next to the summary. + ### tmpfs build mode ```bash diff --git a/tools/run-inventory-full-sweep.sh b/tools/run-inventory-full-sweep.sh new file mode 100755 index 00000000..ea98846e --- /dev/null +++ b/tools/run-inventory-full-sweep.sh @@ -0,0 +1,673 @@ +#!/usr/bin/env bash +set -u -o pipefail + +SCRIPT_SELF="${BASH_SOURCE[0]}" +while [[ -L "$SCRIPT_SELF" ]]; do + SCRIPT_DIRNAME="$(cd -P "$(dirname "$SCRIPT_SELF")" >/dev/null 2>&1 && pwd)" + SCRIPT_SELF="$(readlink "$SCRIPT_SELF")" + [[ "$SCRIPT_SELF" != /* ]] && SCRIPT_SELF="$SCRIPT_DIRNAME/$SCRIPT_SELF" +done +REPO_ROOT="$(cd -P "$(dirname "$SCRIPT_SELF")/.." >/dev/null 2>&1 && pwd)" +SCRIPT_DIR="$REPO_ROOT/.skills/mcc-integration-testing/scripts" +RUN_ROOT="${RUN_ROOT:-/tmp/mcc-inventory-full-sweep/$(date +%Y%m%d-%H%M%S)}" +VERSIONS="${VERSIONS_OVERRIDE:-1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.18 1.19 1.20 1.21 26.1}" +RUN_ISSUE_SCRIPT="${RUN_ISSUE_SCRIPT:-0}" +ISSUE_VERSION="${ISSUE_VERSION:-1.20.4}" +STOP_ON_FAIL="${STOP_ON_FAIL:-1}" + +usage() { + cat <<'USAGE' +Usage: tools/run-inventory-full-sweep.sh [options] + +Runs MCC inventory command/API coverage against real local Minecraft servers. +The matrix is sequential because mc-* tmux sessions are shared state. + +Options: + --versions "1.20.4 1.21.11" Space-separated versions to test. + --run-issue-script Run the Issue #3112 MCCScript repro after a passing sweep. + --issue-version VERSION Version for the Issue #3112 repro. Default: 1.20.4. + --keep-going Continue after failures. + --stop-on-fail Stop on first failure. Default. + -h, --help Show this help. + +Environment overrides: + VERSIONS_OVERRIDE, RUN_ROOT, RUN_ISSUE_SCRIPT, ISSUE_VERSION, STOP_ON_FAIL, + MCC_SERVERS. + +Examples: + tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11" + RUN_ISSUE_SCRIPT=1 tools/run-inventory-full-sweep.sh --versions "1.20.4" +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --versions) + VERSIONS="$2" + shift 2 + ;; + --run-issue-script) + RUN_ISSUE_SCRIPT=1 + shift + ;; + --issue-version) + ISSUE_VERSION="$2" + shift 2 + ;; + --keep-going) + STOP_ON_FAIL=0 + shift + ;; + --stop-on-fail) + STOP_ON_FAIL=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +source "$REPO_ROOT/tools/mcc-env.sh" +source "$SCRIPT_DIR/common.sh" + +mkdir -p "$RUN_ROOT" +SUMMARY="$RUN_ROOT/summary.tsv" +printf 'target\tstatus\tdetail\tlog\n' > "$SUMMARY" + +wait_for_file_pattern_local() { + local file="$1" + local pattern="$2" + local timeout="${3:-10}" + local end=$((SECONDS + timeout)) + while (( SECONDS < end )); do + if [[ -f "$file" ]] && grep -Eq "$pattern" "$file"; then + return 0 + fi + sleep 0.2 + done + return 1 +} + +sanitize_version() { + printf '%s' "$1" | tr '.-' '__' +} + +server_target_for() { + local version="$1" + local dir + if [[ -d "${MCC_SERVERS:-}/$version-Vanilla" ]]; then + printf '%s-Vanilla' "$version" + elif [[ -d "$REPO_ROOT/MinecraftOfficial/downloads/$version" ]]; then + printf '%s' "$version" + else + printf '%s-Vanilla' "$version" + fi +} + +server_dir_for() { + local target="$1" + local root="${MCC_SERVERS:-$REPO_ROOT/MinecraftOfficial/downloads}" + printf '%s/%s\n' "$root" "$target" +} + +rcon_port_for() { + local target="$1" + local props + props="$(server_dir_for "$target")/server.properties" + if [[ -f "$props" ]]; then + local port_line + port_line="$(grep -E '^rcon\.port=' "$props" | tail -n 1 || true)" + if [[ -n "$port_line" ]]; then + printf '%s\n' "${port_line#rcon.port=}" + return 0 + fi + fi + printf '25575\n' +} + +run_rcon() { + local port="$1" + local command="$2" + local attempt + for attempt in 1 2 3 4 5; do + if mc-rcon "$command" "$port" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + return 1 +} + +run_rcon_or_detail() { + local port="$1" + local cmd="$2" + if run_rcon "$port" "$cmd"; then + return 0 + fi + FAIL_DETAIL="rcon failed: $cmd" + return 1 +} + +run_rcon_any_or_detail() { + local port="$1" + local detail="$2" + shift 2 + local cmd + for cmd in "$@"; do + if run_rcon "$port" "$cmd"; then + return 0 + fi + done + FAIL_DETAIL="$detail" + return 1 +} + +run_rcon_any() { + local port="$1" + shift + local cmd + for cmd in "$@"; do + if run_rcon "$port" "$cmd"; then + return 0 + fi + done + return 1 +} + +run_rcon_each() { + local port="$1" + shift + local cmd + for cmd in "$@"; do + run_rcon "$port" "$cmd" || true + done +} + +send_mcc_command() { + local session="$1" + local log_file="$2" + local command="$3" + local delay="${4:-1}" + local block_file="$5" + local mark + mark="$(wc -c < "$log_file" 2>/dev/null || printf '0')" + mcc-cmd --session "$session" "$command" >/dev/null + sleep "$delay" + LAST_BLOCK="$(tail -c "+$((mark + 1))" "$log_file" 2>/dev/null || true)" + { + printf '\n>>> %s\n' "$command" + printf '%s\n' "$LAST_BLOCK" + } >> "$block_file" +} + +assert_contains() { + grep -Eq "$2" <<<"$1" || { FAIL_DETAIL="$3"; return 1; } +} + +assert_not_contains() { + if grep -Eq "$2" <<<"$1"; then + FAIL_DETAIL="$3" + return 1 + fi +} + +assert_no_runtime_crash() { + local log_file="$1" + if grep -Eq 'Queue empty|Unhandled exception|Object reference not set|Failed to parse packet|Failed to process incoming packet|Connection has been lost' "$log_file"; then + FAIL_DETAIL="runtime log contains crash/disconnect marker" + return 1 + fi +} + +clear_dropped_items() { + local port="$1" + run_rcon_any "$port" "kill @e[type=item]" "kill @e[type=Item]" >/dev/null 2>&1 || true +} + +open_chest() { + local session="$1" + local log_file="$2" + local block_file="$3" + send_mcc_command "$session" "$log_file" "useblock 1 80 0" 2 "$block_file" + if wait_for_file_pattern_local "$log_file" "Inventory # 1 opened: Chest" 4; then + return 0 + fi + send_mcc_command "$session" "$log_file" "useblock 1 80 0" 2 "$block_file" + wait_for_file_pattern_local "$log_file" "Inventory # 1 opened: Chest" 12 +} + +setup_world() { + local port="$1" + run_rcon_or_detail "$port" "gamerule sendCommandFeedback true" || return 1 + run_rcon "$port" "gamerule keepInventory true" || true + run_rcon "$port" "time set day" || true + run_rcon "$port" "weather clear" || true + run_rcon "$port" "difficulty peaceful" || true +} + +setup_area() { + local port="$1" + run_rcon_each "$port" "fill -2 78 -3 3 82 3 air 0 replace" "fill -2 78 -3 3 82 3 air" "fill -2 78 -3 3 82 3 minecraft:air" + run_rcon_each "$port" "fill -2 79 -3 3 79 3 stone 0 replace" "fill -2 79 -3 3 79 3 stone" "fill -2 79 -3 3 79 3 minecraft:stone" + run_rcon_each "$port" "setblock 1 80 0 air 0 replace" "setblock 1 80 0 air" "setblock 1 80 0 minecraft:air" + run_rcon_each "$port" "setblock 1 80 0 chest 0 replace" "setblock 1 80 0 chest" "setblock 1 80 0 minecraft:chest" + run_rcon_each "$port" "blockdata 1 80 0 {Items:[]}" "data merge block 1 80 0 {Items:[]}" +} + +setup_player() { + local port="$1" + local username="$2" + run_rcon "$port" "op $username" || true + run_rcon "$port" "gamemode creative $username" || return 1 + run_rcon_any "$port" "tp $username 1.5 80 2.5" "tp $username 1 80 2" || true +} + +run_inventory_sequence() { + local version="$1" + local rcon_port="$2" + local session="$3" + local username="$4" + local log_file="$5" + local block_file="$6" + + send_mcc_command "$session" "$log_file" "inventory player drop -1 all" 1 "$block_file" || true + for slot in 36 37 38 39 40 41 42 43 44; do + send_mcc_command "$session" "$log_file" "inventory creativedelete $slot" 0.2 "$block_file" || true + done + + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_contains "$LAST_BLOCK" 'Inventory #0 - Player Inventory' "player inventory did not list" || return 1 + send_mcc_command "$session" "$log_file" "inventory inventories" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#0[[:space:]]+- Player Inventory' "inventory discovery did not list player inventory" || return 1 + + send_mcc_command "$session" "$log_file" "inventory creativegive 36 Diamond 16" 1 "$block_file" + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#36[[:space:]]*: x16[[:space:]]+Diamond' "creativegive did not populate player slot 36" || return 1 + send_mcc_command "$session" "$log_file" "inventory search Diamond 16" 1 "$block_file" + assert_contains "$LAST_BLOCK" 'Diamond' "inventory search did not find Diamond" || return 1 + send_mcc_command "$session" "$log_file" "inventory creativedelete 36" 1 "$block_file" + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#36[[:space:]]*: x16[[:space:]]+Diamond' "creativedelete left Diamond in player slot 36" || return 1 + + send_mcc_command "$session" "$log_file" "inventory creativegive 36 Dirt 3" 1 "$block_file" + run_rcon "$rcon_port" "gamemode survival $username" || return 1 + sleep 1 + send_mcc_command "$session" "$log_file" "inventory player click 36 right" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#36[[:space:]]*: x1[[:space:]]+Dirt' "player right-click did not halve Dirt stack" || return 1 + assert_contains "$LAST_BLOCK" '#-1[[:space:]]*: x2[[:space:]]+Dirt' "player right-click did not put Dirt on cursor" || return 1 + send_mcc_command "$session" "$log_file" "inventory player click 36 left" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#36[[:space:]]*: x3[[:space:]]+Dirt' "player left-click did not merge Dirt back into slot 36" || return 1 + assert_not_contains "$LAST_BLOCK" '#-1[[:space:]]*: x[0-9]+[[:space:]]+Dirt' "player left-click merge left Dirt on cursor" || return 1 + + run_rcon_any "$rcon_port" "tp $username 1.5 80 2.5" "tp $username 1 80 2" || true + sleep 1 + send_mcc_command "$session" "$log_file" "inventory player drop 36" 0.2 "$block_file" + clear_dropped_items "$rcon_port" + sleep 1 + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#36[[:space:]]*: x2[[:space:]]+Dirt' "single drop did not decrement Dirt stack" || return 1 + + run_rcon "$rcon_port" "gamemode creative $username" || return 1 + sleep 1 + clear_dropped_items "$rcon_port" + send_mcc_command "$session" "$log_file" "inventory creativedelete 36" 1 "$block_file" + send_mcc_command "$session" "$log_file" "inventory creativegive 36 Dirt 3" 1 "$block_file" + run_rcon "$rcon_port" "gamemode survival $username" || return 1 + run_rcon_any "$rcon_port" "tp $username 1.5 80 2.5" "tp $username 1 80 2" || true + sleep 1 + send_mcc_command "$session" "$log_file" "inventory player drop 36 all" 0.2 "$block_file" + clear_dropped_items "$rcon_port" + sleep 1 + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#36[[:space:]]*: x[0-9]+[[:space:]]+Dirt' "drop all left Dirt in player slot 36" || return 1 + + run_rcon "$rcon_port" "gamemode creative $username" || return 1 + run_rcon_any "$rcon_port" "tp $username 1.5 80 2.5" "tp $username 1 80 2" || true + sleep 2 + send_mcc_command "$session" "$log_file" "inventory creativegive 36 Diamond 16" 1 "$block_file" + send_mcc_command "$session" "$log_file" "inventory creativegive 37 GoldIngot 7" 1 "$block_file" + send_mcc_command "$session" "$log_file" "changeslot 9" 1 "$block_file" + run_rcon "$rcon_port" "gamemode survival $username" || return 1 + sleep 1 + open_chest "$session" "$log_file" "$block_file" || { FAIL_DETAIL="chest did not open"; return 1; } + + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#54[[:space:]]*: x16[[:space:]]+Diamond' "container list did not mirror player slot 36 as chest slot 54" || return 1 + assert_contains "$LAST_BLOCK" '#55[[:space:]]*: x7[[:space:]]+Gold[[:space:]]+Ingot' "container list did not mirror player slot 37 as chest slot 55" || return 1 + + send_mcc_command "$session" "$log_file" "inventory container click 54 ShiftClick" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#0[[:space:]]*: x16[[:space:]]+Diamond' "container shift-click did not move Diamond to chest slot 0" || return 1 + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#36[[:space:]]*: x16[[:space:]]+Diamond' "issue case failed: player slot 36 still showed shifted Diamond" || return 1 + + send_mcc_command "$session" "$log_file" "inventory container click 55 ShiftRightClick" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#1[[:space:]]*: x7[[:space:]]+Gold[[:space:]]+Ingot' "container shift-right-click did not move GoldIngot to chest slot 1" || return 1 + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#37[[:space:]]*: x7[[:space:]]+Gold[[:space:]]+Ingot' "player slot 37 still showed shifted GoldIngot" || return 1 + + send_mcc_command "$session" "$log_file" "inventory search GoldIngot 7" 1 "$block_file" + assert_contains "$LAST_BLOCK" 'Gold[[:space:]]+Ingot' "inventory search did not find GoldIngot after moving to container" || return 1 + + send_mcc_command "$session" "$log_file" "inventory container click 0 right" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#0[[:space:]]*: x8[[:space:]]+Diamond' "container right-click did not halve chest stack" || return 1 + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#-1[[:space:]]*: x8[[:space:]]+Diamond' "container right-click did not put half stack on cursor" || return 1 + + send_mcc_command "$session" "$log_file" "inventory container click 2 right" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#2[[:space:]]*: x1[[:space:]]+Diamond' "container right-click did not place one item into empty slot 2" || return 1 + send_mcc_command "$session" "$log_file" "inventory container click 2 left" 2 "$block_file" + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#2[[:space:]]*: x8[[:space:]]+Diamond' "container left-click did not merge cursor into slot 2" || return 1 + send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#-1[[:space:]]*: x[0-9]+[[:space:]]+Diamond' "container left-click merge left Diamond on cursor" || return 1 + + send_mcc_command "$session" "$log_file" "inventory container drop 2" 0.2 "$block_file" + clear_dropped_items "$rcon_port" + sleep 1 + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_contains "$LAST_BLOCK" '#2[[:space:]]*: x7[[:space:]]+Diamond' "container single drop did not decrement chest slot 2" || return 1 + send_mcc_command "$session" "$log_file" "inventory container drop 2 all" 0.2 "$block_file" + clear_dropped_items "$rcon_port" + sleep 1 + send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#2[[:space:]]*: x[0-9]+[[:space:]]+Diamond' "container drop all left Diamond in chest slot 2" || return 1 + + send_mcc_command "$session" "$log_file" "inventory container close" 1 "$block_file" + send_mcc_command "$session" "$log_file" "inventory inventories" 1 "$block_file" + assert_not_contains "$LAST_BLOCK" '#1[[:space:]]*-' "container close left inventory #1 visible" || return 1 + + run_rcon "$rcon_port" "gamemode creative $username" || return 1 + sleep 1 + send_mcc_command "$session" "$log_file" "inventory creativegive 37 Emerald 1" 1 "$block_file" + send_mcc_command "$session" "$log_file" "inventory player click 37 middle" 1 "$block_file" + assert_contains "$LAST_BLOCK" 'middle' "middle-click command path did not execute" || return 1 + + assert_no_runtime_crash "$log_file" || return 1 +} + +run_one_version() { + local version="$1" + local target + target="$(server_target_for "$version")" + local safe session username version_dir cfg log_file block_file mcc_root rcon_port + safe="$(sanitize_version "$version")" + session="inventory-full-$safe" + username="InvF${safe//_/}" + username="${username:0:16}" + version_dir="$RUN_ROOT/$version" + cfg="$version_dir/MinecraftClient.ini" + log_file="/tmp/mcc-debug/$session/mcc-debug.log" + block_file="$version_dir/command-blocks.log" + mkdir -p "$version_dir" "/tmp/mcc-debug/$session" + : > "$log_file" + : > "$block_file" + + echo "== inventory $version ==" + bash "$SCRIPT_DIR/ensure_offline_server.sh" "$target" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "$version" "server setup failed" "$log_file" >> "$SUMMARY"; return 1; } + mc-start "$target" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "$version" "server start failed" "$log_file" >> "$SUMMARY"; return 1; } + wait_for_server_ready "$target" >/dev/null || true + rcon_port="$(rcon_port_for "$target")" + + bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$cfg" "$version" "$username" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "$version" "config setup failed" "$log_file" >> "$SUMMARY"; mc-stop "$target" --confirm >/dev/null 2>&1 || true; return 1; } + sed -i 's#^Server = .*#Server = { Host = "localhost", Port = 25565 }#' "$cfg" + FAIL_DETAIL="" + setup_world "$rcon_port" || { printf '%s\tFAIL\t%s\t%s\n' "$version" "${FAIL_DETAIL:-world setup failed}" "$log_file" >> "$SUMMARY"; mc-stop "$target" --confirm >/dev/null 2>&1 || true; return 1; } + + mcc_root="$(dirname "$cfg")" + mkdir -p "/tmp/mcc-debug/$session" + local input_file="/tmp/mcc-debug/$session/mcc_input.txt" + local pid_file="/tmp/mcc-debug/$session/mcc.pid" + : > "$input_file" + ( + cd "$mcc_root" || exit 1 + printf '%s\n' "$$" > "$pid_file" + exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE="$input_file" dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build > "$log_file" 2>&1 + ) & + local mcc_pid=$! + printf '%s\n' "$mcc_pid" > "$pid_file" + + local ok=0 + if ! wait_for_file_pattern_local "$log_file" "Server was successfully joined" 40; then + FAIL_DETAIL="MCC did not join server" + ok=1 + else + setup_player "$rcon_port" "$username" || { FAIL_DETAIL="player setup failed after join"; ok=1; } + setup_area "$rcon_port" || { ok=1; } + setup_player "$rcon_port" "$username" || { FAIL_DETAIL="player setup failed after area setup"; ok=1; } + sleep 2 + if [[ -z "${FAIL_DETAIL:-}" ]]; then + FAIL_DETAIL="" + run_inventory_sequence "$version" "$rcon_port" "$session" "$username" "$log_file" "$block_file" + ok=$? + fi + fi + + kill "$mcc_pid" >/dev/null 2>&1 || true + wait "$mcc_pid" >/dev/null 2>&1 || true + mc-stop "$target" --confirm >/dev/null 2>&1 || true + wait_for_server_stop "$target" >/dev/null 2>&1 || true + + if [[ "$ok" -eq 0 ]]; then + printf '%s\tPASS\tfull inventory command/API sweep\t%s\n' "$version" "$log_file" >> "$SUMMARY" + echo "PASS $version" + return 0 + fi + + printf '%s\tFAIL\t%s\t%s\n' "$version" "${FAIL_DETAIL:-unknown failure}" "$log_file" >> "$SUMMARY" + echo "${FAIL_DETAIL:-unknown failure}" >&2 + echo "FAIL $version" + return 1 +} + +write_issue3112_script() { + local script_path="$1" + cat > "$script_path" <<'CS' +//MCCScript 1.0 + +MCC.LoadBot(new Issue3112InventoryReproBot()); + +//MCCScript Extensions + +public class Issue3112InventoryReproBot : ChatBot +{ + private int ticks; + private int phase; + private bool finished; + + public override void AfterGameJoined() + { + ticks = 0; + phase = 0; + finished = false; + LogToConsole("ISSUE3112_REPRO_START"); + } + + public override void Update() + { + if (finished) + return; + + ticks++; + + if (phase == 0 && ticks >= 20) + { + PerformInternalCommand("inventory creativedelete 36"); + PerformInternalCommand("inventory creativegive 36 DiamondOre 1"); + PerformInternalCommand("changeslot 9"); + phase = 1; + ticks = 0; + return; + } + + if (phase == 1 && ticks >= 20) + { + if (!PlayerHasOre()) + { + Fail("missing ore before container click"); + return; + } + + PerformInternalCommand("useblock 1 80 0"); + phase = 2; + ticks = 0; + return; + } + + if (phase == 2 && ticks >= 40) + { + if (!GetInventories().ContainsKey(1)) + { + Fail("container did not open"); + return; + } + + PerformInternalCommand("inventory container click 54 ShiftClick"); + phase = 3; + ticks = 0; + return; + } + + if (phase == 3 && ticks >= 40) + { + if (PlayerHasOre()) + Fail("ISSUE3112_REPRO_STALE_FAIL"); + else + Pass(); + } + } + + private bool PlayerHasOre() + { + foreach (var item in GetPlayerInventory().Items.Values) + { + if (item.Type == ItemType.DiamondOre && item.Count > 0) + return true; + } + + return false; + } + + private void Pass() + { + finished = true; + LogToConsole("ISSUE3112_REPRO_PASS"); + PerformInternalCommand("inventory container close"); + } + + private void Fail(string reason) + { + finished = true; + LogToConsole(reason); + } +} +CS +} + +run_issue3112_repro() { + local version="$1" + local target + target="$(server_target_for "$version")" + local safe session username version_dir cfg log_file script_file mcc_root rcon_port + safe="$(sanitize_version "$version")" + session="issue3112-$safe" + username="Iss3112${safe//_/}" + username="${username:0:16}" + version_dir="$RUN_ROOT/issue3112-$version" + cfg="$version_dir/MinecraftClient.ini" + log_file="/tmp/mcc-debug/$session/mcc-debug.log" + script_file="$version_dir/issue3112_repro.cs" + mkdir -p "$version_dir" "/tmp/mcc-debug/$session" + : > "$log_file" + write_issue3112_script "$script_file" + + echo "== issue3112 $version ==" + bash "$SCRIPT_DIR/ensure_offline_server.sh" "$target" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "server setup failed" "$log_file" >> "$SUMMARY"; return 1; } + mc-start "$target" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "server start failed" "$log_file" >> "$SUMMARY"; return 1; } + wait_for_server_ready "$target" >/dev/null || true + rcon_port="$(rcon_port_for "$target")" + + bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$cfg" "$version" "$username" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "config setup failed" "$log_file" >> "$SUMMARY"; mc-stop "$target" --confirm >/dev/null 2>&1 || true; return 1; } + sed -i 's#^Server = .*#Server = { Host = "localhost", Port = 25565 }#' "$cfg" + FAIL_DETAIL="" + setup_world "$rcon_port" || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "${FAIL_DETAIL:-world setup failed}" "$log_file" >> "$SUMMARY"; mc-stop "$target" --confirm >/dev/null 2>&1 || true; return 1; } + + mcc_root="$(dirname "$cfg")" + local input_file="/tmp/mcc-debug/$session/mcc_input.txt" + local pid_file="/tmp/mcc-debug/$session/mcc.pid" + : > "$input_file" + ( + cd "$mcc_root" || exit 1 + printf '%s\n' "$$" > "$pid_file" + exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE="$input_file" dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build > "$log_file" 2>&1 + ) & + local mcc_pid=$! + printf '%s\n' "$mcc_pid" > "$pid_file" + + local ok=0 + if ! wait_for_file_pattern_local "$log_file" "Server was successfully joined" 40; then + FAIL_DETAIL="MCC did not join server" + ok=1 + else + setup_player "$rcon_port" "$username" || { FAIL_DETAIL="player setup failed after join"; ok=1; } + setup_area "$rcon_port" || { ok=1; } + setup_player "$rcon_port" "$username" || { FAIL_DETAIL="player setup failed after area setup"; ok=1; } + sleep 2 + if [[ -z "${FAIL_DETAIL:-}" ]]; then + mcc-cmd --session "$session" "script $script_file" >/dev/null + if wait_for_file_pattern_local "$log_file" "ISSUE3112_REPRO_PASS" 30; then + ok=0 + else + FAIL_DETAIL="Issue #3112 repro did not pass" + ok=1 + fi + fi + fi + + kill "$mcc_pid" >/dev/null 2>&1 || true + wait "$mcc_pid" >/dev/null 2>&1 || true + mc-stop "$target" --confirm >/dev/null 2>&1 || true + wait_for_server_stop "$target" >/dev/null 2>&1 || true + + if [[ "$ok" -eq 0 ]]; then + printf '%s\tPASS\tIssue #3112 mirrored inventory script\t%s\n' "issue3112-$version" "$log_file" >> "$SUMMARY" + echo "PASS issue3112 $version" + return 0 + fi + + printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "${FAIL_DETAIL:-unknown failure}" "$log_file" >> "$SUMMARY" + echo "${FAIL_DETAIL:-unknown failure}" >&2 + echo "FAIL issue3112 $version" + return 1 +} + +overall=0 +for version in $VERSIONS; do + if ! run_one_version "$version"; then + overall=1 + [[ "$STOP_ON_FAIL" == "1" ]] && break + fi +done + +echo "SUMMARY=$SUMMARY" + +if [[ "$RUN_ISSUE_SCRIPT" == "1" && "$overall" -eq 0 ]]; then + if ! run_issue3112_repro "$ISSUE_VERSION"; then + overall=1 + fi + echo "SUMMARY=$SUMMARY" +fi + +exit "$overall" From b10bd81bee4185964bddd277eed3e04fb1cb9e0d Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 5 Jun 2026 21:00:39 +0200 Subject: [PATCH 3/4] Keep inventory sweep generalized --- .skills/mcc-integration-testing/SKILL.md | 8 +- tools/README.md | 3 - tools/run-inventory-full-sweep.sh | 206 +---------------------- 3 files changed, 3 insertions(+), 214 deletions(-) diff --git a/.skills/mcc-integration-testing/SKILL.md b/.skills/mcc-integration-testing/SKILL.md index 6316e0b8..e7c0831d 100644 --- a/.skills/mcc-integration-testing/SKILL.md +++ b/.skills/mcc-integration-testing/SKILL.md @@ -138,12 +138,6 @@ Default coverage includes: - creative middle-click command path - log scan for packet parse failures, queue-empty crashes, unhandled exceptions, and disconnects -Run the Issue #3112 repro after a passing sweep: - -```bash -tools/run-inventory-full-sweep.sh --versions "1.20.4" --run-issue-script -``` - The script writes `summary.tsv` under `RUN_ROOT` and per-version logs under `/tmp/mcc-debug/inventory-full-/mcc-debug.log`. When a matrix has existing PASS rows, do not rerun them unless a later code change affects that row or the user asks for a full rerun. Derive remaining rows from summaries: @@ -203,7 +197,7 @@ Optionally override the login name with the fourth argument to the config helper - `tools/run-creative-e2e.sh` - ordered creative-mode E2E regression scenario - `tools/run-inventory-full-sweep.sh` - - full inventory command/API sweep across one or more versions, with optional Issue #3112 MCCScript repro + - full inventory command/API sweep across one or more versions ## Evidence Discipline diff --git a/tools/README.md b/tools/README.md index 87a65480..c69d3dd3 100644 --- a/tools/README.md +++ b/tools/README.md @@ -36,9 +36,6 @@ tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11" # Full default major-version sweep tools/run-inventory-full-sweep.sh - -# Run the Issue #3112 mirrored-player-inventory script after a passing sweep -tools/run-inventory-full-sweep.sh --versions "1.20.4" --run-issue-script ``` Useful environment overrides: diff --git a/tools/run-inventory-full-sweep.sh b/tools/run-inventory-full-sweep.sh index ea98846e..397edd3c 100755 --- a/tools/run-inventory-full-sweep.sh +++ b/tools/run-inventory-full-sweep.sh @@ -11,8 +11,6 @@ REPO_ROOT="$(cd -P "$(dirname "$SCRIPT_SELF")/.." >/dev/null 2>&1 && pwd)" SCRIPT_DIR="$REPO_ROOT/.skills/mcc-integration-testing/scripts" RUN_ROOT="${RUN_ROOT:-/tmp/mcc-inventory-full-sweep/$(date +%Y%m%d-%H%M%S)}" VERSIONS="${VERSIONS_OVERRIDE:-1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.18 1.19 1.20 1.21 26.1}" -RUN_ISSUE_SCRIPT="${RUN_ISSUE_SCRIPT:-0}" -ISSUE_VERSION="${ISSUE_VERSION:-1.20.4}" STOP_ON_FAIL="${STOP_ON_FAIL:-1}" usage() { @@ -24,19 +22,15 @@ The matrix is sequential because mc-* tmux sessions are shared state. Options: --versions "1.20.4 1.21.11" Space-separated versions to test. - --run-issue-script Run the Issue #3112 MCCScript repro after a passing sweep. - --issue-version VERSION Version for the Issue #3112 repro. Default: 1.20.4. --keep-going Continue after failures. --stop-on-fail Stop on first failure. Default. -h, --help Show this help. Environment overrides: - VERSIONS_OVERRIDE, RUN_ROOT, RUN_ISSUE_SCRIPT, ISSUE_VERSION, STOP_ON_FAIL, - MCC_SERVERS. + VERSIONS_OVERRIDE, RUN_ROOT, STOP_ON_FAIL, MCC_SERVERS. Examples: tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11" - RUN_ISSUE_SCRIPT=1 tools/run-inventory-full-sweep.sh --versions "1.20.4" USAGE } @@ -46,14 +40,6 @@ while [[ $# -gt 0 ]]; do VERSIONS="$2" shift 2 ;; - --run-issue-script) - RUN_ISSUE_SCRIPT=1 - shift - ;; - --issue-version) - ISSUE_VERSION="$2" - shift 2 - ;; --keep-going) STOP_ON_FAIL=0 shift @@ -348,7 +334,7 @@ run_inventory_sequence() { send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" assert_contains "$LAST_BLOCK" '#0[[:space:]]*: x16[[:space:]]+Diamond' "container shift-click did not move Diamond to chest slot 0" || return 1 send_mcc_command "$session" "$log_file" "inventory player list" 1 "$block_file" - assert_not_contains "$LAST_BLOCK" '#36[[:space:]]*: x16[[:space:]]+Diamond' "issue case failed: player slot 36 still showed shifted Diamond" || return 1 + assert_not_contains "$LAST_BLOCK" '#36[[:space:]]*: x16[[:space:]]+Diamond' "mirrored player slot 36 still showed shifted Diamond" || return 1 send_mcc_command "$session" "$log_file" "inventory container click 55 ShiftRightClick" 2 "$block_file" send_mcc_command "$session" "$log_file" "inventory container list" 1 "$block_file" @@ -472,187 +458,6 @@ run_one_version() { return 1 } -write_issue3112_script() { - local script_path="$1" - cat > "$script_path" <<'CS' -//MCCScript 1.0 - -MCC.LoadBot(new Issue3112InventoryReproBot()); - -//MCCScript Extensions - -public class Issue3112InventoryReproBot : ChatBot -{ - private int ticks; - private int phase; - private bool finished; - - public override void AfterGameJoined() - { - ticks = 0; - phase = 0; - finished = false; - LogToConsole("ISSUE3112_REPRO_START"); - } - - public override void Update() - { - if (finished) - return; - - ticks++; - - if (phase == 0 && ticks >= 20) - { - PerformInternalCommand("inventory creativedelete 36"); - PerformInternalCommand("inventory creativegive 36 DiamondOre 1"); - PerformInternalCommand("changeslot 9"); - phase = 1; - ticks = 0; - return; - } - - if (phase == 1 && ticks >= 20) - { - if (!PlayerHasOre()) - { - Fail("missing ore before container click"); - return; - } - - PerformInternalCommand("useblock 1 80 0"); - phase = 2; - ticks = 0; - return; - } - - if (phase == 2 && ticks >= 40) - { - if (!GetInventories().ContainsKey(1)) - { - Fail("container did not open"); - return; - } - - PerformInternalCommand("inventory container click 54 ShiftClick"); - phase = 3; - ticks = 0; - return; - } - - if (phase == 3 && ticks >= 40) - { - if (PlayerHasOre()) - Fail("ISSUE3112_REPRO_STALE_FAIL"); - else - Pass(); - } - } - - private bool PlayerHasOre() - { - foreach (var item in GetPlayerInventory().Items.Values) - { - if (item.Type == ItemType.DiamondOre && item.Count > 0) - return true; - } - - return false; - } - - private void Pass() - { - finished = true; - LogToConsole("ISSUE3112_REPRO_PASS"); - PerformInternalCommand("inventory container close"); - } - - private void Fail(string reason) - { - finished = true; - LogToConsole(reason); - } -} -CS -} - -run_issue3112_repro() { - local version="$1" - local target - target="$(server_target_for "$version")" - local safe session username version_dir cfg log_file script_file mcc_root rcon_port - safe="$(sanitize_version "$version")" - session="issue3112-$safe" - username="Iss3112${safe//_/}" - username="${username:0:16}" - version_dir="$RUN_ROOT/issue3112-$version" - cfg="$version_dir/MinecraftClient.ini" - log_file="/tmp/mcc-debug/$session/mcc-debug.log" - script_file="$version_dir/issue3112_repro.cs" - mkdir -p "$version_dir" "/tmp/mcc-debug/$session" - : > "$log_file" - write_issue3112_script "$script_file" - - echo "== issue3112 $version ==" - bash "$SCRIPT_DIR/ensure_offline_server.sh" "$target" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "server setup failed" "$log_file" >> "$SUMMARY"; return 1; } - mc-start "$target" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "server start failed" "$log_file" >> "$SUMMARY"; return 1; } - wait_for_server_ready "$target" >/dev/null || true - rcon_port="$(rcon_port_for "$target")" - - bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$cfg" "$version" "$username" >/dev/null || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "config setup failed" "$log_file" >> "$SUMMARY"; mc-stop "$target" --confirm >/dev/null 2>&1 || true; return 1; } - sed -i 's#^Server = .*#Server = { Host = "localhost", Port = 25565 }#' "$cfg" - FAIL_DETAIL="" - setup_world "$rcon_port" || { printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "${FAIL_DETAIL:-world setup failed}" "$log_file" >> "$SUMMARY"; mc-stop "$target" --confirm >/dev/null 2>&1 || true; return 1; } - - mcc_root="$(dirname "$cfg")" - local input_file="/tmp/mcc-debug/$session/mcc_input.txt" - local pid_file="/tmp/mcc-debug/$session/mcc.pid" - : > "$input_file" - ( - cd "$mcc_root" || exit 1 - printf '%s\n' "$$" > "$pid_file" - exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE="$input_file" dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build > "$log_file" 2>&1 - ) & - local mcc_pid=$! - printf '%s\n' "$mcc_pid" > "$pid_file" - - local ok=0 - if ! wait_for_file_pattern_local "$log_file" "Server was successfully joined" 40; then - FAIL_DETAIL="MCC did not join server" - ok=1 - else - setup_player "$rcon_port" "$username" || { FAIL_DETAIL="player setup failed after join"; ok=1; } - setup_area "$rcon_port" || { ok=1; } - setup_player "$rcon_port" "$username" || { FAIL_DETAIL="player setup failed after area setup"; ok=1; } - sleep 2 - if [[ -z "${FAIL_DETAIL:-}" ]]; then - mcc-cmd --session "$session" "script $script_file" >/dev/null - if wait_for_file_pattern_local "$log_file" "ISSUE3112_REPRO_PASS" 30; then - ok=0 - else - FAIL_DETAIL="Issue #3112 repro did not pass" - ok=1 - fi - fi - fi - - kill "$mcc_pid" >/dev/null 2>&1 || true - wait "$mcc_pid" >/dev/null 2>&1 || true - mc-stop "$target" --confirm >/dev/null 2>&1 || true - wait_for_server_stop "$target" >/dev/null 2>&1 || true - - if [[ "$ok" -eq 0 ]]; then - printf '%s\tPASS\tIssue #3112 mirrored inventory script\t%s\n' "issue3112-$version" "$log_file" >> "$SUMMARY" - echo "PASS issue3112 $version" - return 0 - fi - - printf '%s\tFAIL\t%s\t%s\n' "issue3112-$version" "${FAIL_DETAIL:-unknown failure}" "$log_file" >> "$SUMMARY" - echo "${FAIL_DETAIL:-unknown failure}" >&2 - echo "FAIL issue3112 $version" - return 1 -} - overall=0 for version in $VERSIONS; do if ! run_one_version "$version"; then @@ -663,11 +468,4 @@ done echo "SUMMARY=$SUMMARY" -if [[ "$RUN_ISSUE_SCRIPT" == "1" && "$overall" -eq 0 ]]; then - if ! run_issue3112_repro "$ISSUE_VERSION"; then - overall=1 - fi - echo "SUMMARY=$SUMMARY" -fi - exit "$overall" From 82555bd8233c969266b7cbe52559adfd51e37768 Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 5 Jun 2026 21:17:09 +0200 Subject: [PATCH 4/4] Keep version adaptation skill generalized --- .skills/mcc-version-adaptation/SKILL.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.skills/mcc-version-adaptation/SKILL.md b/.skills/mcc-version-adaptation/SKILL.md index c3be7c12..54b1c782 100644 --- a/.skills/mcc-version-adaptation/SKILL.md +++ b/.skills/mcc-version-adaptation/SKILL.md @@ -194,13 +194,6 @@ PY Do the same for `SERVERBOUND_TEMPLATE`. Clientbound and serverbound can change independently. Do not inherit a newer palette just because one side looks similar. For example, `1.21.11` used the same play packet order as `1.21.9/1.21.10` for the tested inventory path, while `26.1` had additional shifts. -Known packet lessons: - -- `1.9`, `1.9.1`, and `1.9.2` need their own packet palette. They are not safe to route through the later 1.9.x palette. -- Pure `1.19` serverbound IDs differ from later 1.19.x. Do not put `MessageAcknowledgment` at `0x03`; pure 1.19 has `ChatCommand` at `0x03`, `ChatMessage` at `0x04`, and `ChatPreview` at `0x05`. -- A wrong packet palette often appears as unrelated inventory failure: creative give/delete disconnects, `Queue empty`, or `Failed to process incoming packet`. -- Game event reason `3` is `CHANGE_GAME_MODE`. If RCON changed the player to creative but MCC still refuses creative inventory commands, inspect `ChangeGameState` handling. - ## Step 5: Check Variant Encoding Changes For entity types that use variant serializers (Cat, Wolf, Frog, Painting), check if the codec changed between versions by inspecting: @@ -230,17 +223,6 @@ Compare key packet codec classes between versions. Known changes: When in doubt, compare the relevant packet class (e.g. `ClientboundAddEntityPacket.java`) between versions. -## Step 7.1: Check JoinGame and Respawn Formats - -JoinGame and Respawn are high-risk because dimension fields changed several times: - -- `1.16` and `1.16.1`: dimension type/name handling uses string identifiers in places where later versions do not. -- `1.16.2` through `1.18.2`: dimension type can be an NBT compound in JoinGame/Respawn. -- `1.19+`: dimension type commonly moves back to identifiers. -- `1.20.6+`: registry-driven IDs appear in more fields. - -When a version joins but terrain, inventory, or later packets look misaligned, inspect JoinGame/Respawn first. A single wrong dimension-field read leaves unread bytes in the packet and can make the next packet look broken. - ## Step 8: Update Block Collision Shapes (Physics Engine) MCC's physics engine uses block collision shape data from PrismarineJS `minecraft-data` to perform accurate AABB collision detection (stored in `MinecraftClient/Physics/BlockShapeData.json`, embedded as a resource).