mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge pull request #3077 from BruceChenQAQ/bruce/shared-server-mcc-sessions
tools: isolate MCC sessions while sharing local servers
This commit is contained in:
commit
248a7a2f6f
24 changed files with 2547 additions and 276 deletions
|
|
@ -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:-<repo>/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/<session>/`.
|
||||
- `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/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` 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/<session>/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-<session>`
|
||||
- **Logs**: `mcc-log-mcc --session smoke-a` or `tail -f "${TMPDIR:-/tmp}/mcc-debug/<session>/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-<session> -p -S -50` captures the last 50 lines of a tmux session without attaching.
|
||||
|
||||
## Tool files
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ usage() {
|
|||
cat <<'EOF'
|
||||
Usage: reset_shared_test_state.sh [--all | <server-dir>...]
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 <<EOF
|
||||
|
|
|
|||
199
.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh
Executable file
199
.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
MC_VERSION="${VERSION%-Vanilla}"
|
||||
if [[ "$MC_VERSION" == "$VERSION" ]]; then
|
||||
MC_VERSION="$VERSION"
|
||||
fi
|
||||
|
||||
SESSION_A="parallel-smoke-a-${MC_VERSION//[^a-zA-Z0-9]/_}"
|
||||
SESSION_B="parallel-smoke-b-${MC_VERSION//[^a-zA-Z0-9]/_}"
|
||||
USERNAME_A="SmokeA"
|
||||
USERNAME_B="SmokeB"
|
||||
|
||||
RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing"
|
||||
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
||||
RUN_DIR="$RUN_ROOT/parallel-smoke-$RUN_ID"
|
||||
BUILD_LOG="$RUN_DIR/build.log"
|
||||
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
|
||||
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
|
||||
SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log"
|
||||
|
||||
LOG_A="$(_mcc_session_log_file "$SESSION_A")"
|
||||
LOG_B="$(_mcc_session_log_file "$SESSION_B")"
|
||||
INPUT_A="$(_mcc_session_input_file "$SESSION_A")"
|
||||
INPUT_B="$(_mcc_session_input_file "$SESSION_B")"
|
||||
PID_A_FILE="$(_mcc_session_pid_file "$SESSION_A")"
|
||||
PID_B_FILE="$(_mcc_session_pid_file "$SESSION_B")"
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
wait_for_file_pattern() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
local timeout="${4:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for: $description" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_server_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 <<EOF
|
||||
PASS
|
||||
Run directory: $RUN_DIR
|
||||
Server version: $VERSION
|
||||
Server port: $SERVER_PORT
|
||||
Session A: $SESSION_A ($USERNAME_A)
|
||||
Session A input: $INPUT_A
|
||||
Session A log: $LOG_A
|
||||
Session B: $SESSION_B ($USERNAME_B)
|
||||
Session B input: $INPUT_B
|
||||
Session B log: $LOG_B
|
||||
Server log: $SERVER_FILE_LOG
|
||||
Build log: $BUILD_LOG
|
||||
EOF
|
||||
|
|
@ -22,4 +22,4 @@ echo "MCC highlights:"
|
|||
grep -E "Server was successfully joined|FileInput|smoke_test_from_mcc_full_spectrum|There are [0-9]+ of a max|health|Creative" "$MCC_LOG" || true
|
||||
echo
|
||||
echo "Server highlights:"
|
||||
grep -E "joined the game|Made CursorBot a server operator|game mode|smoke_test_from_mcc_full_spectrum|summon|particle|playsound|tnt" "$SERVER_LOG" || true
|
||||
grep -E "joined the game|Made .* a server operator|game mode|smoke_test_from_mcc_full_spectrum|summon|particle|playsound|tnt" "$SERVER_LOG" || true
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ try
|
|||
|
||||
ToolEnvelope initialWorldState = await CallSuccessAsync(client, executed, "mcc_world_state");
|
||||
JsonElement initialWorldData = RequireData(initialWorldState);
|
||||
string botName = ReadString(initialWorldData, "username") ?? "CursorBot";
|
||||
string botName = ReadString(initialWorldData, "username") ?? "MCCBot";
|
||||
Coordinate initialLocation = ReadCoordinate(initialWorldData, "location");
|
||||
|
||||
long setupBaseline = 0;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
11
Directory.Build.props
Normal file
11
Directory.Build.props
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);**/bin/**;**/obj/**</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MCC_BUILD_ROOT)' != ''">
|
||||
<BaseOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/</BaseIntermediateOutputPath>
|
||||
<MSBuildProjectExtensionsPath>$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -38,20 +38,20 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Brigadier.NET" Version="1.2.13" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.143.0" />
|
||||
<PackageReference Include="Consolonia" Version="11.3.10" />
|
||||
<PackageReference Include="Consolonia" Version="11.3.12.3" />
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.5.1" />
|
||||
<PackageReference Include="DynamicExpresso.Core" Version="2.19.3" />
|
||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
||||
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.1.0" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.5" />
|
||||
<PackageReference Include="Samboy063.Tomlet" Version="6.2.0" />
|
||||
<PackageReference Include="Sentry" Version="6.3.0" />
|
||||
<PackageReference Include="Sentry" Version="6.3.1" />
|
||||
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
|
||||
<PackageReference Include="starksoft.aspen" Version="1.1.8">
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
</details>
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
@ -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/<session>/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/<session>/`.
|
||||
- `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/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` 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:
|
||||
|
|
|
|||
|
|
@ -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] <command>" >&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" <<EOF
|
||||
SESSION=$SESSION
|
||||
USERNAME=$USERNAME
|
||||
MODE=$MODE
|
||||
CFG=$CFG
|
||||
LOG=$MCC_LOG
|
||||
INPUT=$INPUT_FILE
|
||||
PID_FILE=$PID_FILE
|
||||
TMUX_SESSION=$MCC_TMUX_SESSION
|
||||
SERVER_VERSION=$VERSION
|
||||
EOF
|
||||
|
||||
MCC_ARGS=("$CFG" "$USERNAME" "-" "localhost:$PORT")
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 把 `mcc-kill` 改成精准杀当前 session,不再 `pkill -f MinecraftClient`**
|
||||
|
||||
```bash
|
||||
mcc-kill() {
|
||||
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")"
|
||||
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
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);**/bin/**;**/obj/**</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MCC_BUILD_ROOT)' != ''">
|
||||
<BaseOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/</BaseIntermediateOutputPath>
|
||||
<MSBuildProjectExtensionsPath>$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
- [ ] **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/<worktree>/` 或 `/tmp/mcc-build/<worktree>/`
|
||||
|
||||
- [ ] **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/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` 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` 外置重复编译问题
|
||||
|
|
@ -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 `<repo>/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 `<resolved repo root>/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/<session>/`
|
||||
|
||||
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-<session>`
|
||||
|
||||
Server tmux sessions remain version-scoped:
|
||||
|
||||
- `mc-<version>`
|
||||
|
||||
### 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/<worktree>/` on Linux
|
||||
- fallback to `${TMPDIR:-/tmp}/mcc-build/<worktree>/` 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.
|
||||
|
|
@ -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/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` when `/dev/shm` is unavailable.
|
||||
|
||||
## Data Sources
|
||||
|
||||
Two types of data can be used as input:
|
||||
|
|
|
|||
|
|
@ -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" <<EOF
|
||||
session=$SESSION
|
||||
user=$USERNAME
|
||||
mode=$RUNTIME_MODE
|
||||
config=$CFG
|
||||
log=$MCC_LOG
|
||||
input=$INPUT_FILE
|
||||
pid=$PID_FILE
|
||||
tmux=$MCC_TMUX_SESSION
|
||||
server_version=$VERSION
|
||||
server_port=$PORT
|
||||
EOF
|
||||
|
||||
# --- Launch MCC ---
|
||||
echo "[4/4] Launching MCC in $MODE mode..."
|
||||
: > "$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"
|
||||
|
|
|
|||
523
tools/mcc-env.sh
523
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 <<EOF
|
||||
Refusing to ${action} without --confirm.
|
||||
Keep shared servers running by default. Only stop or reset them when the user explicitly asks, or when you need to switch server versions.
|
||||
If you really intend to do this, rerun: ${rerun_command} --confirm
|
||||
EOF
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Minecraft Server Management ---
|
||||
mc-start() { bash "$MCC_REPO/tools/start-server.sh" "${1:-1.20.6}"; }
|
||||
mc-stop() { local v="${1:-1.20.6}"; echo "stop" > "$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] <command>" >&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")"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
140
tools/test-mcc-env.sh
Executable file
140
tools/test-mcc-env.sh
Executable file
|
|
@ -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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue