diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 5e67687d..1e348445 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -6,6 +6,14 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + VERSION="${1:-1.21.11-Vanilla}" SERVER_DIR="${MCC_SERVERS:?}/$VERSION" PROPS_FILE="$SERVER_DIR/server.properties" @@ -49,6 +57,15 @@ wait_for_server_stop() { sleep 1 ((elapsed += 1)) done + + # Legacy servers can leave the tmux session around after stdin stop. + # Fall back to force-killing the session so the harness can continue. + mc-kill "$VERSION" >/dev/null 2>&1 || true + + if ! server_running; then + return 0 + fi + echo "Timed out waiting for $VERSION to stop" >&2 return 1 } @@ -58,7 +75,7 @@ upsert_property() { local value="$2" if grep -Eq "^${key}=" "$PROPS_FILE"; then - sed -i "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE" + sed_in_place "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE" else printf '%s=%s\n' "$key" "$value" >> "$PROPS_FILE" fi diff --git a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh index f36129fa..9eae53b3 100644 --- a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh +++ b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh @@ -1,6 +1,14 @@ #!/usr/bin/env bash set -euo pipefail +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + if [[ $# -lt 3 || $# -gt 4 ]]; then echo "Usage: $0 [login]" >&2 exit 1 @@ -28,7 +36,7 @@ fi cp "$TEMPLATE_INI" "$OUTPUT_INI" -sed -i \ +sed_in_place \ -e "s#^Account = .*#Account = { Login = \"$LOGIN_NAME\", Password = \"$PASSWORD_VALUE\" }#" \ -e "s#^AccountType = .*#AccountType = \"$ACCOUNT_TYPE\"#" \ -e "s#^MinecraftVersion = \"[^\"]*\"\\(.*\\)\$#MinecraftVersion = \"$MC_VERSION\"\\1#" \ diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh new file mode 100755 index 00000000..45629525 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" + +RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements/matrix" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +MATRIX_DIR="$RUN_ROOT/$RUN_ID" +RESULTS_TSV="$MATRIX_DIR/results.tsv" +BUILD_LOG="$MATRIX_DIR/build.log" +REPORT_MD="$MATRIX_DIR/report.md" +PRECHECK_TXT="$MATRIX_DIR/preflight.txt" + +mkdir -p "$MATRIX_DIR" + +write_row() { + local fields=("$@") + + while (( ${#fields[@]} < 14 )); do + fields+=("") + done + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${fields[5]}" "${fields[6]}" \ + "${fields[7]}" "${fields[8]}" "${fields[9]}" "${fields[10]}" "${fields[11]}" "${fields[12]}" \ + "${fields[13]}" >> "$RESULTS_TSV" +} + +resolve_server_dir() { + local version="$1" + local candidate + + for candidate in "$version" "$version-Vanilla"; do + if [[ -d "$MCC_SERVERS/$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +run_version() { + local version="$1" + local profile="$2" + local family="$3" + local server_dir="$4" + local summary_env + + if bash "$SCRIPT_DIR/run_achievements_test.sh" --no-build "$server_dir" "$version" "$profile"; then + : + fi + + summary_env="${TMPDIR:-/tmp}/mcc-achievements/$server_dir/latest/summary.env" + if [[ ! -f "$summary_env" ]]; then + write_row "$version" "$server_dir" "unknown" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "Summary file was not produced." "" "" "" + return + fi + + # shellcheck disable=SC1090 + source "$summary_env" + + write_row "$VERSION" "$SERVER_DIR" "$PORT" "$FAMILY" "$INITIAL_STATUS" "$GRANT_STATUS" "$REVOKE_STATUS" \ + "$API_STATUS" "$VERDICT" "$NOTE" "$RUN_DIR" "$MCC_LOG" "$COPIED_SERVER_LOG" "$COMMAND_LOG" +} + +{ + printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" + printf 'RUN_DIR=%s\n' "$MATRIX_DIR" + printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" +} > "$PRECHECK_TXT" + +printf 'Version\tServerDir\tPort\tFamily\tInitial\tGrant\tRevoke\tAPI\tVerdict\tNote\tRunDir\tMccLog\tServerLog\tCommandLog\n' > "$RESULTS_TSV" + +JAVA_OK="yes" +TMUX_OK="yes" +DOTNET_OK="yes" +BUILD_OK="yes" + +if ! command -v dotnet >/dev/null 2>&1; then + DOTNET_OK="no" +fi + +if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then + JAVA_OK="no" +fi + +if ! command -v tmux >/dev/null 2>&1; then + TMUX_OK="no" +fi + +if [[ "$DOTNET_OK" == "yes" ]]; then + if ! dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release > "$BUILD_LOG" 2>&1; then + BUILD_OK="no" + fi +else + : > "$BUILD_LOG" +fi + +{ + printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" + printf 'RUN_DIR=%s\n' "$MATRIX_DIR" + printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" + printf 'dotnet=%s\n' "$DOTNET_OK" + printf 'java=%s\n' "$JAVA_OK" + printf 'tmux=%s\n' "$TMUX_OK" + printf 'build=%s\n' "$BUILD_OK" +} > "$PRECHECK_TXT" + +while IFS='|' read -r version profile family; do + [[ -z "$version" ]] && continue + + if [[ "$DOTNET_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "dotnet is not available on PATH." + continue + fi + + if [[ "$BUILD_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "dotnet build failed. See $BUILD_LOG." + continue + fi + + if [[ "$JAVA_OK" != "yes" || "$TMUX_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "java or tmux is not available, so live server execution was blocked." + continue + fi + + if ! server_dir="$(resolve_server_dir "$version")"; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "⚠️ Partial" \ + "Server directory for $version was not found under $MCC_SERVERS." + continue + fi + + run_version "$version" "$profile" "$family" "$server_dir" +done <<'EOF' +1.8|legacy|Legacy 🧱 +1.11.2|legacy|Legacy 🧱 +1.12.2|modern|First advancements 🌱 +1.19.4|modern|Stable modern ✅ +1.20|modern|Telemetry edge 1 ⚠️ +1.20.2|modern|Telemetry edge 2 ⚠️ +1.20.4|modern|End of 1.20.x ⚠️ +1.20.6|modern|Post-1.20.6 🔧 +1.21.2|modern|1.21.2 family 🔧 +1.21.11|modern|showAdvancements 🆕 +26.1|modern|Latest supported 🚀 +EOF + +bash "$SCRIPT_DIR/summarize_achievements_matrix.sh" "$MATRIX_DIR" > "$REPORT_MD" +printf '%s\n' "$MATRIX_DIR" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh new file mode 100755 index 00000000..88c2b027 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -0,0 +1,443 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" + +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +usage() { + cat <<'EOF' +Usage: run_achievements_test.sh [--no-build] + +Examples: + .skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.8 1.8 legacy + .skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.21.11-Vanilla 1.21.11 modern +EOF +} + +DO_BUILD=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-build) DO_BUILD=false; shift ;; + --build) DO_BUILD=true; shift ;; + -h|--help) usage; exit 0 ;; + *) break ;; + esac +done + +if [[ $# -ne 3 ]]; then + usage >&2 + exit 1 +fi + +SERVER_DIR="$1" +MC_VERSION="$2" +PROFILE="$3" + +if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then + echo "Unsupported profile: $PROFILE" >&2 + exit 1 +fi + +RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +RUN_DIR="$RUN_ROOT/$SERVER_DIR/$RUN_ID" +LATEST_LINK="$RUN_ROOT/$SERVER_DIR/latest" +MCC_LOG="$RUN_DIR/mcc.log" +BUILD_LOG="$RUN_DIR/build.log" +SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log" +SERVER_FILE_LOG="$RUN_DIR/server-latest.log" +COMMAND_LOG="$RUN_DIR/commands.log" +SUMMARY_ENV="$RUN_DIR/summary.env" +PROBE_SCRIPT="$RUN_DIR/achievement_probe.cs" +CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" +INPUT_FILE="$REPO_ROOT/mcc_input.txt" +SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log" +TARGET_ID="minecraft:story/root" +TARGET_COMMAND_GRANT="advancement grant CursorBot only minecraft:story/root" +TARGET_COMMAND_REVOKE="advancement revoke CursorBot only minecraft:story/root" +TARGET_TYPE="Modern 🌱" +PORT="unknown" +MCC_PID="" + +INITIAL_STATUS="❌" +GRANT_STATUS="❌" +REVOKE_STATUS="❌" +API_STATUS="❌" +VERDICT="❌ Fail" +NOTE="Run did not complete." +EXECUTED="yes" + +if [[ "$PROFILE" == "legacy" ]]; then + TARGET_ID="achievement.openInventory" + TARGET_COMMAND_GRANT="achievement give achievement.openInventory CursorBot" + TARGET_COMMAND_REVOKE="achievement take achievement.openInventory CursorBot" + TARGET_TYPE="Legacy 🧱" +fi + +mkdir -p "$RUN_DIR" + +write_summary() { + { + printf 'VERSION=%q\n' "$MC_VERSION" + printf 'SERVER_DIR=%q\n' "$SERVER_DIR" + printf 'PROFILE=%q\n' "$PROFILE" + printf 'FAMILY=%q\n' "$TARGET_TYPE" + printf 'PORT=%q\n' "$PORT" + printf 'RUN_DIR=%q\n' "$RUN_DIR" + printf 'MCC_LOG=%q\n' "$MCC_LOG" + printf 'SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log" + printf 'SERVER_FILE_LOG=%q\n' "$SERVER_LOG_FILE" + printf 'SERVER_TMUX_LOG=%q\n' "$SERVER_TMUX_LOG" + printf 'COPIED_SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log" + printf 'COMMAND_LOG=%q\n' "$COMMAND_LOG" + printf 'SUMMARY_ENV=%q\n' "$SUMMARY_ENV" + printf 'TARGET_ID=%q\n' "$TARGET_ID" + printf 'INITIAL_STATUS=%q\n' "$INITIAL_STATUS" + printf 'GRANT_STATUS=%q\n' "$GRANT_STATUS" + printf 'REVOKE_STATUS=%q\n' "$REVOKE_STATUS" + printf 'API_STATUS=%q\n' "$API_STATUS" + printf 'VERDICT=%q\n' "$VERDICT" + printf 'NOTE=%q\n' "$NOTE" + printf 'EXECUTED=%q\n' "$EXECUTED" + } > "$SUMMARY_ENV" +} + +capture_server_logs() { + mc-log "$SERVER_DIR" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true + if [[ -f "$SERVER_LOG_FILE" ]]; then + cp "$SERVER_LOG_FILE" "$RUN_DIR/server-latest.log" 2>/dev/null || true + fi +} + +cleanup() { + capture_server_logs + + if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then + echo "quit" >> "$INPUT_FILE" 2>/dev/null || true + sleep 2 + kill "$MCC_PID" 2>/dev/null || true + wait "$MCC_PID" 2>/dev/null || true + fi + + mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true + ln -sfn "$RUN_DIR" "$LATEST_LINK" + write_summary +} +trap cleanup EXIT + +log_step() { + printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$COMMAND_LOG" +} + +fail() { + NOTE="$1" + VERDICT="❌ Fail" + exit 1 +} + +wait_for_file_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + local timeout="${4:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for: $description" >&2 + return 1 +} + +wait_for_server_ready() { + local timeout="${1:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for server readiness" >&2 + return 1 +} + +disable_noisy_bots() { + sed_in_place '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" +} + +ensure_root_config() { + if [[ -f "$REPO_ROOT/MinecraftClient.ini" ]]; then + return + fi + + ( + cd "$REPO_ROOT" + dotnet run --project MinecraftClient -c Release --no-build -- --help >/dev/null 2>&1 + ) +} + +write_probe_script() { + cat > "$PROBE_SCRIPT" < updated, IReadOnlyList removedIds, bool reset) + { + LogToConsole($"[ACH_TEST] event reset={reset} updated={updated.Count} removed={removedIds.Count}"); + DumpState("event"); + } + + private void DumpState(string origin) + { + Achievement[] all = GetAchievements(); + Achievement[] unlocked = GetUnlockedAchievements(); + Achievement[] locked = GetLockedAchievements(); + Achievement? target = null; + + foreach (Achievement entry in all) + { + if (entry.Id == TargetId) + { + target = entry; + break; + } + } + + string titleState = "missing"; + string completionState = "missing"; + + if (target is not null) + { + titleState = target.Title is null ? "null" : "present"; + completionState = target.IsCompleted ? "done" : "todo"; + } + + LogToConsole($"[ACH_TEST] snapshot origin={origin} all={all.Length} unlocked={unlocked.Length} locked={locked.Length}"); + LogToConsole($"[ACH_TEST] target_state origin={origin} id={TargetId} title={titleState} completed={completionState}"); + } +} +EOF +} + +run_server_command() { + local cmd="$1" + local attempt + + log_step "SERVER> $cmd" + for attempt in 1 2 3 4 5; do + if mc-rcon "$cmd" >/dev/null 2>&1; then + sleep 1 + return 0 + fi + sleep 1 + done + + fail "Server command failed: $cmd" +} + +run_mcc_command() { + local name="$1" + local cmd="$2" + local delay="${3:-2}" + local start_line=0 + local end_line=0 + + if [[ -f "$MCC_LOG" ]]; then + start_line="$(wc -l < "$MCC_LOG")" + fi + + log_step "MCC> $cmd" + echo "$cmd" >> "$INPUT_FILE" + sleep "$delay" + + if [[ -f "$MCC_LOG" ]]; then + end_line="$(wc -l < "$MCC_LOG")" + fi + + if (( end_line > start_line )); then + sed -n "$((start_line + 1)),$((end_line))p" "$MCC_LOG" > "$RUN_DIR/$name.mcc.log" + else + : > "$RUN_DIR/$name.mcc.log" + fi +} + +assert_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + + grep -Fq "$pattern" "$file" || fail "$description" +} + +if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then + fail "java was not found on PATH." +fi + +if ! command -v tmux >/dev/null 2>&1; then + fail "tmux was not found on PATH." +fi + +if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then + fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR" +fi + +PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")" + +ensure_root_config +"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR" +disable_noisy_bots +write_probe_script + +if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then + sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" +fi + +if $DO_BUILD; then + log_step "BUILD> dotnet build MinecraftClient.sln -c Release" + mcc-build > "$BUILD_LOG" 2>&1 || fail "dotnet build failed." +else + : > "$BUILD_LOG" +fi + +: > "$INPUT_FILE" +rm -f "$MCC_LOG" + +log_step "Starting server $SERVER_DIR on port $PORT" +mc-start "$SERVER_DIR" >/dev/null +wait_for_server_ready || fail "Server did not become ready." + +log_step "Starting MCC for $MC_VERSION" +( + cd "$REPO_ROOT" + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + CursorBot \ + - \ + "localhost:$PORT" \ + "--accounttype=mojang" \ + "--minecraftversion=$MC_VERSION" \ + "--terrainandmovements=true" \ + "--inventoryhandling=true" \ + "--entityhandling=true" \ + "--autorespawn=true" \ + "--debugmessages=true" \ + > "$MCC_LOG" 2>&1 +) & +MCC_PID=$! + +wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join." +wait_for_file_pattern "$SERVER_LOG_FILE" "CursorBot joined the game" "server join entry" 30 || fail "Server never logged the join." + +run_server_command "op CursorBot" +run_server_command "gamerule sendCommandFeedback true" +if [[ "$PROFILE" == "modern" ]]; then + run_server_command "gamerule logAdminCommands true" +fi +run_server_command "time set day" +run_server_command "weather clear" + +run_mcc_command "load_probe" "script $PROBE_SCRIPT" 3 +wait_for_file_pattern "$MCC_LOG" "[ACH_TEST] probe initialized" "probe startup" 30 || fail "Probe script did not initialize." + +run_mcc_command "baseline_debug" "debug state" 2 +run_mcc_command "baseline_all" "achievement" 2 +run_mcc_command "baseline_locked" "achievement locked" 2 +run_mcc_command "baseline_unlocked" "achievement unlocked" 2 + +run_server_command "$TARGET_COMMAND_GRANT" +sleep 3 +run_mcc_command "after_grant_all" "achievement" 2 +run_mcc_command "after_grant_unlocked" "achievement unlocked" 2 + +run_server_command "$TARGET_COMMAND_REVOKE" +sleep 3 +run_mcc_command "after_revoke_all" "achievement" 2 +run_mcc_command "after_revoke_locked" "achievement locked" 2 + +assert_pattern "$MCC_LOG" "Achievements/Advancements:" "Achievement command header never appeared." + +if ! grep -Fq "No achievements/advancements received yet." "$RUN_DIR/baseline_all.mcc.log"; then + INITIAL_STATUS="✅" +fi + +if grep -Fq "$TARGET_ID" "$RUN_DIR/after_grant_unlocked.mcc.log" && grep -Fq "[DONE]" "$RUN_DIR/after_grant_unlocked.mcc.log"; then + GRANT_STATUS="✅" +fi + +if [[ "$PROFILE" == "legacy" ]]; then + if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then + REVOKE_STATUS="✅" + fi +else + if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then + REVOKE_STATUS="✅" + elif [[ "$GRANT_STATUS" == "✅" ]] && ! grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_all.mcc.log"; then + REVOKE_STATUS="✅" + fi +fi + +if grep -Fq "[ACH_TEST] event" "$MCC_LOG" && grep -Fq "target_state origin=event id=$TARGET_ID title=" "$MCC_LOG"; then + API_STATUS="✅" +fi + +case "$INITIAL_STATUS|$GRANT_STATUS|$REVOKE_STATUS|$API_STATUS" in + "✅|✅|✅|✅") + VERDICT="✅ Pass" + NOTE="All planned achievement checks passed." + ;; + *"✅"*) + VERDICT="⚠️ Partial" + NOTE="At least one achievement phase passed, but the matrix did not fully clear." + ;; + *) + VERDICT="❌ Fail" + NOTE="Achievement checks did not produce the expected evidence." + ;; +esac + +run_mcc_command "quit" "quit" 2 +NOTE="$NOTE Artifacts saved in $RUN_DIR." diff --git a/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh new file mode 100755 index 00000000..9dbeb78c --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: summarize_achievements_matrix.sh " >&2 + exit 1 +fi + +MATRIX_DIR="$1" +RESULTS_TSV="$MATRIX_DIR/results.tsv" +PRECHECK_TXT="$MATRIX_DIR/preflight.txt" +BUILD_LOG="$MATRIX_DIR/build.log" + +if [[ ! -f "$RESULTS_TSV" ]]; then + echo "Missing results file: $RESULTS_TSV" >&2 + exit 1 +fi + +echo "# Achievements Matrix Report" +echo +echo "## Executed" +echo +if [[ -f "$PRECHECK_TXT" ]]; then + echo '```text' + cat "$PRECHECK_TXT" + echo '```' +fi +echo +echo "- Matrix artifacts: \`$MATRIX_DIR\`" +echo "- Results TSV: \`$RESULTS_TSV\`" +echo "- Build log: \`$BUILD_LOG\`" +echo "- Execution mode: sequential" +echo "- Auth mode: offline" +echo +echo "## Observed" +echo +echo "| Version | Port | Family | Initial snapshot | Grant | Revoke | API callback | Verdict |" +echo "|---|---:|---|---|---|---|---|---|" +awk -F '\t' 'NR > 1 { + printf("| `%s` | `%s` | %s | %s | %s | %s | %s | %s |\n", + $1, $3, $4, $5, $6, $7, $8, $9); +}' "$RESULTS_TSV" + +echo +echo "## Artifact Links" +echo +awk -F '\t' 'NR > 1 { + printf("- `%s`: run=`%s`, mcc=`%s`, server=`%s`, commands=`%s`\n", $1, $11, $12, $13, $14); + printf(" note: %s\n", $10); +}' "$RESULTS_TSV" + +echo +echo "## Inferred" +echo +echo "- Only rows with real MCC and server-log artifacts count as executed proof." +echo "- Rows blocked by missing Java, tmux, or server directories are environment-limited, not product pass results." +echo "- Legacy rows remain the highest-risk bucket because static inspection suggests pre-1.12 \`Statistics\` packets may not currently reach the achievements handler." diff --git a/MinecraftClient/LegacyAchievementCatalog.cs b/MinecraftClient/LegacyAchievementCatalog.cs new file mode 100644 index 00000000..bce17f8d --- /dev/null +++ b/MinecraftClient/LegacyAchievementCatalog.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace MinecraftClient +{ + internal static class LegacyAchievementCatalog + { + public static IReadOnlyList Ids { get; } = + [ + "achievement.openInventory", + "achievement.mineWood", + "achievement.buildWorkBench", + "achievement.buildPickaxe", + "achievement.buildFurnace", + "achievement.acquireIron", + "achievement.buildHoe", + "achievement.makeBread", + "achievement.bakeCake", + "achievement.buildBetterPickaxe", + "achievement.cookFish", + "achievement.onARail", + "achievement.buildSword", + "achievement.killEnemy", + "achievement.killCow", + "achievement.flyPig", + "achievement.snipeSkeleton", + "achievement.diamonds", + "achievement.diamondsToYou", + "achievement.portal", + "achievement.ghast", + "achievement.blazeRod", + "achievement.potion", + "achievement.theEnd", + "achievement.theEnd2", + "achievement.enchantments", + "achievement.overkill", + "achievement.bookcase", + "achievement.breedCow", + "achievement.spawnWither", + "achievement.killWither", + "achievement.fullBeacon", + "achievement.exploreAllBiomes", + "achievement.overpowered" + ]; + + private static readonly HashSet s_idSet = new(Ids, StringComparer.Ordinal); + + public static bool Contains(string id) + { + return s_idSet.Contains(id); + } + } +} diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 7032bcbd..eeabc8e0 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -91,6 +91,7 @@ namespace MinecraftClient.Protocol.Handlers private int currentDimension; private bool isOnlineMode = false; private readonly BlockingCollection>> packetQueue = new(); + private readonly Dictionary legacyAchievementProgress = new(StringComparer.Ordinal); private float LastYaw, LastPitch; private double lastSentX, lastSentY, lastSentZ; private float lastSentYaw, lastSentPitch; @@ -120,6 +121,7 @@ namespace MinecraftClient.Protocol.Handlers Tuple? netReader = null; // reader thread readonly ILogger log; readonly RandomNumberGenerator randomGen; + private bool legacyAchievementsInitialized; public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo, int rawProtocolVersion = 0) @@ -3132,6 +3134,11 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.RecipeBookSettings: break; + case PacketTypesIn.Statistics: + if (protocolVersion < MC_1_12_Version) + HandleLegacyStatistics(packetData); + break; + case PacketTypesIn.Advancements: HandleAdvancements(packetData); break; @@ -3147,9 +3154,39 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + /// + /// Handle the Statistics packet for pre-1.12 legacy achievements. + /// + private void HandleLegacyStatistics(Queue packetData) + { + int statCount = dataTypes.ReadNextVarInt(packetData); + + for (int i = 0; i < statCount; i++) + { + string statId = dataTypes.ReadNextString(packetData); + int value = dataTypes.ReadNextVarInt(packetData); + + if (statId.StartsWith("achievement.", StringComparison.Ordinal)) + legacyAchievementProgress[statId] = value > 0; + } + + List added = new(LegacyAchievementCatalog.Ids.Count + legacyAchievementProgress.Count); + + foreach (string achievementId in LegacyAchievementCatalog.Ids) + added.Add(CreateLegacyAchievement(achievementId, legacyAchievementProgress.TryGetValue(achievementId, out bool completed) && completed)); + + foreach (var (achievementId, completed) in legacyAchievementProgress) + { + if (!LegacyAchievementCatalog.Contains(achievementId)) + added.Add(CreateLegacyAchievement(achievementId, completed)); + } + + handler.OnAchievementsUpdate(added, [], reset: !legacyAchievementsInitialized); + legacyAchievementsInitialized = true; + } + /// /// Handle the Advancements packet (1.12+). - /// Also handles the Statistics packet for pre-1.12 legacy achievements. /// private void HandleAdvancements(Queue packetData) { @@ -3282,6 +3319,16 @@ namespace MinecraftClient.Protocol.Handlers handler.OnAchievementsUpdate(added, removedIds, reset); } + private static Achievement CreateLegacyAchievement(string id, bool isCompleted) + { + Dictionary criteria = new(StringComparer.Ordinal) + { + [id] = isCompleted + }; + IReadOnlyList[] requirements = [[id]]; + return new Achievement(id, null, null, AchievementType.Legacy, false, isCompleted, requirements, criteria); + } + /// /// Compute whether an advancement is completed based on AND-of-ORs requirements. ///