diff --git a/.skills/mcc-dev-workflow/SKILL.md b/.skills/mcc-dev-workflow/SKILL.md index b1a9ef01..79980b63 100644 --- a/.skills/mcc-dev-workflow/SKILL.md +++ b/.skills/mcc-dev-workflow/SKILL.md @@ -12,7 +12,7 @@ Use this skill when the task needs a real local server loop, not just code readi - Solution: `MinecraftClient.sln` - Runtime target: `.NET 10` / `net10.0` - Environment: Linux, macOS, or WSL with Java, tmux, python3, and dotnet available -- Default server root: `${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}` +- Default server root after `source tools/mcc-env.sh`: `${MCC_SERVERS:-/MinecraftOfficial/downloads}` - Default validation target when the user does not specify a version: `1.21.11` ## Console modes @@ -34,6 +34,50 @@ Both modes support the same commands and input/output through `ConsoleIO.Backend - A server log line containing `Done (` means startup finished. It does not guarantee that RCON is ready on the first attempt. Retry early `mc-rcon` commands. - When instructions, docs, and code disagree, trust current code and current tool behavior first. +## Shared server, isolated MCC sessions + +- `mc-*` commands operate on the shared local Minecraft server. +- `mcc-*` commands operate on one MCC client session. +- The default `session` is the current worktree name. +- The default username is derived from `session`, unless you pass `--username`. +- Session files live under `${TMPDIR:-/tmp}/mcc-debug//`. +- `MCC_SERVERS` stays the shared server-root override. + +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. + +Two worktrees can debug against one shared server like this: + +```bash +# worktree A +cd ~/Minecraft/Minecraft-Console-Client +source tools/mcc-env.sh +mc-start 1.21.11 +mcc-debug -v 1.21.11 --file-input + +# worktree B +cd ~/Minecraft/Minecraft-Console-Client-foo +source tools/mcc-env.sh +mcc-debug -v 1.21.11 --file-input + +# from each worktree, mcc-* targets that worktree's default session +mcc-state +``` + +If you want two MCC sessions from the same worktree, pass `--session NAME` explicitly. + +## tmpfs build mode + +Use this on machines with enough RAM when you want worktree-isolated builds outside the repo tree: + +```bash +source tools/mcc-env.sh +export MCC_BUILD_MODE=tmpfs +mcc-build +mcc-build-clean +``` + +`MCC_BUILD_MODE=tmpfs` redirects build output to `/dev/shm/mcc-build//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` if `/dev/shm` is unavailable. + ## Preflight and reset Before scripted runs, especially on macOS or in a reused tmux environment: @@ -58,9 +102,11 @@ Interactive shell: ```bash source tools/mcc-env.sh +SESSION="$(_mcc_resolve_session)" +USERNAME="$(_mcc_resolve_username "$SESSION")" mc-start 1.21.11 mc-log 1.21.11 100 -mc-rcon "op CursorBot" +mc-rcon "op $USERNAME" mc-stop 1.21.11 ``` @@ -68,7 +114,7 @@ Non-interactive shell: ```bash tools/start-server.sh 1.21.11 -tools/mc-rcon.sh "op CursorBot" +tools/mc-rcon.sh "op mcc_smoke_a" ``` If the servers live outside the repo, set `MCC_SERVERS` before sourcing or invoking the tools: @@ -104,16 +150,16 @@ mcc-debug -v 1.21.11 --file-input --no-build ### What mcc-debug.sh does 1. Builds MCC (unless `--no-build`) -2. Creates a clean temp config at `/tmp/mcc-debug/MinecraftClient.debug.ini` with CursorBot account, Terrain/Inventory/Entity enabled and noisy bots disabled +2. Creates a clean temp config at `${TMPDIR:-/tmp}/mcc-debug//MinecraftClient.debug.ini` 3. Ensures server is running (starts if not, waits for `Done (`) -4. Launches MCC in the specified mode +4. Launches MCC in a session-scoped tmux session and session-scoped log/input/pid files ### After launch -- **FileInput mode**: drive MCC via `mcc-cmd "debug state"` or `echo "debug state" >> mcc_input.txt` -- **Interactive/TUI mode**: attach with `tmux attach -t mcc-debug`, type commands directly -- **Logs**: `tail -f /tmp/mcc-debug/mcc-debug.log` (FileInput mode only; TUI/interactive mode outputs to tmux) -- **Server RCON**: `mc-rcon "op CursorBot"`, `mc-rcon "gamemode creative CursorBot"` +- **FileInput mode**: drive MCC via `mcc-cmd --session smoke-a "debug state"`, or just `mcc-cmd "debug state"` from the same worktree +- **Interactive/TUI mode**: attach with `tmux attach -t mcc-` +- **Logs**: `mcc-log-mcc --session smoke-a` or `tail -f "${TMPDIR:-/tmp}/mcc-debug//mcc-debug.log"` +- **Server RCON**: grant op or gamemode to the username derived from that session ## Debug commands (in-game) @@ -128,7 +174,7 @@ Prints a one-shot summary of MCC's internal state: ``` === MCC Debug State === Server: localhost:25565 -Username: CursorBot +Username: mcc_smoke_a Protocol: 774 GameMode: 1 Health: 20.0 @@ -152,18 +198,20 @@ For agents calling MCC commands programmatically: ```bash source tools/mcc-env.sh -mcc-debug -v 1.21.11 --file-input --no-build +SESSION="smoke-a" +mcc-debug -v 1.21.11 --file-input --session "$SESSION" --no-build # Send commands: -mcc-cmd "debug state" -mcc-cmd "inventory player list" -mcc-cmd "entity" +mcc-cmd --session "$SESSION" "debug state" +mcc-cmd --session "$SESSION" "inventory player list" +mcc-cmd --session "$SESSION" "entity" # Check results: -tail -20 /tmp/mcc-debug/mcc-debug.log +mcc-log-mcc --session "$SESSION" # Stop: -mcc-cmd "quit" +mcc-cmd --session "$SESSION" "quit" +mcc-kill --session "$SESSION" mc-stop 1.21.11 ``` @@ -171,10 +219,11 @@ mc-stop 1.21.11 ```bash source tools/mcc-env.sh -mcc-debug -v 1.21.11 +SESSION="live-a" +mcc-debug -v 1.21.11 --session "$SESSION" # In another terminal: -tmux attach -t mcc-debug +tmux attach -t "mcc-$SESSION" # Type commands directly in MCC console ``` @@ -192,22 +241,23 @@ TUI mode runs Consolonia full-screen in a tmux session. Key differences: ```bash source tools/mcc-env.sh -mcc-debug -v 1.21.11 -m tui --no-build +SESSION="tui-a" +mcc-debug -v 1.21.11 -m tui --session "$SESSION" --no-build # Cannot use mcc-cmd (no FileInput); must use tmux send-keys: -tmux send-keys -t mcc-debug "/debug state" Enter +tmux send-keys -t "mcc-$SESSION" "/debug state" Enter # Read TUI screen: -tmux capture-pane -t mcc-debug -p -S -30 +tmux capture-pane -t "mcc-$SESSION" -p -S -30 # Stop: -tmux send-keys -t mcc-debug Escape +tmux send-keys -t "mcc-$SESSION" Escape ``` **Caveat with tmux send-keys and Consolonia**: When sending text containing `/`, the Enter key may need to be sent separately: ```bash -tmux send-keys -t mcc-debug "/inventory player list" -tmux send-keys -t mcc-debug Enter +tmux send-keys -t "mcc-$SESSION" "/inventory player list" +tmux send-keys -t "mcc-$SESSION" Enter ``` ## mcc-env.sh quick reference @@ -226,26 +276,28 @@ After `source tools/mcc-env.sh`: | `mc-wait-stop VER [SEC]` | Wait for server shutdown, with force-kill fallback | | `mc-reset-test-env [--all|VER...]` | Reset shared tmux server state and stale pipes | | `mcc-build` | Build MCC | -| `mcc-run [PORT]` | Run MCC classic+FileInput on port | -| `mcc-tui [PORT]` | Run MCC TUI mode in tmux | -| `mcc-cmd "CMD"` | Append command to mcc_input.txt | -| `mcc-kill` | Kill MCC process and debug session | +| `mcc-build-clean` | Clear the current worktree's build output | +| `mcc-run [--session NAME] [--username NAME] [--port PORT]` | Convenience wrapper for `mcc-debug --file-input --no-build` | +| `mcc-tui [--session NAME] [--username NAME] [--port PORT]` | Convenience wrapper for `mcc-debug -m tui --no-build` | +| `mcc-cmd [--session NAME] "CMD"` | Append a command to one session's input file | +| `mcc-kill [--session NAME]` | Kill one MCC process and session | | `mcc-debug [OPTS]` | One-step debug session (see above) | -| `mcc-log-mcc` | Tail MCC debug log | -| `mcc-state` | Send `debug state` and print last 30 log lines | +| `mcc-log-mcc [--session NAME]` | Tail one MCC debug log | +| `mcc-state [--session NAME]` | Send `debug state` and print the last 30 log lines | | `mcc-preflight [VER...]` | Verify Java, tmux, dotnet, python3, and server dirs | ## Temporary config recipe ```bash source tools/mcc-env.sh -TEST_ROOT="${TMPDIR:-/tmp}/mcc-dev" -CFG="$TEST_ROOT/MinecraftClient.1.21.11.ini" -mkdir -p "$TEST_ROOT" +SESSION="smoke-a" +USERNAME="$(_mcc_resolve_username "$SESSION")" +CFG="$(_mcc_session_root "$SESSION")/MinecraftClient.debug.ini" +mkdir -p "$(_mcc_session_root "$SESSION")" bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \ "$CFG" \ "1.21.11" \ - "CursorBot" + "$USERNAME" ``` For TUI mode, also add: @@ -259,14 +311,14 @@ MCC output should include: - `[MCC] Server was successfully joined.` -Server output should include: +Server output should include the session-derived username, for example: -- `CursorBot joined the game` +- `mcc_smoke_a joined the game` Basic command check: ```bash -mcc-cmd "inventory player list" +mcc-cmd --session smoke-a "inventory player list" ``` If a scripted run fails before MCC joins, check for a harness problem before assuming a product regression. Missing `mcc.log`, a pre-join `Connection refused`, or a server that never reached `Done (` usually means shared-state cleanup or startup failed. @@ -292,7 +344,7 @@ If a scripted run fails before MCC joins, check for a harness problem before ass - Legacy `1.8` and `1.8.9` servers may need `use-native-transport=false` in `server.properties` on some Linux environments. - For timing-sensitive work, do not trust wall-clock intuition. Use a real server run and capture evidence from logs or test scripts. - **TUI mode tip**: if the terminal becomes unresponsive after a crash, run `stty sane && reset` to restore it. -- **tmux capture trick**: `tmux capture-pane -t mcc-debug -p -S -50` captures the last 50 lines of a tmux session without attaching. +- **tmux capture trick**: `tmux capture-pane -t mcc- -p -S -50` captures the last 50 lines of a tmux session without attaching. ## Tool files diff --git a/.skills/mcc-integration-testing/references/command-matrix.md b/.skills/mcc-integration-testing/references/command-matrix.md index 31077374..782bc745 100644 --- a/.skills/mcc-integration-testing/references/command-matrix.md +++ b/.skills/mcc-integration-testing/references/command-matrix.md @@ -19,7 +19,7 @@ This skill uses a fixed set of stable commands for local offline integration tes - `look east` - `/gamemode survival` - `respawn` -- `/tp CursorBot 0 -60 0` +- `/tp MCCBot 0 -60 0` - `smoke_test_from_mcc_full_spectrum` - `integration_test_chat_response` @@ -29,63 +29,63 @@ Notes: ## Server-side commands via `mc-rcon` -- `op CursorBot` +- `op MCCBot` - `gamerule sendCommandFeedback true` - `gamerule logAdminCommands true` - `time set day` - `weather clear` - `say Hello from the server console` -- `msg CursorBot This is a private whisper` -- `effect give CursorBot minecraft:speed 30 1` -- `effect give CursorBot minecraft:regeneration 10 1` -- `kill CursorBot` +- `msg MCCBot This is a private whisper` +- `effect give MCCBot minecraft:speed 30 1` +- `effect give MCCBot minecraft:regeneration 10 1` +- `kill MCCBot` ## Representative entity coverage -- `execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~` -- `execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~` -- `execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~` -- `execute as CursorBot at @s run summon minecraft:skeleton ~8 ~ ~` -- `execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~` -- `execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~` -- `execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2` -- `execute as CursorBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:"minecraft:diamond",count:1}}` -- `execute as CursorBot at @s run summon minecraft:spider ~10 ~ ~` -- `execute as CursorBot at @s run summon minecraft:pig ~-8 ~ ~` +- `execute as MCCBot at @s run summon minecraft:cow ~2 ~ ~` +- `execute as MCCBot at @s run summon minecraft:zombie ~4 ~ ~` +- `execute as MCCBot at @s run summon minecraft:creeper ~6 ~ ~` +- `execute as MCCBot at @s run summon minecraft:skeleton ~8 ~ ~` +- `execute as MCCBot at @s run summon minecraft:villager ~-2 ~ ~` +- `execute as MCCBot at @s run summon minecraft:allay ~-4 ~ ~` +- `execute as MCCBot at @s run summon minecraft:armor_stand ~ ~ ~2` +- `execute as MCCBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:"minecraft:diamond",count:1}}` +- `execute as MCCBot at @s run summon minecraft:spider ~10 ~ ~` +- `execute as MCCBot at @s run summon minecraft:pig ~-8 ~ ~` ## Block placement coverage -- `execute as CursorBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone` -- `execute as CursorBot at @s run setblock ~5 ~ ~5 minecraft:chest` -- `execute as CursorBot at @s run setblock ~5 ~1 ~5 minecraft:furnace` -- `execute as CursorBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table` +- `execute as MCCBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone` +- `execute as MCCBot at @s run setblock ~5 ~ ~5 minecraft:chest` +- `execute as MCCBot at @s run setblock ~5 ~1 ~5 minecraft:furnace` +- `execute as MCCBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table` ## Dimension change coverage -- `execute in minecraft:the_nether run tp CursorBot 0 64 0` -- `execute in minecraft:overworld run tp CursorBot 0 -60 0` +- `execute in minecraft:the_nether run tp MCCBot 0 64 0` +- `execute in minecraft:overworld run tp MCCBot 0 -60 0` ## Representative particle coverage -- `execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force` -- `execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force` -- `execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force` -- `execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force` -- `execute as CursorBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force` -- `execute as CursorBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force` +- `execute as MCCBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force` +- `execute as MCCBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force` +- `execute as MCCBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force` +- `execute as MCCBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force` +- `execute as MCCBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force` +- `execute as MCCBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force` ## Representative sound coverage -- `execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0` -- `execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0` -- `execute as CursorBot at @s run playsound minecraft:entity.experience_orb.pickup master CursorBot ~ ~ ~ 1 1 0` +- `execute as MCCBot at @s run playsound minecraft:entity.lightning_bolt.thunder master MCCBot ~ ~ ~ 1 1 0` +- `execute as MCCBot at @s run playsound minecraft:block.note_block.bell master MCCBot ~ ~ ~ 1 1 0` +- `execute as MCCBot at @s run playsound minecraft:entity.experience_orb.pickup master MCCBot ~ ~ ~ 1 1 0` ## Explosion coverage -- `execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~` -- `execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~` +- `execute as MCCBot at @s run summon minecraft:tnt ~3 ~ ~` +- `execute as MCCBot at @s run summon minecraft:tnt ~6 ~ ~` ## Kill and respawn cycle -- `kill CursorBot` (via RCON, requires survival mode) +- `kill MCCBot` (via RCON, requires survival mode) - `respawn` (via MCC command after death) diff --git a/.skills/mcc-integration-testing/scripts/common.sh b/.skills/mcc-integration-testing/scripts/common.sh index 973b5da3..ac5c0250 100755 --- a/.skills/mcc-integration-testing/scripts/common.sh +++ b/.skills/mcc-integration-testing/scripts/common.sh @@ -73,7 +73,7 @@ wait_for_server_stop() { ((elapsed += 1)) done - mc-kill "$version" >/dev/null 2>&1 || true + mc-kill "$version" --confirm >/dev/null 2>&1 || true if ! server_running "$version"; then return 0 @@ -104,7 +104,7 @@ remove_stale_stdin_pipe() { local version="$1" local pipe_path="$MCC_SERVERS/$version/stdin.pipe" - if [[ -e "$pipe_path" && ! -p "$pipe_path" ]]; then + if [[ -e "$pipe_path" ]] && ! server_running "$version"; then rm -f "$pipe_path" fi } diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 38e73978..8ede375b 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -41,12 +41,12 @@ upsert_property() { if [[ ! -f "$PROPS_FILE" ]]; then mc-start "$VERSION" wait_for_server_ready "$VERSION" - mc-stop "$VERSION" + mc-stop "$VERSION" --confirm wait_for_server_stop "$VERSION" fi if server_running; then - mc-stop "$VERSION" + mc-stop "$VERSION" --confirm wait_for_server_stop "$VERSION" 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 64727a58..1e30f673 100644 --- a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh +++ b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh @@ -28,11 +28,11 @@ if [[ $# -ge 3 && -f "$1" ]]; then TEMPLATE_INI="$1" OUTPUT_INI="$2" MC_VERSION="$3" - LOGIN_NAME="${4:-CursorBot}" + LOGIN_NAME="${4:-MCCBot}" else OUTPUT_INI="$1" MC_VERSION="$2" - LOGIN_NAME="${3:-CursorBot}" + LOGIN_NAME="${3:-MCCBot}" fi ACCOUNT_TYPE="${MCC_TEST_ACCOUNT_TYPE:-mojang}" diff --git a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh index 2d84ac1b..eab7c694 100755 --- a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh +++ b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh @@ -12,7 +12,7 @@ usage() { cat <<'EOF' Usage: reset_shared_test_state.sh [--all | ...] -Kills shared tmux test sessions and removes stale stdin pipes. +Kills shared server tmux test sessions and removes stale server stdin pipes. EOF } @@ -26,8 +26,6 @@ kill_named_session() { tmux kill-session -t "$session_name" 2>/dev/null || true } -kill_named_session "mcc-debug" - if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then while IFS= read -r session_name; do [[ -z "$session_name" ]] && continue @@ -36,9 +34,7 @@ if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then while IFS= read -r pipe_path; do [[ -z "$pipe_path" ]] && continue - if [[ ! -p "$pipe_path" ]]; then - rm -f "$pipe_path" - fi + rm -f "$pipe_path" done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true) else for version in "$@"; do @@ -46,5 +42,3 @@ else remove_stale_stdin_pipe "$version" done fi - -rm -f "$MCC_REPO/mcc_input.txt" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh index df0236e0..1484acd6 100755 --- a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -37,6 +37,8 @@ fi SERVER_DIR="$1" MC_VERSION="$2" PROFILE="$3" +SESSION_NAME="achievements-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}" +TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")" if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then echo "Unsupported profile: $PROFILE" >&2 @@ -55,11 +57,11 @@ 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" +INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")" 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_COMMAND_GRANT="advancement grant $TEST_USERNAME only minecraft:story/root" +TARGET_COMMAND_REVOKE="advancement revoke $TEST_USERNAME only minecraft:story/root" TARGET_TYPE="Modern 🌱" PORT="unknown" MCC_PID="" @@ -74,8 +76,8 @@ 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_COMMAND_GRANT="achievement give achievement.openInventory $TEST_USERNAME" + TARGET_COMMAND_REVOKE="achievement take achievement.openInventory $TEST_USERNAME" TARGET_TYPE="Legacy 🧱" fi @@ -124,7 +126,7 @@ cleanup() { wait "$MCC_PID" 2>/dev/null || true fi - mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true + mc-stop "$SERVER_DIR" --confirm >/dev/null 2>&1 || true wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true ln -sfn "$RUN_DIR" "$LATEST_LINK" write_summary @@ -286,7 +288,7 @@ if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR" fi -bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null || fail "Failed to prepare temporary MCC config." +bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null || fail "Failed to prepare temporary MCC config." PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")" "$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR" @@ -296,6 +298,7 @@ if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" fi +mkdir -p "$(dirname "$INPUT_FILE")" : > "$INPUT_FILE" rm -f "$MCC_LOG" @@ -306,9 +309,9 @@ wait_for_server_ready "$SERVER_DIR" || 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 -- \ + MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- \ "$CFG" \ - CursorBot \ + "$TEST_USERNAME" \ - \ "localhost:$PORT" \ "--accounttype=mojang" \ @@ -323,9 +326,9 @@ log_step "Starting MCC for $MC_VERSION" 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." +wait_for_file_pattern "$SERVER_LOG_FILE" "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join." -run_server_command "op CursorBot" +run_server_command "op $TEST_USERNAME" run_server_command "gamerule sendCommandFeedback true" if [[ "$PROFILE" == "modern" ]]; then run_server_command "gamerule logAdminCommands true" diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh index 51021974..181862a8 100755 --- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -17,31 +17,31 @@ RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing" RUN_ID="$(date +%Y%m%d-%H%M%S)" RUN_DIR="$RUN_ROOT/$RUN_ID" SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log" -MCC_LOG="$RUN_DIR/mcc.log" +SESSION_NAME="full-spectrum-${MC_VERSION//[^a-zA-Z0-9]/_}" +TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")" +MCC_LOG="$(_mcc_session_log_file "$SESSION_NAME")" +PID_FILE="$(_mcc_session_pid_file "$SESSION_NAME")" +MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION_NAME")" BUILD_LOG="$RUN_DIR/build.log" SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log" SERVER_FILE_LOG="$RUN_DIR/server-latest.log" -INPUT_FILE="$REPO_ROOT/mcc_input.txt" +INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")" CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" -MCC_PID="" mkdir -p "$RUN_DIR" cleanup() { - if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then - mcc-cmd "quit" >/dev/null 2>&1 || true - sleep 2 - kill "$MCC_PID" 2>/dev/null || true - wait "$MCC_PID" 2>/dev/null || true - fi + mcc-cmd --session "$SESSION_NAME" "quit" >/dev/null 2>&1 || true + sleep 2 + mcc-kill --session "$SESSION_NAME" >/dev/null 2>&1 || true - mc-stop "$VERSION" >/dev/null 2>&1 || true + mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true } trap cleanup EXIT prepare_config() { - bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null + bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null } wait_for_server_log_pattern() { @@ -129,35 +129,57 @@ run_server_command() { run_mcc_command() { local cmd="$1" echo "MCC> $cmd" - mcc-cmd "$cmd" + mcc-cmd --session "$SESSION_NAME" "$cmd" sleep 2 } +start_mcc_session() { + local -a mcc_args=("$CFG" "$TEST_USERNAME" "-" "localhost:$SERVER_PORT") + local mcc_args_cmd + mcc_args_cmd="$(printf '%q ' "${mcc_args[@]}")" + + tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true + rm -f "$PID_FILE" + tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ + "cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $mcc_args_cmd > '$MCC_LOG' 2>&1" + + for _ in $(seq 1 25); do + if [[ -s "$PID_FILE" ]]; then + return 0 + fi + sleep 0.2 + done + + fail "Failed to capture MCC PID for session $SESSION_NAME" +} + bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null "$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" +mcc-reset-session --session "$SESSION_NAME" >/dev/null echo "Building MCC..." mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" prepare_config SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")" +if [[ -z "$SERVER_PORT" ]]; then + fail "Failed to resolve server port" +fi +mkdir -p "$(dirname "$INPUT_FILE")" "$(dirname "$MCC_LOG")" : > "$INPUT_FILE" +rm -f "$MCC_LOG" echo "Starting server..." mc-start "$VERSION" >/dev/null wait_for_server_ready "$VERSION" || fail "Server did not become ready" echo "Starting MCC..." -( - cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" CursorBot - "localhost:$SERVER_PORT" > "$MCC_LOG" 2>&1 -) & -MCC_PID=$! +start_mcc_session wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join" -wait_for_server_log_pattern "CursorBot joined the game" "server join entry" 30 || fail "Server never logged the join" +wait_for_server_log_pattern "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join" -run_server_command "op CursorBot" +run_server_command "op $TEST_USERNAME" run_server_command "gamerule sendCommandFeedback true" run_server_command "gamerule logAdminCommands true" run_server_command "time set day" @@ -176,7 +198,7 @@ run_mcc_command "/time query daytime" run_mcc_command "smoke_test_from_mcc_full_spectrum" # ── Phase 2: Movement and look commands ── -run_mcc_command "/tp CursorBot 0 -60 0" +run_mcc_command "/tp $TEST_USERNAME 0 -60 0" sleep 3 run_mcc_command "look up" sleep 1 @@ -193,35 +215,35 @@ run_mcc_command "inventory creativeclear 38" run_mcc_command "inventory player list" # ── Phase 4: Block placement and interaction ── -run_server_command "execute as CursorBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone" +run_server_command "execute as $TEST_USERNAME at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone" sleep 2 -run_server_command "execute as CursorBot at @s run setblock ~5 ~ ~5 minecraft:chest" +run_server_command "execute as $TEST_USERNAME at @s run setblock ~5 ~ ~5 minecraft:chest" sleep 1 -run_server_command "execute as CursorBot at @s run setblock ~5 ~1 ~5 minecraft:furnace" +run_server_command "execute as $TEST_USERNAME at @s run setblock ~5 ~1 ~5 minecraft:furnace" sleep 1 -run_server_command "execute as CursorBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table" +run_server_command "execute as $TEST_USERNAME at @s run setblock ~6 ~ ~5 minecraft:crafting_table" sleep 1 # ── Phase 5: Entity spawning (expanded coverage) ── -run_server_command "execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:skeleton ~8 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2" -run_server_command "execute as CursorBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:\"minecraft:diamond\",count:1}}" -run_server_command "execute as CursorBot at @s run summon minecraft:spider ~10 ~ ~" -run_server_command "execute as CursorBot at @s run summon minecraft:pig ~-8 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:cow ~2 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:zombie ~4 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:creeper ~6 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:skeleton ~8 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:villager ~-2 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:allay ~-4 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:armor_stand ~ ~ ~2" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:\"minecraft:diamond\",count:1}}" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:spider ~10 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:pig ~-8 ~ ~" sleep 2 run_mcc_command "entity" # ── Phase 6: Effects and environment ── -run_server_command "effect give CursorBot minecraft:speed 30 1" +run_server_command "effect give $TEST_USERNAME minecraft:speed 30 1" sleep 2 run_mcc_command "health" -run_server_command "effect give CursorBot minecraft:regeneration 10 1" +run_server_command "effect give $TEST_USERNAME minecraft:regeneration 10 1" sleep 2 run_mcc_command "health" @@ -233,39 +255,39 @@ run_mcc_command "/gamemode creative" sleep 2 # ── Phase 8: Dimension change (nether) ── -run_server_command "execute in minecraft:the_nether run tp CursorBot 0 64 0" +run_server_command "execute in minecraft:the_nether run tp $TEST_USERNAME 0 64 0" sleep 4 run_mcc_command "health" -run_server_command "execute in minecraft:overworld run tp CursorBot 0 -60 0" +run_server_command "execute in minecraft:overworld run tp $TEST_USERNAME 0 -60 0" sleep 4 # ── Phase 9: Server chat and whisper ── run_server_command "say Hello from the server console" sleep 2 -run_server_command "msg CursorBot This is a private whisper" +run_server_command "msg $TEST_USERNAME This is a private whisper" sleep 2 run_mcc_command "integration_test_chat_response" # ── Phase 10: Particles, sounds, and explosions ── -run_server_command "execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force" -run_server_command "execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force" -run_server_command "execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force" -run_server_command "execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force" -run_server_command "execute as CursorBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force" -run_server_command "execute as CursorBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force" +run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force" +run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force" +run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force" +run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force" +run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force" +run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force" -run_server_command "execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0" -run_server_command "execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0" -run_server_command "execute as CursorBot at @s run playsound minecraft:entity.experience_orb.pickup master CursorBot ~ ~ ~ 1 1 0" +run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:entity.lightning_bolt.thunder master $TEST_USERNAME ~ ~ ~ 1 1 0" +run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:block.note_block.bell master $TEST_USERNAME ~ ~ ~ 1 1 0" +run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:entity.experience_orb.pickup master $TEST_USERNAME ~ ~ ~ 1 1 0" -run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:tnt ~3 ~ ~" sleep 2 -run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~" +run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:tnt ~6 ~ ~" # ── Phase 11: Kill and respawn cycle ── run_mcc_command "/gamemode survival" sleep 2 -run_server_command "kill CursorBot" +run_server_command "kill $TEST_USERNAME" sleep 4 run_mcc_command "respawn" sleep 4 @@ -295,14 +317,14 @@ assert_not_contains "$MCC_LOG" "Failed to load settings" "MCC failed to reload i assert_not_contains "$MCC_LOG" "NullReferenceException" "A NullReferenceException occurred during the test" # ── Assertions: Server log ── -assert_contains "$SERVER_FILE_LOG" "CursorBot joined the game" "Server never saw CursorBot join" +assert_contains "$SERVER_FILE_LOG" "$TEST_USERNAME joined the game" "Server never saw $TEST_USERNAME join" assert_contains "$SERVER_FILE_LOG" "smoke_test_from_mcc_full_spectrum" "Server never received the client chat message" assert_contains "$SERVER_FILE_LOG" "Displaying particle minecraft:happy_villager" "Particle events were not recorded on the server" -assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to CursorBot" "Sound events were not recorded on the server" +assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to $TEST_USERNAME" "Sound events were not recorded on the server" assert_contains "$SERVER_FILE_LOG" "Summoned new Primed TNT" "TNT summon did not occur on the server" assert_contains "$SERVER_FILE_LOG" "integration_test_chat_response" "Server never received the chat response test message" assert_contains "$SERVER_FILE_LOG" "Hello from the server console" "Server say command was not logged" -assert_contains "$SERVER_FILE_LOG" "Killed CursorBot" "Server kill command did not execute" +assert_contains "$SERVER_FILE_LOG" "Killed $TEST_USERNAME" "Server kill command did not execute" assert_not_contains "$SERVER_FILE_LOG" "Sending unknown packet 'clientbound/minecraft:disconnect'" "Server hit the disconnect packet regression during the test" cat <&2 + return 1 +} + +wait_for_server_log_pattern() { + local pattern="$1" + local description="$2" + local timeout="${3:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$SERVER_LOG_FILE" ]] && grep -Fq "$pattern" "$SERVER_LOG_FILE"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for server log: $description" >&2 + return 1 +} + +capture_server_logs() { + mc-log "$VERSION" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true + if [[ -f "$SERVER_LOG_FILE" ]]; then + cp "$SERVER_LOG_FILE" "$SERVER_FILE_LOG" + fi +} + +fail() { + capture_server_logs + echo "FAIL: $1" >&2 + echo "Run directory: $RUN_DIR" >&2 + exit 1 +} + +cleanup() { + mcc-cmd --session "$SESSION_A" "quit" >/dev/null 2>&1 || true + mcc-cmd --session "$SESSION_B" "quit" >/dev/null 2>&1 || true + sleep 1 + mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true + mcc-kill --session "$SESSION_B" >/dev/null 2>&1 || true + mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true + wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true +} +trap cleanup EXIT + +assert_session_alive() { + local session="$1" + local pid_file="$2" + if [[ -s "$pid_file" ]]; then + local pid + pid="$(tr -cd '0-9' < "$pid_file")" + if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then + return 0 + fi + fi + + tmux has-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null +} + +assert_server_alive() { + server_running "$VERSION" || fail "Shared server session is not alive" +} + +start_file_input_session() { + local session="$1" + local username="$2" + local port="$3" + bash "$REPO_ROOT/tools/mcc-debug.sh" \ + --version "$VERSION" \ + --port "$port" \ + --file-input \ + --no-build \ + --session "$session" \ + --username "$username" >/dev/null +} + +bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null +bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null +"$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" >/dev/null +mcc-reset-session --session "$SESSION_A" >/dev/null +mcc-reset-session --session "$SESSION_B" >/dev/null + +echo "Building MCC..." +mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" + +echo "Starting shared server..." +mc-start "$VERSION" >/dev/null +wait_for_server_ready "$VERSION" || fail "Server did not become ready" +SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")" +if [[ -z "$SERVER_PORT" ]]; then + fail "Failed to resolve server port" +fi + +echo "Starting MCC session A..." +start_file_input_session "$SESSION_A" "$USERNAME_A" "$SERVER_PORT" +echo "Starting MCC session B..." +start_file_input_session "$SESSION_B" "$USERNAME_B" "$SERVER_PORT" + +wait_for_file_pattern "$LOG_A" "Server was successfully joined." "session A join success" 90 || fail "Session A failed to join" +wait_for_file_pattern "$LOG_B" "Server was successfully joined." "session B join success" 90 || fail "Session B failed to join" +wait_for_server_log_pattern "$USERNAME_A joined the game" "server join for session A" 30 || fail "Server never logged $USERNAME_A join" +wait_for_server_log_pattern "$USERNAME_B joined the game" "server join for session B" 30 || fail "Server never logged $USERNAME_B join" + +echo "Sending debug state to both sessions..." +mcc-cmd --session "$SESSION_A" "debug state" +mcc-cmd --session "$SESSION_B" "debug state" +sleep 2 +wait_for_file_pattern "$LOG_A" "[FileInput] > debug state" "session A debug state command" 20 || fail "Session A did not consume debug state" +wait_for_file_pattern "$LOG_B" "[FileInput] > debug state" "session B debug state command" 20 || fail "Session B did not consume debug state" + +echo "Killing session A..." +mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true +sleep 2 + +assert_session_alive "$SESSION_B" "$PID_B_FILE" || fail "Session B is not alive after killing session A" +assert_server_alive + +echo "Verifying session B still responds..." +mcc-cmd --session "$SESSION_B" "health" +wait_for_file_pattern "$LOG_B" "[FileInput] > health" "session B health command after session A kill" 20 || fail "Session B stopped responding after session A kill" + +if [[ -s "$PID_A_FILE" ]]; then + pid_a="$(tr -cd '0-9' < "$PID_A_FILE")" + if [[ -n "$pid_a" ]] && kill -0 "$pid_a" 2>/dev/null; then + fail "Session A is still alive after mcc-kill" + fi +fi + +capture_server_logs + +cat < - + diff --git a/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj b/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj index 0ee15b7a..96c52c1a 100644 --- a/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj +++ b/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj @@ -8,7 +8,7 @@ - + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..41d20762 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,11 @@ + + + $(DefaultItemExcludes);**/bin/**;**/obj/** + + + + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/ + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/ + $(BaseIntermediateOutputPath) + + diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index fadb2203..9a9c8baa 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -38,20 +38,20 @@ - + - - + + - + NU1701 diff --git a/docs/guide/ai-assisted-development.md b/docs/guide/ai-assisted-development.md index 9f8fdc03..852df5db 100644 --- a/docs/guide/ai-assisted-development.md +++ b/docs/guide/ai-assisted-development.md @@ -367,10 +367,16 @@ This gives you the helper functions used by the workflow: - `mc-cmd` - `mc-log` - `mc-rcon` +- `mc-reset-test-env` - `mcc-build` +- `mcc-build-clean` - `mcc-run` +- `mcc-tui` - `mcc-cmd` +- `mcc-log-mcc` +- `mcc-state` - `mcc-kill` +- `mcc-debug` - `mcc-reload` @@ -412,7 +418,7 @@ Then make sure the helper functions are loaded: ```bash type mc-start type mcc-build -type mcc-run +type mcc-debug ``` @@ -426,11 +432,55 @@ The moving parts are: - a local Minecraft server running in `tmux` - `mc-rcon` for server-side commands such as `/op`, `/give`, `/summon`, or gamerule setup - MCC started with `MCC_FILE_INPUT=1` -- `FileInputBot`, which watches `mcc_input.txt` and turns file lines into MCC commands or server chat +- `FileInputBot`, which watches the session input file under `${TMPDIR:-/tmp}/mcc-debug//mcc_input.txt` - logs from MCC and the local server, which the agent can inspect between runs The result is simple: the agent can change code, rebuild, start the app, inject commands, and read the result without waiting for a human to sit in the terminal. +## Shared Server, Isolated MCC Sessions + +The harness separates shared server state from MCC client state. + +- `mc-*` commands operate on the shared local Minecraft server. +- `mcc-*` commands operate on one MCC client session. +- The default `session` is the current worktree name. +- The default username is derived from `session`, so two worktrees can join the same shared server without kicking each other. +- Session files live under `${TMPDIR:-/tmp}/mcc-debug//`. +- `MCC_SERVERS` stays the shared server-root override. + +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. + +Two worktrees can share one local server like this: + +```bash +# worktree A +cd ~/Minecraft/Minecraft-Console-Client +source tools/mcc-env.sh +mc-start 1.21.11 +mcc-debug -v 1.21.11 --file-input + +# worktree B +cd ~/Minecraft/Minecraft-Console-Client-foo +source tools/mcc-env.sh +mcc-debug -v 1.21.11 --file-input + +# from each worktree, mcc-* targets that worktree's default session +mcc-state +``` + +If you need two MCC sessions from one worktree, pass `--session NAME` explicitly. + +### tmpfs build mode + +```bash +source tools/mcc-env.sh +export MCC_BUILD_MODE=tmpfs +mcc-build +mcc-build-clean +``` + +When `MCC_BUILD_MODE=tmpfs`, build output goes to `/dev/shm/mcc-build//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` when `/dev/shm` is unavailable. + ## Repository Tools These are the repo-level tools that make the workflow practical. @@ -438,6 +488,7 @@ These are the repo-level tools that make the workflow practical. | Path | Purpose | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `tools/mcc-env.sh` | Loads the shell helper functions used for the normal loop. | +| `tools/mcc-debug.sh` | Starts a session-scoped MCC debug run with generated config, log, pid, and tmux state. | | `tools/start-server.sh` | Starts a local Minecraft server in a named `tmux` session with a FIFO for stdin. | | `tools/mc-rcon.sh` | Sends RCON commands to the local server using `python3`. | | `tools/decompile.sh` | Downloads `MinecraftDecompiler.jar` if needed, decompiles the requested Minecraft version, and fetches `server.jar` for server-side work. | @@ -453,7 +504,7 @@ There is one more piece worth calling out: - `MinecraftClient/ChatBots/FileInputBot.cs` is what makes file-driven command injection possible. - It is loaded when `MCC_FILE_INPUT=1` is set. -- `mcc-run` in `tools/mcc-env.sh` already sets that flag for you. +- `mcc-debug --file-input` and `mcc-run` already set that flag for you. ## Skills @@ -485,9 +536,12 @@ Some skills also carry their own bundled resources: This is the core loop you should expect an agent to follow. -### 1. Start the local server +### 1. Start the local server and resolve the MCC identity ```bash +source tools/mcc-env.sh +SESSION="smoke-a" +USERNAME="$(_mcc_resolve_username "$SESSION")" mc-start 1.20.6 ``` @@ -506,13 +560,7 @@ mcc-build ### 3. Run MCC with file input enabled ```bash -mcc-run -``` - -The raw form looks like this: - -```bash -MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - localhost:25565 +mcc-debug -v 1.20.6 --file-input --session "$SESSION" --no-build ``` ### 4. Set up server state through RCON @@ -520,20 +568,20 @@ MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - Examples: ```bash -mc-rcon "op CursorBot" +mc-rcon "op $USERNAME" mc-rcon "gamerule sendCommandFeedback true" -mc-rcon "give CursorBot diamond_sword 1" +mc-rcon "give $USERNAME diamond_sword 1" mc-rcon "summon minecraft:armor_stand ~ ~ ~" ``` -### 5. Drive MCC through `mcc_input.txt` +### 5. Drive MCC through the session input file Examples: ```bash -mcc-cmd "inventory player list" -mcc-cmd "entity" -mcc-cmd "/gamemode creative" +mcc-cmd --session "$SESSION" "inventory player list" +mcc-cmd --session "$SESSION" "entity" +mcc-cmd --session "$SESSION" "/gamemode creative" ``` Behavior: @@ -638,12 +686,15 @@ Use skills: Typical loop: ```bash +source tools/mcc-env.sh +SESSION="smoke-a" +USERNAME="$(_mcc_resolve_username "$SESSION")" mc-start 1.20.6 mcc-build -mcc-run -mc-rcon "op CursorBot" -mcc-cmd "inventory player list" -mcc-cmd "entity" +mcc-debug -v 1.20.6 --file-input --session "$SESSION" --no-build +mc-rcon "op $USERNAME" +mcc-cmd --session "$SESSION" "inventory player list" +mcc-cmd --session "$SESSION" "entity" ``` Then inspect the MCC output, patch the code, and use: diff --git a/docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md b/docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md new file mode 100644 index 00000000..975787b9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md @@ -0,0 +1,789 @@ +# MCC 共享服务器与隔离会话 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让多个 Git worktree 共享同一套本地 Minecraft 测试服务器,同时让每个 MCC 调试会话按 `session` 隔离,并支持按 worktree 选择性把构建输出放到 tmpfs。 + +**Architecture:** 保持 `mc-*` 命令面向共享服务器,保留 `MCC_SERVERS` 作为共享 server root override;把 `mcc-*` 命令改成面向显式 `session` 的客户端会话工具,默认 `session` 取当前 worktree 名,默认用户名从 `session` 派生。构建层通过统一的 helper 和 `Directory.Build.props` 接入可选的 tmpfs 构建根,不再依赖 shell 中泄漏的 `MCC_REPO`。 + +**Tech Stack:** Bash, tmux, dotnet CLI, MSBuild `Directory.Build.props`, repo 自带的 MCC integration harness + +--- + +### Task 1: 建立会话与构建根解析的基础 helper,并补一个可重复运行的 shell smoke test + +**Files:** +- Create: `tools/test-mcc-env.sh` +- Modify: `tools/mcc-env.sh` + +- [ ] **Step 1: 先写一个会失败的 shell smoke test,锁定会话名、用户名和路径解析规则** + +```bash +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$REPO_ROOT/tools/mcc-env.sh" + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [[ "$expected" != "$actual" ]]; then + echo "FAIL: $label" >&2 + echo " expected: $expected" >&2 + echo " actual: $actual" >&2 + exit 1 + fi +} + +assert_regex() { + local regex="$1" + local actual="$2" + local label="$3" + if [[ ! "$actual" =~ $regex ]]; then + echo "FAIL: $label" >&2 + echo " regex: $regex" >&2 + echo " actual: $actual" >&2 + exit 1 + fi +} + +session="$(_mcc_resolve_session "demo-branch")" +assert_eq "demo-branch" "$session" "explicit session" + +short_name="$(_mcc_resolve_username "feature-ai")" +assert_eq "mcc_feature_ai" "$short_name" "short derived username" + +long_name="$(_mcc_resolve_username "very-long-worktree-name")" +assert_regex '^mcc_[a-z0-9_]{7}_[0-9a-f]{4}$' "$long_name" "long derived username shape" +assert_eq "16" "${#long_name}" "long derived username length" + +assert_eq "${TMPDIR:-/tmp}/mcc-debug/demo-branch" "$(_mcc_session_root "demo-branch")" "session root" +assert_eq "mcc-demo-branch" "$(_mcc_tmux_session_name "demo-branch")" "tmux session name" + +MCC_BUILD_MODE=tmpfs +build_root="$(_mcc_build_root)" +assert_regex '^(/dev/shm|/tmp)/mcc-build/.+$' "$build_root" "tmpfs build root" + +echo "PASS" +``` + +- [ ] **Step 2: 运行测试,确认它先失败** + +Run: `bash tools/test-mcc-env.sh` + +Expected: FAIL,错误类似 `_mcc_resolve_session: command not found` + +- [ ] **Step 3: 在 `tools/mcc-env.sh` 中加入 repo root、server root、session、username、会话目录、tmux 名和 build root helper** + +```bash +if [[ -n "${BASH_SOURCE[0]:-}" ]]; then + _mcc_env_source="${BASH_SOURCE[0]}" +elif [[ -n "${ZSH_VERSION:-}" ]]; then + _mcc_env_source="${(%):-%N}" +else + _mcc_env_source="$0" +fi + +TOOLS_DIR="$(cd "$(dirname "$_mcc_env_source")" && pwd)" +MCC_REPO_ROOT="$(cd "$TOOLS_DIR/.." && pwd)" +unset _mcc_env_source + +_mcc_repo_root() { + printf '%s\n' "$MCC_REPO_ROOT" +} + +_mcc_servers_root() { + printf '%s\n' "${MCC_SERVERS:-$MCC_REPO_ROOT/MinecraftOfficial/downloads}" +} + +_mcc_current_worktree_name() { + git -C "$MCC_REPO_ROOT" rev-parse --show-toplevel 2>/dev/null | xargs basename +} + +_mcc_resolve_session() { + local explicit="${1:-}" + if [[ -n "$explicit" ]]; then + printf '%s\n' "$explicit" + return 0 + fi + + local worktree + worktree="$(_mcc_current_worktree_name)" + if [[ -n "$worktree" ]]; then + printf '%s\n' "$worktree" + else + basename "$MCC_REPO_ROOT" + fi +} + +_mcc_sha1_short() { + if command -v sha1sum >/dev/null 2>&1; then + printf '%s' "$1" | sha1sum | awk '{print substr($1, 1, 4)}' + else + printf '%s' "$1" | shasum -a 1 | awk '{print substr($1, 1, 4)}' + fi +} + +_mcc_resolve_username() { + local session="$1" + local normalized + normalized="$(printf '%s' "$session" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g')" + local candidate="mcc_${normalized}" + if (( ${#candidate} <= 16 )); then + printf '%s\n' "$candidate" + return 0 + fi + + local hash + hash="$(_mcc_sha1_short "$normalized")" + printf '%s_%s\n' "${candidate:0:11}" "$hash" +} + +_mcc_session_root() { + printf '%s/mcc-debug/%s\n' "${TMPDIR:-/tmp}" "$1" +} + +_mcc_session_log_file() { + printf '%s/mcc-debug.log\n' "$(_mcc_session_root "$1")" +} + +_mcc_session_input_file() { + printf '%s/mcc_input.txt\n' "$(_mcc_session_root "$1")" +} + +_mcc_session_pid_file() { + printf '%s/mcc.pid\n' "$(_mcc_session_root "$1")" +} + +_mcc_session_meta_file() { + printf '%s/session.meta\n' "$(_mcc_session_root "$1")" +} + +_mcc_tmux_session_name() { + printf 'mcc-%s\n' "$1" +} + +_mcc_build_root() { + local worktree + worktree="$(_mcc_current_worktree_name)" + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + if [[ -d /dev/shm && -w /dev/shm ]]; then + printf '/dev/shm/mcc-build/%s\n' "$worktree" + else + printf '%s/mcc-build/%s\n' "${TMPDIR:-/tmp}" "$worktree" + fi + return 0 + fi + + printf '%s\n' "$MCC_REPO_ROOT" +} +``` + +- [ ] **Step 4: 再跑 smoke test,确认规则都落地** + +Run: `bash tools/test-mcc-env.sh` + +Expected: PASS + +- [ ] **Step 5: 提交这一小步** + +```bash +git add tools/mcc-env.sh tools/test-mcc-env.sh +git commit -m "tools: add MCC session and build root helpers" +``` + +### Task 2: 让 `mcc-*` shell helper 全部按 `session` 工作,并新增安全的 session reset + +**Files:** +- Modify: `tools/mcc-env.sh` +- Test: `tools/test-mcc-env.sh` + +- [ ] **Step 1: 在 smoke test 里加入 `mcc-cmd` 和 `mcc-reset-session` 的行为检查** + +```bash +session="wrapper-smoke" +input_file="$(_mcc_session_input_file "$session")" +rm -rf "$(_mcc_session_root "$session")" + +mcc-cmd --session "$session" "debug state" +grep -Fq "debug state" "$input_file" + +mcc-reset-session --session "$session" +[[ ! -e "$(_mcc_session_root "$session")" ]] +``` + +- [ ] **Step 2: 运行测试,确认它先因为参数解析或命令不存在而失败** + +Run: `bash tools/test-mcc-env.sh` + +Expected: FAIL,错误类似 `mcc-reset-session: command not found` 或 `Unknown option: --session` + +- [ ] **Step 3: 在 `tools/mcc-env.sh` 里把 `mcc-*` helper 改成 session-aware** + +```bash +mcc-build() { + local repo_root + repo_root="$(_mcc_repo_root)" + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + local build_root + build_root="$(_mcc_build_root)" + mkdir -p "$build_root" + MCC_BUILD_ROOT="$build_root" dotnet build "$repo_root/MinecraftClient.sln" -c Release + else + dotnet build "$repo_root/MinecraftClient.sln" -c Release + fi +} + +mcc-cmd() { + local session="" + local command="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="$2"; shift 2 ;; + *) command="$1"; shift ;; + esac + done + + [[ -n "$command" ]] || { echo "Usage: mcc-cmd [--session NAME] " >&2; return 1; } + session="$(_mcc_resolve_session "$session")" + local input_file + input_file="$(_mcc_session_input_file "$session")" + mkdir -p "$(dirname "$input_file")" + printf '%s\n' "$command" >> "$input_file" +} + +mcc-log-mcc() { + local session="${1:-}" + if [[ "$session" == "--session" ]]; then + session="${2:-}" + fi + session="$(_mcc_resolve_session "$session")" + tail -f "$(_mcc_session_log_file "$session")" +} + +mcc-state() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="$2"; shift 2 ;; + *) echo "Unknown option: $1" >&2; return 1 ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + mcc-cmd --session "$session" "debug state" + sleep 1 + tail -30 "$(_mcc_session_log_file "$session")" +} + +mcc-reset-session() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="$2"; shift 2 ;; + *) echo "Unknown option: $1" >&2; return 1 ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + tmux kill-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null || true + rm -rf "$(_mcc_session_root "$session")" +} +``` + +- [ ] **Step 4: 运行 smoke test,确认 session input 与 reset 生效** + +Run: `bash tools/test-mcc-env.sh` + +Expected: PASS + +- [ ] **Step 5: 提交这一小步** + +```bash +git add tools/mcc-env.sh tools/test-mcc-env.sh +git commit -m "tools: scope MCC shell helpers by session" +``` + +### Task 3: 改造 `mcc-debug.sh`、`mcc-log-tail.sh` 和 kill 流程,隔离 tmux、log、config、PID、metadata + +**Files:** +- Modify: `tools/mcc-debug.sh` +- Modify: `tools/mcc-log-tail.sh` +- Modify: `tools/mcc-env.sh` + +- [ ] **Step 1: 用真实命令先验证当前行为会互相覆盖** + +Run: + +```bash +source tools/mcc-env.sh +mcc-debug -v 1.21.11 --file-input --no-build +ls -la /tmp/mcc-debug +tmux list-sessions | grep '^mcc-debug:' +``` + +Expected: 看到固定目录 `/tmp/mcc-debug` 和固定 tmux 名 `mcc-debug` + +- [ ] **Step 2: 在 `tools/mcc-debug.sh` 中加入 `--session` 和 `--username`,并把所有运行时工件改成 session-scoped** + +```bash +SESSION="" +USERNAME="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --session) SESSION="$2"; shift 2 ;; + --username) USERNAME="$2"; shift 2 ;; + -v|--version) VERSION="$2"; shift 2 ;; + -m|--mode) MODE="$2"; shift 2 ;; + -p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;; + --no-build) DO_BUILD=false; shift ;; + --debug-on) DEBUG_ON=true; shift ;; + --file-input) FILE_INPUT=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +SESSION="$(_mcc_resolve_session "$SESSION")" +USERNAME="${USERNAME:-$(_mcc_resolve_username "$SESSION")}" +SESSION_ROOT="$(_mcc_session_root "$SESSION")" +CFG="$SESSION_ROOT/MinecraftClient.debug.ini" +MCC_LOG="$(_mcc_session_log_file "$SESSION")" +INPUT_FILE="$(_mcc_session_input_file "$SESSION")" +PID_FILE="$(_mcc_session_pid_file "$SESSION")" +META_FILE="$(_mcc_session_meta_file "$SESSION")" +MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION")" + +mkdir -p "$SESSION_ROOT" + +cat > "$META_FILE" <&2; return 1 ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + local pid_file meta_file tmux_name + pid_file="$(_mcc_session_pid_file "$session")" + meta_file="$(_mcc_session_meta_file "$session")" + tmux_name="$(_mcc_tmux_session_name "$session")" + + if [[ -f "$pid_file" ]]; then + pid="$(cat "$pid_file")" + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + wait "$pid" 2>/dev/null || true + fi + rm -f "$pid_file" + fi + + tmux kill-session -t "$tmux_name" 2>/dev/null || true + [[ -f "$meta_file" ]] && rm -f "$meta_file" +} +``` + +- [ ] **Step 4: 让 `tools/mcc-log-tail.sh` 支持 `--session`,读取 session-specific log** + +```bash +SESSION="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --session) SESSION="$2"; shift 2 ;; + --server) SERVER_VER="$2"; shift 2 ;; + --server-only) SERVER_ONLY=true; SERVER_VER="$2"; shift 2 ;; + -h|--help) echo "Usage: tools/mcc-log-tail.sh [--session NAME] [--server VER] [--server-only VER]"; exit 0 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +SESSION="$(_mcc_resolve_session "$SESSION")" +MCC_LOG="$(_mcc_session_log_file "$SESSION")" +``` + +- [ ] **Step 5: 用两个显式 session 跑一次真实 smoke** + +Run: + +```bash +source tools/mcc-env.sh +mcc-debug -v 1.21.11 --session smoke-a --username SmokeA --file-input --no-build +mcc-debug -v 1.21.11 --session smoke-b --username SmokeB --file-input --no-build +test -f "$(_mcc_session_log_file smoke-a)" +test -f "$(_mcc_session_log_file smoke-b)" +test -f "$(_mcc_session_meta_file smoke-a)" +test -f "$(_mcc_session_meta_file smoke-b)" +tmux list-sessions | grep '^mcc-smoke-a:' +tmux list-sessions | grep '^mcc-smoke-b:' +``` + +Expected: 两套独立文件与两套 tmux 会话都存在 + +- [ ] **Step 6: 提交这一小步** + +```bash +git add tools/mcc-debug.sh tools/mcc-log-tail.sh tools/mcc-env.sh +git commit -m "tools: isolate MCC debug state by session" +``` + +### Task 4: 加入按 worktree 选择性启用的 tmpfs 构建根,并修正外置 `obj` 的 MSBuild 过滤问题 + +**Files:** +- Create: `Directory.Build.props` +- Modify: `tools/mcc-env.sh` +- Test: `tools/test-mcc-env.sh` + +- [ ] **Step 1: 先用当前仓库验证“直接外置 obj 会失败”,把这个回归固定住** + +Run: + +```bash +rm -rf /tmp/mcc-plan-build-smoke +dotnet build MinecraftClient.sln -c Release -v minimal --nologo \ + -p:BaseOutputPath=/tmp/mcc-plan-build-smoke/bin/ \ + -p:BaseIntermediateOutputPath=/tmp/mcc-plan-build-smoke/obj/ \ + -p:MSBuildProjectExtensionsPath=/tmp/mcc-plan-build-smoke/obj/ +``` + +Expected: FAIL,包含重复 `AssemblyInfo` 或 `TargetFrameworkAttribute` 一类错误 + +- [ ] **Step 2: 新建 `Directory.Build.props`,统一把 `MCC_BUILD_ROOT` 接到每个项目自己的外置 `bin/obj`,并显式排除仓库内 `bin/obj`** + +```xml + + + $(DefaultItemExcludes);**/bin/**;**/obj/** + + + + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/ + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/ + $(BaseIntermediateOutputPath) + + +``` + +- [ ] **Step 3: 在 `tools/mcc-env.sh` 中把 `mcc-build`、`mcc-run`、`mcc-tui`、`mcc-debug` 的 `dotnet` 调用统一接入 `MCC_BUILD_MODE=tmpfs`** + +```bash +_mcc_dotnet_env() { + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + local build_root + build_root="$(_mcc_build_root)" + mkdir -p "$build_root" + env MCC_BUILD_ROOT="$build_root" "$@" + return 0 + fi + + "$@" +} + +mcc-run() { + local session="" username="" port="25565" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="$2"; shift 2 ;; + --username) username="$2"; shift 2 ;; + --port) port="$2"; shift 2 ;; + *) break ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + username="${username:-$(_mcc_resolve_username "$session")}" + mkdir -p "$(_mcc_session_root "$session")" + _mcc_dotnet_env env \ + MCC_FILE_INPUT=1 \ + MCC_INPUT_FILE="$(_mcc_session_input_file "$session")" \ + dotnet run --project "$(_mcc_repo_root)/MinecraftClient" -c Release -- \ + "$(_mcc_session_root "$session")/MinecraftClient.debug.ini" \ + "$username" \ + - \ + "localhost:$port" +} + +mcc-tui() { + local session="" username="" port="25565" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="$2"; shift 2 ;; + --username) username="$2"; shift 2 ;; + --port) port="$2"; shift 2 ;; + *) break ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + username="${username:-$(_mcc_resolve_username "$session")}" + tmux new-session -d -s "$(_mcc_tmux_session_name "$session")" -x 160 -y 50 \ + "cd '$(_mcc_repo_root)' && MCC_BUILD_MODE='${MCC_BUILD_MODE:-local}' bash tools/mcc-debug.sh --session '$session' --username '$username' -p '$port' -m tui --no-build" +} + +mcc-build-clean() { + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + rm -rf "$(_mcc_build_root)" + else + dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release + fi +} +``` + +- [ ] **Step 4: 扩展 smoke test,检查 `tmpfs` build root helper 和清理命令** + +```bash +MCC_BUILD_MODE=tmpfs +build_root="$(_mcc_build_root)" +mkdir -p "$build_root/probe" +mcc-build-clean +[[ ! -e "$build_root/probe" ]] +``` + +- [ ] **Step 5: 用 tmpfs 模式重新 build,确认能成功且产物出现在 worktree 专属构建根** + +Run: + +```bash +source tools/mcc-env.sh +export MCC_BUILD_MODE=tmpfs +mcc-build +find "$(_mcc_build_root)" -maxdepth 3 -type d | sed -n '1,20p' +``` + +Expected: `Build succeeded.`,并且构建目录位于 `/dev/shm/mcc-build//` 或 `/tmp/mcc-build//` + +- [ ] **Step 6: 提交这一小步** + +```bash +git add Directory.Build.props tools/mcc-env.sh tools/test-mcc-env.sh +git commit -m "build: add worktree-aware tmpfs build mode" +``` + +### Task 5: 更新 integration harness,并新增“单服务器双会话”自动化 smoke test + +**Files:** +- Create: `.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh` +- Modify: `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh` +- Modify: `.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh` +- Modify: `tools/run-creative-e2e.sh` + +- [ ] **Step 1: 新增并行会话 smoke test 脚本,先把预期行为写下来** + +```bash +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +source "$REPO_ROOT/tools/mcc-env.sh" +source "$SCRIPT_DIR/common.sh" + +VERSION="${1:-1.21.11}" +SESSION_A="parallel-a" +SESSION_B="parallel-b" +USER_A="ParallelA" +USER_B="ParallelB" + +bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null +bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null +mcc-reset-session --session "$SESSION_A" +mcc-reset-session --session "$SESSION_B" +mcc-build +mc-start "$VERSION" >/dev/null +wait_for_server_ready "$VERSION" + +mcc-debug -v "$VERSION" --session "$SESSION_A" --username "$USER_A" --file-input --no-build +mcc-debug -v "$VERSION" --session "$SESSION_B" --username "$USER_B" --file-input --no-build + +grep -Fq "Server was successfully joined" "$(_mcc_session_log_file "$SESSION_A")" +grep -Fq "Server was successfully joined" "$(_mcc_session_log_file "$SESSION_B")" +mcc-cmd --session "$SESSION_A" "debug state" +mcc-cmd --session "$SESSION_B" "debug state" + +mcc-kill --session "$SESSION_A" +tmux has-session -t "$(_mcc_tmux_session_name "$SESSION_B")" +mc-log "$VERSION" 50 | grep -Fq "$USER_B joined the game" +``` + +- [ ] **Step 2: 运行脚本,确认它先因为新参数或旧的共享路径逻辑而失败** + +Run: `bash .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh 1.21.11` + +Expected: FAIL,错误类似 `Unknown option: --session`、固定 `mcc_input.txt` 被共用,或者只有一个客户端会话存活 + +- [ ] **Step 3: 把 `reset_shared_test_state.sh` 缩回“只管共享服务器”,不要再删除 repo 根下 `mcc_input.txt`** + +```bash +if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then + while IFS= read -r session_name; do + [[ -z "$session_name" ]] && continue + kill_named_session "$session_name" + done < <(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true) + + while IFS= read -r pipe_path; do + [[ -z "$pipe_path" ]] && continue + if [[ ! -p "$pipe_path" ]]; then + rm -f "$pipe_path" + fi + done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true) +fi +``` + +- [ ] **Step 4: 更新现有 integration 脚本,让它们使用 session-specific input/log,而不是 repo 根下固定文件** + +```bash +SESSION="creative-e2e-${SERVER_DIR//\//_}" +USERNAME="$(_mcc_resolve_username "$SESSION")" +INPUT_FILE="$(_mcc_session_input_file "$SESSION")" +MCC_LOG="$(_mcc_session_log_file "$SESSION")" + +rm -rf "$(_mcc_session_root "$SESSION")" +mkdir -p "$(_mcc_session_root "$SESSION")" + +( + cd "$REPO_ROOT" + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + "$CFG" \ + "$USERNAME" \ + - \ + "localhost:$SERVER_PORT" \ + > "$MCC_LOG" 2>&1 +) & +``` + +- [ ] **Step 5: 重新运行并行 smoke test 与现有 creative/full-spectrum 回归** + +Run: + +```bash +bash .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh 1.21.11 +bash tools/run-creative-e2e.sh 1.21.11 1.21.11 modern +bash .skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh 1.21.11 +``` + +Expected: 三个脚本都 PASS;并行 smoke test 中一个 session 被 kill 后,另一个 session 和共享服务器继续存活 + +- [ ] **Step 6: 提交这一小步** + +```bash +git add .skills/mcc-integration-testing/scripts/reset_shared_test_state.sh \ + .skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh \ + .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh \ + tools/run-creative-e2e.sh +git commit -m "test: cover shared server with isolated MCC sessions" +``` + +### Task 6: 更新文档,并给出两个 worktree 并行调试的标准操作流程 + +**Files:** +- Modify: `tools/README.md` +- Modify: `docs/guide/ai-assisted-development.md` +- Modify: `.skills/mcc-dev-workflow/SKILL.md` + +- [ ] **Step 1: 在文档里先补“共享服务器,隔离 MCC 会话”的核心规则** + +```md +## Shared Server, Isolated MCC Sessions + +- `mc-*` commands operate on the shared local Minecraft server. +- `mcc-*` commands operate on one MCC client session. +- Default `session` = current worktree name. +- Default username is derived from `session`, so two worktrees can join the same server without kicking each other. +``` + +- [ ] **Step 2: 在 `mcc-dev-workflow` 文档和 skill 里补两个 worktree 的真实示例** + +````md +```bash +# worktree A +cd ~/Minecraft/Minecraft-Console-Client +source tools/mcc-env.sh +mc-start 1.21.11 +mcc-debug -v 1.21.11 --file-input + +# worktree B +cd ~/Minecraft/Minecraft-Console-Client-foo +source tools/mcc-env.sh +mcc-debug -v 1.21.11 --file-input + +# Each worktree gets: +# - its own session +# - its own username +# - its own log/input/config/tmux session +# - the same shared server +``` +```` + +- [ ] **Step 3: 在文档里加入 tmpfs 构建模式与清理命令** + +````md +### tmpfs build mode + +```bash +source tools/mcc-env.sh +export MCC_BUILD_MODE=tmpfs +mcc-build +mcc-build-clean +``` + +When `MCC_BUILD_MODE=tmpfs`, build outputs are redirected to `/dev/shm/mcc-build//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` if `/dev/shm` is unavailable. +```` + +- [ ] **Step 4: 跑最终验证矩阵并保存关键输出** + +Run: + +```bash +bash tools/test-mcc-env.sh +source tools/mcc-env.sh && unset MCC_BUILD_MODE && mcc-build +source tools/mcc-env.sh && export MCC_BUILD_MODE=tmpfs && mcc-build +bash .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh 1.21.11 +``` + +Expected: + +```text +PASS +Build succeeded. +Build succeeded. +parallel session smoke: PASS +``` + +- [ ] **Step 5: 提交文档与最终验证结果** + +```bash +git add tools/README.md docs/guide/ai-assisted-development.md .skills/mcc-dev-workflow/SKILL.md +git commit -m "docs: document shared server and isolated MCC sessions" +``` + +### Self-Review Checklist + +- [ ] 计划里的 `session`、`username`、`MCC_BUILD_MODE`、`MCC_BUILD_ROOT` 命名在所有任务中一致 +- [ ] 没有任何步骤重新引入 repo 根下固定 `mcc_input.txt` +- [ ] 没有任何步骤重新使用 `pkill -f "MinecraftClient"` +- [ ] 并行 smoke test 明确验证“共享服务器 + 双会话 + 单边 kill 不影响另一边” +- [ ] tmpfs build 任务明确覆盖了当前已知的 `obj` 外置重复编译问题 diff --git a/docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md b/docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md new file mode 100644 index 00000000..9fc0e376 --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md @@ -0,0 +1,316 @@ +# Shared Server, Isolated MCC Sessions Design + +## Goal + +Keep local Minecraft server instances shared across worktrees while making MCC debug sessions fully isolated by default. + +The desired workflow is: + +- Multiple Git worktrees can build in parallel without output collisions. +- One shared local test server can be reused by multiple MCC clients. +- Multiple MCC debug sessions can connect to that shared server at the same time without clobbering each other's tmux sessions, logs, temp config, input file, PID tracking, or usernames. + +## Background + +The current repository tooling mixes two different scopes: + +- Shared infrastructure state, such as local server jars and tmux server sessions +- Per-MCC-client debug state, such as `mcc_input.txt`, `/tmp/mcc-debug`, and the fixed `mcc-debug` tmux session + +That works for one active workspace, but it breaks down once multiple worktrees are used at the same time. + +Today the main problems are: + +1. `MCC_REPO` can leak from one shell into another and point helper commands at the wrong worktree. +2. `mcc-debug.sh` writes all temp artifacts into the same `/tmp/mcc-debug` directory. +3. MCC tmux sessions use the same fixed name, `mcc-debug`. +4. MCC helper commands such as `mcc-kill` operate globally instead of targeting one debug session. +5. The default MCC username is fixed, so two clients connecting to the same server will kick each other. +6. Build outputs live inside each worktree by default, which is correct for isolation, but the workflow does not offer an intentional tmpfs-backed fast path for machines with large RAM. + +## Non-Goals + +- Do not isolate or duplicate server assets by worktree. +- Do not support multiple independent servers with the same version name running in parallel in this change. +- Do not redesign the Minecraft runtime or MCC account model. +- Do not make tmpfs build output mandatory. + +## Design Principles + +- Shared resources stay explicit and few. +- Per-session MCC state is isolated by default. +- Repo discovery is local to each script, not inherited from an ambient shell variable. +- Existing workflows should keep working with minimal changes when only one MCC session is active. +- Performance optimizations must not undermine correctness or isolation. + +## State Model + +### Shared State + +The following remain shared across worktrees and shells: + +- `MCC_SERVERS`, when explicitly set by the user +- Default server root at `/MinecraftOfficial/downloads` when `MCC_SERVERS` is not set +- Server tmux session names, still keyed by Minecraft version, for example `mc-1_21_11` +- Server stdin pipes and world data under the shared server root +- RCON access to the shared server + +### Per-Session MCC State + +Each MCC debug session is keyed by a session identifier. + +The following must be isolated per session: + +- MCC tmux session +- Temp config +- MCC log +- MCC input file +- MCC PID file +- Session metadata used by helper commands +- Default username + +### Per-Worktree Build State + +Each worktree gets its own build output root. + +Build isolation is keyed by worktree name, not MCC session name, so multiple MCC sessions from one worktree can share the same compiled binaries. + +## Naming and Identity + +### Session Name + +- A new `session` concept is introduced for all `mcc-*` commands. +- If the user passes `--session NAME`, that value is used. +- Otherwise the default is the current Git worktree name. +- If a worktree name cannot be resolved, the fallback is the basename of the current repo root. + +### Username + +- If the user passes `--username NAME`, that value is used. +- Otherwise the username is derived from the resolved `session`. +- The derived name must: + - use only Minecraft-safe characters already accepted by the existing config flow + - be deterministic + - be at most 16 characters + - remain stable for the same session across runs + +Recommended derivation: + +1. Lowercase the session name. +2. Replace invalid characters with underscores. +3. Prefix with `mcc_`. +4. If the result is longer than 16 characters, keep the first 11 characters and append `_` plus the first 4 hexadecimal characters of the SHA-1 of the normalized session string. + +Example: + +- `feature-ai` -> `mcc_feature_ai` +- `very-long-worktree-name` -> `mcc_veryl_ab12` + +This truncation algorithm must be implemented and tested exactly as written so future changes do not silently rename active debug identities. + +## Repo and Server Root Resolution + +### Repo Root + +Helper scripts must stop exporting `MCC_REPO` as shell-global state. + +Instead: + +- Each script resolves its own repo root from its own location. +- Shared shell helper functions may cache that resolved path internally, but not as a required ambient variable. +- Child commands should receive explicit paths or recompute the repo root themselves. + +This removes the current failure mode where a shell sourced from one clone or worktree accidentally drives tools in another. + +### Server Root + +`MCC_SERVERS` stays as the supported override for the shared server root. + +Resolution order: + +1. If `MCC_SERVERS` is set, use it. +2. Otherwise use `/MinecraftOfficial/downloads`. + +This keeps the useful part of the current workflow: one intentionally shared set of server assets. + +## File and Process Layout + +### MCC Session Root + +Each MCC session gets a unique root directory: + +- `${TMPDIR:-/tmp}/mcc-debug//` + +That directory contains: + +- `MinecraftClient.debug.ini` +- `mcc-debug.log` +- `mcc_input.txt` +- `mcc.pid` +- `session.meta` + +### tmux Sessions + +MCC tmux sessions must be renamed from the fixed `mcc-debug` to a session-scoped name, for example: + +- `mcc-` + +Server tmux sessions remain version-scoped: + +- `mc-` + +### Kill and Reset Scope + +- `mcc-kill --session X` only stops the MCC process and tmux session for `X`. +- A new `mcc-reset-session` command should clear only session-scoped files and tmux sessions. +- Existing shared server reset logic must continue to operate on server state only. +- No `mcc-*` command may kill all `MinecraftClient` processes by pattern. + +## Command Interface + +### Shared Server Commands + +These remain shared-resource commands and do not take `--session`: + +- `mc-start` +- `mc-stop` +- `mc-log` +- `mc-rcon` +- `mc-wait-ready` +- `mc-wait-stop` + +### Session-Scoped MCC Commands + +These commands gain `--session`, and where applicable `--username`: + +- `mcc-debug` +- `mcc-run` +- `mcc-tui` +- `mcc-cmd` +- `mcc-log-mcc` +- `mcc-state` +- `mcc-kill` + +Expected defaults: + +- `--session`: current worktree name +- `--username`: derived from session + +Example commands: + +```bash +mcc-debug -v 1.21.11 --session alice-a --username AliceA --file-input +mcc-debug -v 1.21.11 --session alice-b --username AliceB --file-input +mcc-cmd --session alice-a "debug state" +mcc-log-mcc --session alice-b +mcc-kill --session alice-a +mcc-reset-session --session alice-b +``` + +## Build Output Isolation + +### Base Rule + +Build outputs must be isolated by worktree, not mixed across worktrees. + +This should be implemented by introducing a build root override passed through the helper scripts into `dotnet build` and `dotnet run`, without requiring users to manually edit project files per worktree. + +The important behavior is: + +- Worktree A writes to build root A +- Worktree B writes to build root B +- Both can build concurrently without sharing `bin/obj` + +### tmpfs Acceleration + +Add an opt-in tmpfs-backed build root for machines with large RAM. + +Suggested layout: + +- `/dev/shm/mcc-build//` on Linux +- fallback to `${TMPDIR:-/tmp}/mcc-build//` when `/dev/shm` is unavailable + +Requirements: + +- tmpfs mode is optional, not mandatory +- the resolved build root must be printed in debug output +- a new `mcc-build-clean` command should clear build outputs for the current worktree +- helper scripts must fail clearly if the chosen build root is not writable + +### Compatibility + +If tmpfs mode is disabled, behavior should remain functionally equivalent to today, except for the added worktree-aware path selection. + +## Backward Compatibility + +- Existing single-session workflows should still work without requiring `--session`. +- Existing external `MCC_SERVERS` setups must continue to work. +- Existing server-side helper scripts should keep their public behavior. +- Existing documentation examples can be updated gradually, but the basic commands should remain recognizable. + +## Error Handling + +The tooling should fail early and clearly for: + +- missing server root +- invalid or empty session name +- invalid derived username after normalization +- requested tmux session already in use by a different live MCC process +- unwritable tmpfs or build root +- missing PID file on targeted kill operations + +When possible, the error should print the resolved repo root, shared server root, session name, username, and session root to make misconfiguration obvious. + +## Validation Plan + +### Manual Verification Matrix + +1. Build from two different worktrees at the same time and confirm isolated output roots. +2. Start one shared `1.21.11` server and confirm only one `mc-1_21_11` session exists. +3. Launch two MCC sessions from two different worktrees without explicit usernames and confirm distinct derived usernames. +4. Join both clients to the shared server and confirm neither client disconnects the other. +5. Send different commands through each session's input file and confirm only the intended client responds. +6. Tail each session's log and confirm no cross-session log mixing. +7. Kill one MCC session and confirm: + - the other MCC session stays connected + - the shared server remains running +8. Enable tmpfs build mode in one worktree and verify outputs are written to the resolved tmpfs path. +9. Rebuild after cleaning tmpfs outputs and confirm the worktree still functions. + +### Regression Checks + +- Single-session debug loop still works with no explicit `--session` +- Shared server helpers still work when `MCC_SERVERS` points outside the repo +- TUI mode still works inside tmux with session-specific naming + +## Implementation Notes + +The most likely files to change are: + +- `tools/mcc-env.sh` +- `tools/mcc-debug.sh` +- `tools/start-server.sh` +- `.skills/mcc-integration-testing/scripts/preflight_test_env.sh` +- `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh` +- `tools/README.md` +- `docs/guide/ai-assisted-development.md` +- `.skills/mcc-dev-workflow/SKILL.md` + +If build-output redirection is implemented through MSBuild configuration rather than pure shell arguments, related project or props files may also need changes. + +## Open Decisions Resolved In This Design + +- Keep `MCC_SERVERS`: yes +- Keep `MCC_REPO` as shell-global state: no +- Shared server instances across worktrees: yes +- MCC debug isolation keyed by explicit `session`: yes +- Default `session`: current worktree name +- Default username: derived from session +- Build output isolation key: current worktree +- tmpfs build output: opt-in optimization + +## Summary + +The resulting workflow intentionally shares the expensive and durable part of local MCC development, the server installation and running server instance, while isolating the volatile and user-specific part, the MCC client debug session and build outputs. + +That gives parallel worktree development without forcing duplicate local servers, while removing the current collisions around shell state, tmux naming, temp files, input routing, and username reuse. diff --git a/tools/README.md b/tools/README.md index 7d33d2cf..5c9d29cd 100644 --- a/tools/README.md +++ b/tools/README.md @@ -4,6 +4,38 @@ Scripts for analyzing Minecraft version differences and generating MCC palette f Requires: Python 3.10+ +## Local debug helpers + +The `tools/` directory also contains the shell helpers used for day-to-day MCC debugging: + +```bash +source tools/mcc-env.sh +mc-start 1.21.11 +mcc-debug -v 1.21.11 --file-input +mcc-cmd "debug state" +``` + +### Shared server, isolated MCC sessions + +- `mc-*` commands operate on the shared local Minecraft server. +- `mcc-*` commands operate on one MCC client session. +- The default `session` is the current worktree name. +- The default username is derived from `session`, so two worktrees can join the same shared server without kicking each other. +- `MCC_SERVERS` remains the shared server-root override. + +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. + +### tmpfs build mode + +```bash +source tools/mcc-env.sh +export MCC_BUILD_MODE=tmpfs +mcc-build +mcc-build-clean +``` + +When `MCC_BUILD_MODE=tmpfs`, build output goes to `/dev/shm/mcc-build//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` when `/dev/shm` is unavailable. + ## Data Sources Two types of data can be used as input: diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index c5c6205e..d0ca0e78 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -16,6 +16,8 @@ Options: -v, --version VER Server directory name (default: 1.21.11-Vanilla) -m, --mode MODE Console mode: classic or tui (default: classic) -p, --port PORT Server port (default: 25565) + --session NAME Session name for scoped runtime artifacts + --username NAME MCC username (default: resolved from session) --no-build Skip dotnet build --debug-on Enable debug messages from the start --file-input Use FileInput mode (classic only; enables mcc-cmd) @@ -25,6 +27,7 @@ Examples: tools/mcc-debug.sh # Classic mode, default server tools/mcc-debug.sh -m tui # TUI mode tools/mcc-debug.sh -v 1.21.11-Vanilla --debug-on + tools/mcc-debug.sh --session smoke-a --username SmokeA tools/mcc-debug.sh --file-input # FileInput for script-driven testing EOF } @@ -33,15 +36,57 @@ VERSION="1.21.11-Vanilla" MODE="classic" PORT="25565" PORT_SET_BY_USER=false +SESSION="" +USERNAME="" DO_BUILD=true DEBUG_ON=false FILE_INPUT=false +BUILD_ROOT="$(_mcc_build_root)" +BUILD_ROOT_ENV_PREFIX="" while [[ $# -gt 0 ]]; do case "$1" in - -v|--version) VERSION="$2"; shift 2 ;; - -m|--mode) MODE="$2"; shift 2 ;; - -p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;; + -v|--version) + if [[ $# -lt 2 ]]; then + echo "$1 requires a value" >&2 + exit 1 + fi + VERSION="$2" + shift 2 + ;; + -m|--mode) + if [[ $# -lt 2 ]]; then + echo "$1 requires a value" >&2 + exit 1 + fi + MODE="$2" + shift 2 + ;; + -p|--port) + if [[ $# -lt 2 ]]; then + echo "$1 requires a value" >&2 + exit 1 + fi + PORT="$2" + PORT_SET_BY_USER=true + shift 2 + ;; + --session) + if [[ $# -lt 2 ]]; then + echo "--session requires a value" >&2 + exit 1 + fi + SESSION="$2" + shift 2 + ;; + --username) + if [[ $# -lt 2 ]]; then + echo "--username requires a value" >&2 + exit 1 + fi + USERNAME="$2" + shift 2 + ;; --no-build) DO_BUILD=false; shift ;; --debug-on) DEBUG_ON=true; shift ;; --file-input) FILE_INPUT=true; shift ;; @@ -50,23 +95,45 @@ while [[ $# -gt 0 ]]; do esac done -TEST_ROOT="${TMPDIR:-/tmp}/mcc-debug" -CFG="$TEST_ROOT/MinecraftClient.debug.ini" -MCC_LOG="$TEST_ROOT/mcc-debug.log" -INPUT_FILE="$REPO_ROOT/mcc_input.txt" +SESSION="$(_mcc_resolve_session "$SESSION")" +if [[ -z "$USERNAME" ]]; then + USERNAME="$(_mcc_resolve_username "$SESSION")" +fi + +if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + mkdir -p "$BUILD_ROOT" + printf -v BUILD_ROOT_QUOTED '%q' "$BUILD_ROOT" + BUILD_ROOT_ENV_PREFIX="MCC_BUILD_ROOT=$BUILD_ROOT_QUOTED " +fi + +SESSION_ROOT="$(_mcc_session_root "$SESSION")" +CFG="$SESSION_ROOT/MinecraftClient.debug.ini" +MCC_LOG="$(_mcc_session_log_file "$SESSION")" +INPUT_FILE="$(_mcc_session_input_file "$SESSION")" +PID_FILE="$(_mcc_session_pid_file "$SESSION")" +META_FILE="$(_mcc_session_meta_file "$SESSION")" +MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION")" SESSION_NAME="mc-${VERSION//\./_}" PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" PREFLIGHT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" GET_PORT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" -mkdir -p "$TEST_ROOT" +mkdir -p "$SESSION_ROOT" echo "=== MCC Debug Session ===" echo " Server: $VERSION (port $PORT)" +echo " Session: $SESSION" +echo " User: $USERNAME" echo " Mode: $MODE" +echo " Build: $BUILD_ROOT" +echo " Root: $SESSION_ROOT" echo " Config: $CFG" echo " Log: $MCC_LOG" +echo " Input: $INPUT_FILE" +echo " PID: $PID_FILE" +echo " Meta: $META_FILE" +echo " Tmux: $MCC_TMUX_SESSION" echo "" bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null @@ -74,7 +141,7 @@ bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null # --- Build --- if $DO_BUILD; then echo "[1/4] Building MCC..." - dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo + _mcc_dotnet_env dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo echo " Build OK" else echo "[1/4] Build skipped (--no-build)" @@ -82,7 +149,7 @@ fi # --- Prepare config --- echo "[2/4] Preparing config..." -bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" CursorBot >/dev/null +bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" "$USERNAME" >/dev/null if [[ "$MODE" == "tui" ]]; then if [[ "$(uname)" == "Darwin" ]]; then @@ -130,34 +197,71 @@ if ! $PORT_SET_BY_USER; then PORT="$(bash "$GET_PORT_SCRIPT" "$VERSION")" fi +RUNTIME_MODE="$MODE" +if $FILE_INPUT; then + RUNTIME_MODE="${MODE}-file-input" +fi + +cat > "$META_FILE" < "$INPUT_FILE" rm -f "$MCC_LOG" +rm -f "$PID_FILE" -MCC_ARGS=("$CFG" "CursorBot" "-" "localhost:$PORT") +MCC_ARGS=("$CFG" "$USERNAME" "-" "localhost:$PORT") +MCC_ARGS_CMD="$(printf '%q ' "${MCC_ARGS[@]}")" if [[ "$MODE" == "tui" ]]; then - # TUI mode: needs a real tty — no pipes or redirects allowed - tmux kill-session -t mcc-debug 2>/dev/null || true - tmux new-session -d -s mcc-debug -x 160 -y 50 \ - "cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600" + # TUI mode: needs a real tty - no pipes or redirects allowed + tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true + tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ + "cd '$REPO_ROOT' && ${BUILD_ROOT_ENV_PREFIX}dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600" echo "" - echo " TUI mode started in tmux session 'mcc-debug'" + echo " TUI mode started in tmux session '$MCC_TMUX_SESSION'" echo " (TUI mode uses a real terminal; log file is not available, use MCC's /debug command)" echo "" - echo " Attach: tmux attach -t mcc-debug" + echo " Attach: tmux attach -t $MCC_TMUX_SESSION" echo " Detach: Ctrl+B, D" - echo " Kill MCC: tmux kill-session -t mcc-debug" + echo " Kill MCC: tmux kill-session -t $MCC_TMUX_SESSION" echo "" elif $FILE_INPUT; then - # FileInput mode: run in background, drive via mcc_input.txt - ( - cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "${MCC_ARGS[@]}" > "$MCC_LOG" 2>&1 - ) & - MCC_PID=$! + # FileInput mode: run in detached tmux, drive via session-specific input file + tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true + tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ + "cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env ${BUILD_ROOT_ENV_PREFIX}MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD > '$MCC_LOG' 2>&1" + + for _ in $(seq 1 25); do + if [[ -s "$PID_FILE" ]]; then + break + fi + sleep 0.2 + done + if [[ ! -s "$PID_FILE" ]]; then + echo " Failed to capture MCC PID in $PID_FILE" + exit 1 + fi + + MCC_PID="$(tr -cd '0-9' < "$PID_FILE")" + if [[ -z "$MCC_PID" ]]; then + echo " Invalid PID content in $PID_FILE" + exit 1 + fi + echo " MCC PID: $MCC_PID" + echo " MCC PID file: $PID_FILE" echo "" sleep 5 @@ -175,25 +279,28 @@ elif $FILE_INPUT; then echo "" echo " Send commands: echo 'debug state' >> $INPUT_FILE" echo " Tail log: tail -f $MCC_LOG" + echo " Metadata: cat $META_FILE" + echo " Attach (optional): tmux attach -t $MCC_TMUX_SESSION" echo " Stop MCC: echo 'quit' >> $INPUT_FILE" echo " Stop server: mc-stop $VERSION" + echo " shared servers stay up by default; rerun with --confirm only if you really need to stop it" echo "" else - # Interactive classic mode: run in tmux (no pipe — ConsoleInteractive also needs tty) - tmux kill-session -t mcc-debug 2>/dev/null || true - tmux new-session -d -s mcc-debug -x 160 -y 50 \ - "cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600" + # Interactive classic mode: run in tmux (no pipe - ConsoleInteractive also needs tty) + tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true + tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ + "cd '$REPO_ROOT' && ${BUILD_ROOT_ENV_PREFIX}dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600" echo "" - echo " Classic mode started in tmux session 'mcc-debug'" + echo " Classic mode started in tmux session '$MCC_TMUX_SESSION'" echo "" - echo " Attach: tmux attach -t mcc-debug" + echo " Attach: tmux attach -t $MCC_TMUX_SESSION" echo " Detach: Ctrl+B, D" - echo " Kill MCC: tmux kill-session -t mcc-debug" + echo " Kill MCC: tmux kill-session -t $MCC_TMUX_SESSION" echo " Note: Use MCC's built-in /debug command or enable LogToFile for log output" echo "" fi echo "Quick commands:" -echo " mc-rcon 'op CursorBot' # Give operator" +echo " mc-rcon 'op $USERNAME' # Give operator" echo " mc-rcon 'gamemode creative' # Creative mode" -echo " mc-stop $VERSION # Stop server" +echo " mc-stop $VERSION # shared server stays up by default; rerun with --confirm only when needed" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 904a8a00..217f18cc 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -12,36 +12,391 @@ else fi TOOLS_DIR="$(cd "$(dirname "$_mcc_env_source")" && pwd)" +MCC_REPO_ROOT="$(cd "$TOOLS_DIR/.." && pwd)" unset _mcc_env_source -export MCC_REPO="$(cd "$TOOLS_DIR/.." && pwd)" -export MCC_SERVERS="${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}" +export MCC_REPO="$MCC_REPO_ROOT" +export MCC_SERVERS="${MCC_SERVERS:-$MCC_REPO_ROOT/MinecraftOfficial/downloads}" + +_mcc_repo_root() { + printf '%s\n' "$MCC_REPO_ROOT" +} + +_mcc_servers_root() { + printf '%s\n' "${MCC_SERVERS:-$MCC_REPO_ROOT/MinecraftOfficial/downloads}" +} + +_mcc_current_worktree_name() { + local worktree_root + if ! worktree_root="$(git -C "$MCC_REPO_ROOT" rev-parse --show-toplevel 2>/dev/null)"; then + return 0 + fi + + if [[ -z "$worktree_root" ]]; then + return 0 + fi + + basename "$worktree_root" +} + +_mcc_resolve_session() { + local explicit="${1:-}" + if [[ -n "$explicit" ]]; then + printf '%s\n' "$explicit" + return 0 + fi + + local worktree + worktree="$(_mcc_current_worktree_name)" + if [[ -n "$worktree" ]]; then + printf '%s\n' "$worktree" + return 0 + fi + + basename "$MCC_REPO_ROOT" +} + +_mcc_sha1_short() { + if command -v sha1sum >/dev/null 2>&1; then + printf '%s' "$1" | sha1sum | awk '{print substr($1, 1, 4)}' + else + printf '%s' "$1" | shasum -a 1 | awk '{print substr($1, 1, 4)}' + fi +} + +_mcc_resolve_username() { + local session="$1" + local normalized + normalized="$(printf '%s' "$session" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g')" + local candidate="mcc_${normalized}" + if (( ${#candidate} <= 16 )); then + printf '%s\n' "$candidate" + return 0 + fi + + local hash + hash="$(_mcc_sha1_short "$normalized")" + printf '%s_%s\n' "${candidate:0:11}" "$hash" +} + +_mcc_session_root() { + printf '%s/mcc-debug/%s\n' "${TMPDIR:-/tmp}" "$1" +} + +_mcc_session_log_file() { + printf '%s/mcc-debug.log\n' "$(_mcc_session_root "$1")" +} + +_mcc_session_input_file() { + printf '%s/mcc_input.txt\n' "$(_mcc_session_root "$1")" +} + +_mcc_session_pid_file() { + printf '%s/mcc.pid\n' "$(_mcc_session_root "$1")" +} + +_mcc_session_meta_file() { + printf '%s/session.meta\n' "$(_mcc_session_root "$1")" +} + +_mcc_tmux_session_name() { + printf 'mcc-%s\n' "$1" +} + +_mcc_build_root() { + local worktree + worktree="$(_mcc_current_worktree_name)" + if [[ -z "$worktree" ]]; then + worktree="$(basename "$MCC_REPO_ROOT")" + fi + + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + if [[ -d /dev/shm && -w /dev/shm ]]; then + printf '/dev/shm/mcc-build/%s\n' "$worktree" + else + printf '%s/mcc-build/%s\n' "${TMPDIR:-/tmp}" "$worktree" + fi + return 0 + fi + + printf '%s\n' "$MCC_REPO_ROOT" +} + +_mcc_dotnet_env() { + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + local build_root + build_root="$(_mcc_build_root)" + mkdir -p "$build_root" + env MCC_BUILD_ROOT="$build_root" "$@" + return $? + fi + + "$@" +} # Helper: convert version to tmux session name (dots -> underscores) _mc-session() { echo "mc-${1//\./_}"; } +_mc_requires_confirm() { + local action="$1" + local rerun_command="$2" + cat >&2 < "$MCC_SERVERS/$v/stdin.pipe"; } +mc-stop() { + local v="1.20.6" + local version_set=false + local confirm=false + local -a rerun=(mc-stop) + while [[ $# -gt 0 ]]; do + case "$1" in + --confirm) confirm=true; shift ;; + *) + if [[ "$version_set" == false ]]; then + v="$1" + version_set=true + rerun+=("$1") + shift + else + echo "mc-stop: unexpected argument: $1" >&2 + return 1 + fi + ;; + esac + done + if [[ ${#rerun[@]} -eq 1 ]]; then + rerun+=("$v") + fi + if [[ "$confirm" != true ]]; then + _mc_requires_confirm "stop shared server '$v'" "${rerun[*]}" + return 1 + fi + echo "stop" > "$MCC_SERVERS/$v/stdin.pipe" +} mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; } mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; } -mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; } +mc-kill() { + local v="1.20.6" + local version_set=false + local confirm=false + local -a rerun=(mc-kill) + while [[ $# -gt 0 ]]; do + case "$1" in + --confirm) confirm=true; shift ;; + *) + if [[ "$version_set" == false ]]; then + v="$1" + version_set=true + rerun+=("$1") + shift + else + echo "mc-kill: unexpected argument: $1" >&2 + return 1 + fi + ;; + esac + done + if [[ ${#rerun[@]} -eq 1 ]]; then + rerun+=("$v") + fi + if [[ "$confirm" != true ]]; then + _mc_requires_confirm "force-kill shared server '$v'" "${rerun[*]}" + return 1 + fi + local s + s=$(_mc-session "$v") + tmux kill-session -t "$s" 2>/dev/null + rm -f "$MCC_SERVERS/$v/stdin.pipe" + echo "Killed $s" +} mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; } mc-wait-ready() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "${1:-1.20.6}" >/dev/null && source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_ready "${1:-1.20.6}" "${2:-60}"; } mc-wait-stop() { source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_stop "${1:-1.20.6}" "${2:-60}"; } -mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$@"; } +mc-reset-test-env() { + local confirm=false + local -a args=() + local -a rerun=(mc-reset-test-env) + while [[ $# -gt 0 ]]; do + case "$1" in + --confirm) confirm=true; shift ;; + *) + args+=("$1") + rerun+=("$1") + shift + ;; + esac + done + if [[ "$confirm" != true ]]; then + _mc_requires_confirm "reset shared server test state" "${rerun[*]}" + return 1 + fi + bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "${args[@]}" +} # --- RCON --- mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } # --- MCC Build/Run --- -mcc-build() { dotnet build "$MCC_REPO/MinecraftClient.sln" -c Release; } -mcc-run() { - local port="${1:-25565}" - shift || true - cd "$MCC_REPO" && MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1 +mcc-build() { + local repo_root + repo_root="$(_mcc_repo_root)" + _mcc_dotnet_env dotnet build "$repo_root/MinecraftClient.sln" -c Release +} +mcc-build-clean() { + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + local build_root + build_root="$(_mcc_build_root)" + rm -rf "$build_root" + return 0 + fi + + dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release +} +mcc-run() { + local session="" username="" port="25565" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-run: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + --username) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-run: --username requires a value" >&2 + return 1 + fi + username="$1" + shift + ;; + --port) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-run: --port requires a value" >&2 + return 1 + fi + port="$1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + return 1 + ;; + esac + done + + local -a args=(--file-input --no-build --port "$port") + if [[ -n "$session" ]]; then + args+=(--session "$session") + fi + if [[ -n "$username" ]]; then + args+=(--username "$username") + fi + + bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}" +} +mcc-cmd() { + local session="" + local -a command_parts=() + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-cmd: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + *) + command_parts+=("$1") + shift + ;; + esac + done + + if [[ ${#command_parts[@]} -eq 0 ]]; then + echo "Usage: mcc-cmd [--session NAME] " >&2 + return 1 + fi + session="$(_mcc_resolve_session "$session")" + local input_file + input_file="$(_mcc_session_input_file "$session")" + mkdir -p "$(dirname "$input_file")" + local command + command="${command_parts[*]}" + printf '%s\n' "$command" >> "$input_file" +} +mcc-kill() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-kill: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + return 1 + ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + local pid_file meta_file tmux_session pid pid_comm pid_args + local killed=false + pid_file="$(_mcc_session_pid_file "$session")" + meta_file="$(_mcc_session_meta_file "$session")" + tmux_session="$(_mcc_tmux_session_name "$session")" + + if [[ -f "$pid_file" ]]; then + pid="$(tr -cd '0-9' < "$pid_file")" + if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then + pid_comm="$(ps -p "$pid" -o comm= 2>/dev/null | tr -d '[:space:]')" + pid_args="$(ps -p "$pid" -o args= 2>/dev/null || true)" + if [[ "$pid_comm" == "MinecraftClient" ]] || { [[ "$pid_comm" == "dotnet" ]] && [[ "$pid_args" == *"MinecraftClient"* ]]; }; then + kill "$pid" 2>/dev/null || true + echo "Killed MCC PID $pid for session '$session'" + killed=true + else + echo "Refusing to kill PID $pid for session '$session': unexpected process '$pid_comm'" + fi + else + echo "No live MCC PID found for session '$session' (pid file: $pid_file)" + fi + fi + + if tmux has-session -t "$tmux_session" 2>/dev/null; then + tmux kill-session -t "$tmux_session" 2>/dev/null || true + echo "Killed tmux session '$tmux_session'" + killed=true + fi + + if [[ -f "$pid_file" || -f "$meta_file" ]]; then + rm -f "$pid_file" "$meta_file" + fi + + if [[ "$killed" == false ]]; then + echo "No MCC process or tmux session found for session '$session'" + fi } -mcc-cmd() { echo "$1" >> "$MCC_REPO/mcc_input.txt"; } -mcc-kill() { pkill -f "MinecraftClient" 2>/dev/null && echo "MCC killed" || echo "No MCC process found"; tmux kill-session -t mcc-debug 2>/dev/null || true; } mcc-reload() { mcc-kill sleep 1 @@ -50,16 +405,144 @@ mcc-reload() { # --- TUI Mode --- mcc-tui() { - local port="${1:-25565}" - shift || true - tmux new-session -d -s mcc-debug -x 160 -y 50 \ - "cd '$MCC_REPO' && dotnet run --project MinecraftClient -c Release -- CursorBot - localhost:${port} $* 2>&1; echo '=== MCC EXITED ==='; sleep 600" - echo "TUI mode launched in tmux session 'mcc-debug'" - echo "Attach: tmux attach -t mcc-debug" + local session="" username="" port="25565" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-tui: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + --username) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-tui: --username requires a value" >&2 + return 1 + fi + username="$1" + shift + ;; + --port) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-tui: --port requires a value" >&2 + return 1 + fi + port="$1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + return 1 + ;; + esac + done + + local -a args=(-m tui --no-build --port "$port") + if [[ -n "$session" ]]; then + args+=(--session "$session") + fi + if [[ -n "$username" ]]; then + args+=(--username "$username") + fi + + bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}" +} + +_mcc_session_log_tail() { + local session="$1" + local log_file + log_file="$(_mcc_session_log_file "$session")" + if [[ -e "$log_file" ]]; then + tail -n 30 "$log_file" 2>/dev/null + else + echo "No MCC log found" + fi +} + +_mcc_session_log_follow() { + local session="$1" + local log_file + log_file="$(_mcc_session_log_file "$session")" + tail -f "$log_file" 2>/dev/null || echo "No MCC log found" } # --- Debug helpers --- mcc-debug() { bash "$MCC_REPO/tools/mcc-debug.sh" "$@"; } -mcc-log-mcc() { tail -f "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null || echo "No MCC log found"; } -mcc-state() { echo "debug state" >> "$MCC_REPO/mcc_input.txt"; sleep 1; tail -30 "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null; } +mcc-log-mcc() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-log-mcc: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + return 1 + ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + _mcc_session_log_follow "$session" +} +mcc-state() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-state: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + return 1 + ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + mcc-cmd --session "$session" "debug state" + sleep 1 + _mcc_session_log_tail "$session" +} mcc-preflight() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$@"; } +mcc-reset-session() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-reset-session: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + return 1 + ;; + esac + done + + session="$(_mcc_resolve_session "$session")" + tmux kill-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null || true + rm -rf "$(_mcc_session_root "$session")" +} diff --git a/tools/mcc-log-tail.sh b/tools/mcc-log-tail.sh index fb888a42..2fc269bb 100644 --- a/tools/mcc-log-tail.sh +++ b/tools/mcc-log-tail.sh @@ -2,6 +2,7 @@ # Tail MCC and/or server logs side-by-side or individually. # Usage: # tools/mcc-log-tail.sh # tail MCC log only +# tools/mcc-log-tail.sh --session NAME # tail MCC log for a specific session # tools/mcc-log-tail.sh --server VER # tail both MCC and server logs # tools/mcc-log-tail.sh --server-only VER # tail server log only set -euo pipefail @@ -11,21 +12,47 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" -MCC_LOG="${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" +SESSION="" SERVER_VER="" SERVER_ONLY=false while [[ $# -gt 0 ]]; do case "$1" in - --server) SERVER_VER="$2"; shift 2 ;; - --server-only) SERVER_ONLY=true; SERVER_VER="$2"; shift 2 ;; + --session) + if [[ $# -lt 2 ]]; then + echo "--session requires a value" >&2 + exit 1 + fi + SESSION="$2" + shift 2 + ;; + --server) + if [[ $# -lt 2 ]]; then + echo "--server requires a value" >&2 + exit 1 + fi + SERVER_VER="$2" + shift 2 + ;; + --server-only) + if [[ $# -lt 2 ]]; then + echo "--server-only requires a value" >&2 + exit 1 + fi + SERVER_ONLY=true + SERVER_VER="$2" + shift 2 + ;; -h|--help) - echo "Usage: tools/mcc-log-tail.sh [--server VER] [--server-only VER]" + echo "Usage: tools/mcc-log-tail.sh [--session NAME] [--server VER] [--server-only VER]" exit 0 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done +SESSION="$(_mcc_resolve_session "$SESSION")" +MCC_LOG="$(_mcc_session_log_file "$SESSION")" + if $SERVER_ONLY; then if [[ -z "$SERVER_VER" ]]; then echo "Specify server version with --server-only VER" >&2 @@ -39,16 +66,17 @@ fi if [[ -n "$SERVER_VER" ]]; then SERVER_LOG="$MCC_SERVERS/$SERVER_VER/logs/latest.log" echo "=== Tailing MCC + server logs ===" + echo " Session: $SESSION" echo " MCC: $MCC_LOG" echo " Server: $SERVER_LOG" echo "" tail -f "$MCC_LOG" "$SERVER_LOG" 2>/dev/null else if [[ ! -f "$MCC_LOG" ]]; then - echo "No MCC log found at $MCC_LOG" - echo "Start MCC first with: tools/mcc-debug.sh --file-input" + echo "No MCC log found for session '$SESSION' at $MCC_LOG" + echo "Start MCC first with: tools/mcc-debug.sh --session $SESSION --file-input" exit 1 fi - echo "=== Tailing MCC log: $MCC_LOG ===" + echo "=== Tailing MCC log for session '$SESSION': $MCC_LOG ===" exec tail -f "$MCC_LOG" fi diff --git a/tools/run-creative-e2e.sh b/tools/run-creative-e2e.sh index dce850d4..ea25a464 100644 --- a/tools/run-creative-e2e.sh +++ b/tools/run-creative-e2e.sh @@ -36,10 +36,13 @@ fi SESSION_NAME="mc-${SERVER_DIR//./_}" TEST_ROOT="${TMPDIR:-/tmp}/mcc-creative-e2e/${SERVER_DIR//\//_}" CFG="$TEST_ROOT/MinecraftClient.$MC_VERSION.ini" -MCC_LOG="$TEST_ROOT/mcc.log" +MCC_SESSION="creative-e2e-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}" +TEST_USERNAME="$(_mcc_resolve_username "$MCC_SESSION")" +MCC_LOG="$(_mcc_session_log_file "$MCC_SESSION")" +PID_FILE="$(_mcc_session_pid_file "$MCC_SESSION")" +MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$MCC_SESSION")" SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log" -INPUT_FILE="$REPO_ROOT/mcc_input.txt" -MCC_PID="" +INPUT_FILE="$(_mcc_session_input_file "$MCC_SESSION")" SERVER_PORT="25565" mkdir -p "$TEST_ROOT" @@ -63,12 +66,41 @@ wait_for_file_pattern() { return 1 } +port_is_listening() { + local port="$1" + + if ! command -v python3 >/dev/null 2>&1; then + return 1 + fi + + python3 - "$port" <<'PY' +import socket +import sys + +port = int(sys.argv[1]) + +for addrinfo in socket.getaddrinfo("localhost", port, 0, socket.SOCK_STREAM): + family, socktype, proto, _, sockaddr = addrinfo + try: + sock = socket.socket(family, socktype, proto) + sock.settimeout(0.2) + if sock.connect_ex(sockaddr) == 0: + sock.close() + raise SystemExit(0) + sock.close() + except OSError: + continue + +raise SystemExit(1) +PY +} + wait_for_rcon_port_free() { local timeout="${1:-30}" local elapsed=0 while (( elapsed < timeout )); do - if ! ss -ltn '( sport = :25575 )' 2>/dev/null | grep -Fq ':25575'; then + if ! port_is_listening 25575; then return 0 fi sleep 1 @@ -79,18 +111,16 @@ wait_for_rcon_port_free() { return 1 } cleanup() { - 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 + mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true + sleep 2 + mcc-kill --session "$MCC_SESSION" >/dev/null 2>&1 || true - if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then - echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true + if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + tmux send-keys -t "$SESSION_NAME" "stop" C-m >/dev/null 2>&1 || true wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true fi + rm -f "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true wait_for_rcon_port_free 30 || true } @@ -100,7 +130,7 @@ trap cleanup EXIT prepare_config() { MCC_TEST_ACCOUNT_TYPE=mojang MCC_TEST_PASSWORD=- \ bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \ - "$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" CursorBot >/dev/null + "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null sed_in_place \ -e "s#^Server = .*#Server = { Host = \"localhost\", Port = $SERVER_PORT }#" \ @@ -115,7 +145,7 @@ prepare_config() { send_mcc_command() { local command="$1" local delay="${2:-2}" - echo "$command" >> "$INPUT_FILE" + mcc-cmd --session "$MCC_SESSION" "$command" sleep "$delay" } @@ -147,13 +177,34 @@ assert_log_contains() { wait_for_file_pattern "$file" "$pattern" "$description" "$timeout" } +start_mcc_session() { + local -a mcc_args=("$CFG" "$TEST_USERNAME" "-" "localhost:$SERVER_PORT") + local mcc_args_cmd + mcc_args_cmd="$(printf '%q ' "${mcc_args[@]}")" + + tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true + rm -f "$PID_FILE" + tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ + "cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $mcc_args_cmd > '$MCC_LOG' 2>&1" + + for _ in $(seq 1 25); do + if [[ -s "$PID_FILE" ]]; then + return 0 + fi + sleep 0.2 + done + + echo "Failed to capture MCC PID for session $MCC_SESSION" >&2 + return 1 +} + legacy_server_setup() { run_server_command "gamerule sendCommandFeedback true" run_server_command "time set day" run_server_command "weather clear" - run_server_command "gamemode creative CursorBot" + run_server_command "gamemode creative $TEST_USERNAME" run_server_command "fill -2 79 -2 2 79 2 stone" - run_server_command "tp CursorBot 0 80 0" + run_server_command "tp $TEST_USERNAME 0 80 0" } modern_server_setup() { @@ -161,30 +212,32 @@ modern_server_setup() { run_server_command "gamerule logAdminCommands true" run_server_command "time set day" run_server_command "weather clear" - run_server_command "gamemode creative CursorBot" + run_server_command "gamemode creative $TEST_USERNAME" run_server_command "fill -2 79 -2 2 79 2 stone" - run_server_command "tp CursorBot 0 80 0" + run_server_command "tp $TEST_USERNAME 0 80 0" } legacy_mob_and_effects() { run_server_command "summon Cow 2 80 0" run_server_command "summon Zombie 4 80 0" run_server_command "summon Pig -2 80 0" - run_server_command "effect CursorBot 1 30 1 true" - run_server_command "effect CursorBot 10 10 1 true" + run_server_command "effect $TEST_USERNAME 1 30 1 true" + run_server_command "effect $TEST_USERNAME 10 10 1 true" } modern_mob_and_effects() { run_server_command "summon minecraft:cow 2 80 0" run_server_command "summon minecraft:zombie 4 80 0" run_server_command "summon minecraft:pig -2 80 0" - run_server_command "effect give CursorBot minecraft:speed 30 1 true" - run_server_command "effect give CursorBot minecraft:regeneration 10 1 true" + run_server_command "effect give $TEST_USERNAME minecraft:speed 30 1 true" + run_server_command "effect give $TEST_USERNAME minecraft:regeneration 10 1 true" } bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null -bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" --all >/dev/null +bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$SERVER_DIR" >/dev/null +mcc-reset-session --session "$MCC_SESSION" >/dev/null wait_for_rcon_port_free 30 || true +mkdir -p "$(dirname "$MCC_LOG")" "$(dirname "$INPUT_FILE")" rm -f "$MCC_LOG" "$INPUT_FILE" bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null @@ -199,22 +252,13 @@ prepare_config : > "$INPUT_FILE" -( - cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ - "$CFG" \ - CursorBot \ - - \ - "localhost:$SERVER_PORT" \ - > "$MCC_LOG" 2>&1 -) & -MCC_PID=$! +start_mcc_session assert_log_contains "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 -assert_log_contains "$SERVER_LOG_FILE" "CursorBot joined the game" "server join entry" 30 +assert_log_contains "$SERVER_LOG_FILE" "$TEST_USERNAME joined the game" "server join entry" 30 print_phase "CONNECT" "PASS" -run_server_command "op CursorBot" +run_server_command "op $TEST_USERNAME" sleep 1 if [[ "$PROFILE" == "legacy" ]]; then @@ -237,7 +281,7 @@ assert_log_contains "$SERVER_LOG_FILE" "$cmd_token" "client command on server" 2 print_phase "SEND" "PASS" run_server_command "say $broadcast_token" -run_server_command "tell CursorBot $whisper_token" +run_server_command "tell $TEST_USERNAME $whisper_token" assert_log_contains "$MCC_LOG" "$broadcast_token" "server broadcast in MCC" 20 assert_log_contains "$MCC_LOG" "$whisper_token" "server whisper in MCC" 20 print_phase "RECEIVE" "PASS" diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh new file mode 100755 index 00000000..38c50c84 --- /dev/null +++ b/tools/test-mcc-env.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$REPO_ROOT/tools/mcc-env.sh" + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [[ "$expected" != "$actual" ]]; then + echo "FAIL: $label" >&2 + echo " expected: $expected" >&2 + echo " actual: $actual" >&2 + exit 1 + fi +} + +assert_regex() { + local regex="$1" + local actual="$2" + local label="$3" + if [[ ! "$actual" =~ $regex ]]; then + echo "FAIL: $label" >&2 + echo " regex: $regex" >&2 + echo " actual: $actual" >&2 + exit 1 + fi +} + +tmpfs_build_base() { + if [[ -d /dev/shm && -w /dev/shm ]]; then + printf '/dev/shm' + else + printf '%s' "${TMPDIR:-/tmp}" + fi +} + +session="$(_mcc_resolve_session "demo-branch")" +assert_eq "demo-branch" "$session" "explicit session" + +short_name="$(_mcc_resolve_username "feature-ai")" +assert_eq "mcc_feature_ai" "$short_name" "short derived username" + +long_name="$(_mcc_resolve_username "very-long-worktree-name")" +assert_regex '^mcc_[a-z0-9_]{7}_[0-9a-f]{4}$' "$long_name" "long derived username shape" +assert_eq "16" "${#long_name}" "long derived username length" + +assert_eq "${TMPDIR:-/tmp}/mcc-debug/demo-branch" "$(_mcc_session_root "demo-branch")" "session root" +assert_eq "mcc-demo-branch" "$(_mcc_tmux_session_name "demo-branch")" "tmux session name" + +MCC_BUILD_MODE=tmpfs + +original_repo_root="$MCC_REPO_ROOT" +fallback_root="$REPO_ROOT/nonexistent-worktree" +MCC_REPO_ROOT="$fallback_root" + +fallback_session="$(_mcc_resolve_session)" +assert_eq "$(basename "$fallback_root")" "$fallback_session" "session fallback without git" + +fallback_build_root="$(_mcc_build_root)" +tmpfs_base="$(tmpfs_build_base)" +expected_fallback_root="$tmpfs_base/mcc-build/$(basename "$fallback_root")" +assert_eq "$expected_fallback_root" "$fallback_build_root" "tmpfs build root fallback" + +MCC_REPO_ROOT="$original_repo_root" + +build_root="$(_mcc_build_root)" +expected_prefix="$(tmpfs_build_base)/mcc-build/" +if [[ "$build_root" != "$expected_prefix"* ]]; then + echo "FAIL: tmpfs build root" >&2 + echo " expected prefix: $expected_prefix" >&2 + echo " actual: $build_root" >&2 + exit 1 +fi + +dotnet_env_build_root="$(_mcc_dotnet_env env | awk -F= '$1=="MCC_BUILD_ROOT" { print $2 }')" +assert_eq "$build_root" "$dotnet_env_build_root" "dotnet env wrapper exports MCC_BUILD_ROOT" + +mkdir -p "$build_root/probe" +mcc-build-clean +[[ ! -e "$build_root/probe" ]] + +session="wrapper-smoke" +input_file="$(_mcc_session_input_file "$session")" +rm -rf "$(_mcc_session_root "$session")" + +mcc-cmd --session "$session" debug state +input_contents="$(cat "$input_file")" +assert_eq "debug state" "$input_contents" "session input command is intact" + +mcc-reset-session --session "$session" +[[ ! -e "$(_mcc_session_root "$session")" ]] +malformed_log="${TMPDIR:-/tmp}/mcc-env-session-hang-test.log" +for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc mcc-run mcc-tui; do + set +e + "$func" --session >"$malformed_log" 2>&1 + status=$? + set -e + if [[ $status -eq 0 ]]; then + echo "FAIL: $func accepted --session without a value" >&2 + cat "$malformed_log" >&2 + exit 1 + fi + grep -Fq -- "--session requires a value" "$malformed_log" +done + +guard_root="$(mktemp -d "${TMPDIR:-/tmp}/mcc-env-guard.XXXXXX")" +MCC_SERVERS="$guard_root" +mkdir -p "$MCC_SERVERS/testver" +printf 'unchanged\n' > "$MCC_SERVERS/testver/stdin.pipe" + +confirm_log="${TMPDIR:-/tmp}/mcc-env-confirm-guard.log" +for cmd in \ + "mc-stop testver" \ + "mc-kill testver" \ + "mc-reset-test-env testver" +do + set +e + eval "$cmd" >"$confirm_log" 2>&1 + status=$? + set -e + if [[ $status -eq 0 ]]; then + echo "FAIL: $cmd ran without --confirm" >&2 + cat "$confirm_log" >&2 + exit 1 + fi + grep -Fq -- "--confirm" "$confirm_log" + grep -Fq "Keep shared servers running by default" "$confirm_log" +done + +stdin_contents="$(cat "$MCC_SERVERS/testver/stdin.pipe")" +assert_eq "unchanged" "$stdin_contents" "mc-stop without confirm does not touch stdin pipe" + +: > "$MCC_SERVERS/testver/stdin.pipe" +mc-stop testver --confirm +assert_eq "stop" "$(cat "$MCC_SERVERS/testver/stdin.pipe")" "mc-stop with confirm writes to stdin pipe" + +echo "PASS"