2026-06-05 20:57:49 +02:00
#!/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:
2026-06-05 21:00:39 +02:00
VERSIONS_OVERRIDE, RUN_ROOT, STOP_ON_FAIL, MCC_SERVERS.
2026-06-05 20:57:49 +02:00
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 "
2026-06-05 21:00:39 +02:00
assert_not_contains " $LAST_BLOCK " '#36[[:space:]]*: x16[[:space:]]+Diamond' "mirrored player slot 36 still showed shifted Diamond" || return 1
2026-06-05 20:57:49 +02:00
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 "