mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
bugfix: Fixed crashing and bugs on 1.9, 1.9.1, 1.9.2, 1.16.2, 1.21.10 and Player Inventory
bugfix: Fixed crashing and bugs on 1.9, 1.9.1, 1.9.2, 1.16.2, 1.21.10 and Player Inventory
This commit is contained in:
commit
5f0ca8837f
13 changed files with 905 additions and 62 deletions
|
|
@ -115,6 +115,37 @@ 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
|
||||
|
||||
The script writes `summary.tsv` under `RUN_ROOT` and per-version logs under `/tmp/mcc-debug/inventory-full-<version>/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 +196,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
|
||||
|
||||
## Evidence Discipline
|
||||
|
||||
|
|
@ -224,3 +257,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`.
|
||||
|
|
|
|||
|
|
@ -157,6 +157,43 @@ 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 "<none>"
|
||||
y = b[i] if i < len(b) else "<none>"
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,16 @@ namespace MinecraftClient.Mapping
|
|||
/// <param name="registryCodec">Registry Codec nbt data</param>
|
||||
public static void StoreDimensionList(Dictionary<string, object> registryCodec)
|
||||
{
|
||||
var dimensionListNbt = (object[])(((Dictionary<string, object>)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<string, object>)dimensionTypeRegistry)["value"]);
|
||||
foreach (var (dimensionName, dimensionType) in from Dictionary<string, object> dimensionNbt in dimensionListNbt
|
||||
let dimensionName = (string)dimensionNbt["name"]
|
||||
let dimensionType = (Dictionary<string, object>)dimensionNbt["element"]
|
||||
|
|
@ -333,11 +342,40 @@ namespace MinecraftClient.Mapping
|
|||
return; // Dimension found with prefixed name
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string unprefixedName = name["minecraft:".Length..];
|
||||
if (dimensionList.TryGetValue(unprefixedName, out dimension))
|
||||
{
|
||||
curDimension = dimension;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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<string, object>());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Click a slot in the specified window
|
||||
/// </summary>
|
||||
|
|
@ -2748,6 +2832,8 @@ namespace MinecraftClient
|
|||
changedSlots.Add(new Tuple<short, Item?>((short)slotId, null));
|
||||
break;
|
||||
}
|
||||
|
||||
SyncPlayerInventorySlotsFromWindow(inventory);
|
||||
}
|
||||
|
||||
return handler.SendWindowAction(windowId, slotId, action, item, changedSlots, inventories[windowId].StateID);
|
||||
|
|
@ -2764,7 +2850,16 @@ namespace MinecraftClient
|
|||
/// <returns>TRUE if item given successfully</returns>
|
||||
public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary<string, object>? 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;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -59,32 +59,7 @@
|
|||
<PackageReference Include="Telegram.Bot" Version="22.9.5.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="config\ChatBots\AutoLeaveOnLowHp.cs" />
|
||||
<Compile Remove="config\ChatBots\AutoLook.cs" />
|
||||
<Compile Remove="config\ChatBots\AutoTree.cs" />
|
||||
<Compile Remove="config\ChatBots\CobblestoneMiner.cs" />
|
||||
<Compile Remove="config\ChatBots\DiscordWebhook.cs" />
|
||||
<Compile Remove="config\ChatBots\EntityCount.cs" />
|
||||
<Compile Remove="config\ChatBots\OreMiner.cs" />
|
||||
<Compile Remove="config\ChatBots\PayKassa.cs" />
|
||||
<Compile Remove="config\ChatBots\QIWIAPI.cs" />
|
||||
<Compile Remove="config\ChatBots\SugarCaneMiner.cs" />
|
||||
<Compile Remove="config\ChatBots\TreeFarmer.cs" />
|
||||
<Compile Remove="config\ChatBots\VkMessager.cs" />
|
||||
<Compile Remove="config\ChatBots\WebSocketBot.cs" />
|
||||
<Compile Remove="config\sample-script-extended.cs" />
|
||||
<Compile Remove="config\sample-script-packet-capture.cs" />
|
||||
<Compile Remove="config\sample-script-pm-forwarder.cs" />
|
||||
<Compile Remove="config\sample-script-random-command.cs" />
|
||||
<Compile Remove="config\sample-script-tick-counter.cs" />
|
||||
<Compile Remove="config\sample-script-with-chatbot.cs" />
|
||||
<Compile Remove="config\sample-script-with-http-request.cs" />
|
||||
<Compile Remove="config\sample-script-with-task.cs" />
|
||||
<Compile Remove="config\sample-script-with-world-access.cs" />
|
||||
<Compile Remove="config\sample-script-packet-capture.cs" />
|
||||
<Compile Remove="config\sample-script.cs" />
|
||||
<Compile Remove="config\ChatBots\MineCube.cs" />
|
||||
<Compile Remove="config\ChatBots\SugarCaneFarmer.cs" />
|
||||
<Compile Remove="config\**\*.cs" />
|
||||
<Compile Remove="Mapping\VillagerInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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<int, PacketTypesIn> GetListIn() => typeIn;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Protocol.Handlers.PacketPalettes
|
||||
{
|
||||
public class PacketPalette19 : PacketTypePalette
|
||||
{
|
||||
private readonly Dictionary<int, PacketTypesIn> 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<int, PacketTypesOut> 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<int, PacketTypesIn> GetListIn() => typeIn;
|
||||
protected override Dictionary<int, PacketTypesOut> GetListOut() => typeOut;
|
||||
protected override Dictionary<int, ConfigurationPacketTypesIn> GetConfigurationListIn() => new();
|
||||
protected override Dictionary<int, ConfigurationPacketTypesOut> GetConfigurationListOut() => new();
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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, //
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -841,7 +842,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
packetData); // Dimension Type: NBT Tag Compound
|
||||
break;
|
||||
default:
|
||||
dataTypes.ReadNextString(packetData);
|
||||
dimensionTypeName = dataTypes.ReadNextString(packetData);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1413,7 +1414,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
dataTypes.ReadNextNbt(packetData); // Dimension Type: NBT Tag Compound
|
||||
break;
|
||||
default:
|
||||
dataTypes.ReadNextString(packetData);
|
||||
dimensionTypeNameRespawn = dataTypes.ReadNextString(packetData);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -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<string, object>? 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
|
||||
|
|
|
|||
|
|
@ -26,6 +26,29 @@ 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
|
||||
```
|
||||
|
||||
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-<version>/mcc-debug.log`, and command output blocks are saved next to the summary.
|
||||
|
||||
### tmpfs build mode
|
||||
|
||||
```bash
|
||||
|
|
|
|||
471
tools/run-inventory-full-sweep.sh
Executable file
471
tools/run-inventory-full-sweep.sh
Executable file
|
|
@ -0,0 +1,471 @@
|
|||
#!/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}"
|
||||
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.
|
||||
--keep-going Continue after failures.
|
||||
--stop-on-fail Stop on first failure. Default.
|
||||
-h, --help Show this help.
|
||||
|
||||
Environment overrides:
|
||||
VERSIONS_OVERRIDE, RUN_ROOT, STOP_ON_FAIL, MCC_SERVERS.
|
||||
|
||||
Examples:
|
||||
tools/run-inventory-full-sweep.sh --versions "1.21.10 1.21.11"
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--versions)
|
||||
VERSIONS="$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' "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"
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
exit "$overall"
|
||||
Loading…
Add table
Add a link
Reference in a new issue