Add reusable inventory sweep tooling

This commit is contained in:
Anon 2026-06-05 20:57:49 +02:00
parent c98c2a8f83
commit 5545aabb60
4 changed files with 797 additions and 0 deletions

View file

@ -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-<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 +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`.

View file

@ -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 "<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.
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).

View file

@ -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-<version>/mcc-debug.log`, and command output blocks are saved next to the summary.
### tmpfs build mode
```bash

673
tools/run-inventory-full-sweep.sh Executable file
View file

@ -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"