magicciv/scripts/run/dev.sh

279 lines
9.7 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Dev commands: play, editor, lint, format, test, verify, screenshot
cmd_play() {
local LOG_FILE="$REPO_ROOT/.project/logs/game_$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$(dirname "$LOG_FILE")"
echo -e "${BLUE}Launching Magic Civilization...${NC}"
echo -e "${BLUE}Log: $LOG_FILE${NC}"
WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
$GODOT_BIN --path "$GAME_DIR" --rendering-method gl_compatibility "$@" 2>&1 | tee "$LOG_FILE"
local EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo -e "\n${RED}Game exited with code $EXIT_CODE${NC}"
echo -e "${RED}Crash log: $LOG_FILE${NC}"
tail -20 "$LOG_FILE" | grep -E "SCRIPT ERROR|ERROR:|Crash|FATAL|at:" | head -10
fi
}
cmd_editor() {
echo -e "${BLUE}Opening Godot editor...${NC}"
WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
$GODOT_BIN --path "$GAME_DIR" -e --rendering-method gl_compatibility "$@" &
}
cmd_validate() {
echo -e "${BLUE}Validating game data JSON schemas...${NC}"
python3 "$REPO_ROOT/tools/validate-game-data.py" "$@"
}
cmd_lint() {
local exit_code=0
echo -e "${BLUE}[1/3] GDScript lint...${NC}"
lilith-gdtoolkit-sync --check || {
echo -e "${YELLOW}Config drift detected — syncing...${NC}"
lilith-gdtoolkit-sync
}
gdlint "$GAME_DIR/engine/src/" || exit_code=$?
echo ""
echo -e "${BLUE}[2/3] Rust lint (fmt + clippy)...${NC}"
(cd "$SIMULATOR_DIR" && cargo fmt --check --all) || exit_code=$?
(cd "$SIMULATOR_DIR" && cargo clippy --workspace --all-targets -- -D warnings) || exit_code=$?
echo ""
echo -e "${BLUE}[3/3] Guide lint (ESLint)...${NC}"
pnpm --prefix "$GUIDE_DIR" lint || exit_code=$?
return $exit_code
}
cmd_format() {
echo -e "${BLUE}[1/3] GDScript format...${NC}"
lilith-gdtoolkit-sync --check || {
echo -e "${YELLOW}Config drift detected — syncing...${NC}"
lilith-gdtoolkit-sync
}
gdformat "$GAME_DIR/engine/src/"
echo ""
echo -e "${BLUE}[2/3] Rust format...${NC}"
(cd "$SIMULATOR_DIR" && cargo fmt --all)
echo ""
echo -e "${BLUE}[3/3] Guide format (ESLint --fix)...${NC}"
pnpm --prefix "$GUIDE_DIR" lint:fix
}
cmd_test() {
local exit_code=0
echo -e "${BLUE}Running GUT tests (GDScript)...${NC}"
WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
$GODOT_BIN --path "$GAME_DIR" --headless --script res://addons/gut/gut_cmdln.gd \
-gexit "$@" || exit_code=$?
echo ""
echo -e "${BLUE}Running Rust tests (simulator)...${NC}"
(cd "$SIMULATOR_DIR" && cargo test --workspace) || exit_code=$?
echo ""
echo -e "${BLUE}Running vitest (guide)...${NC}"
pnpm --prefix "$GUIDE_DIR" test || exit_code=$?
echo ""
echo -e "${BLUE}Running stability test (20s game boot)...${NC}"
_run_stability_test || exit_code=$?
return $exit_code
}
_run_stability_test() {
# Boots the game → world_map, waits 20s, captures screenshot.
# If the game crashes before capture, exit code is non-zero.
local LOG="/tmp/stability_test_$$.log"
cmd_screenshot "stability_test" "world_map" "20" > "$LOG" 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}FAIL: Game crashed during stability test${NC}"
grep -E "SCRIPT ERROR|ERROR:" "$LOG" | head -5
return 1
fi
if grep -q "Captured:" "$LOG"; then
echo -e "${GREEN}PASS: Game stable for 20s, screenshot captured${NC}"
return 0
else
echo -e "${RED}FAIL: Game ran but no screenshot captured${NC}"
cat "$LOG" | tail -5
return 1
fi
}
cmd_verify() {
local -a step_names step_times step_results
local overall_exit=0
_verify_step() {
local step_num="$1"
local total="$2"
local label="$3"
shift 3
echo ""
echo -e "${BLUE}[${step_num}/${total}] ${label}${NC}"
local t_start
t_start=$(date +%s%N)
if ! "$@"; then
local t_end elapsed
t_end=$(date +%s%N)
elapsed=$(( (t_end - t_start) / 1000000 ))
step_names+=("$label")
step_times+=("${elapsed}ms")
step_results+=("FAIL")
echo ""
echo -e "${RED}ABORT: '${label}' failed after ${elapsed}ms${NC}"
_verify_summary
exit 1
fi
local t_end elapsed
t_end=$(date +%s%N)
elapsed=$(( (t_end - t_start) / 1000000 ))
step_names+=("$label")
step_times+=("${elapsed}ms")
step_results+=("PASS")
}
_verify_run_in_dir() {
local dir="$1"; shift
(cd "$dir" && "$@")
}
_verify_summary() {
echo ""
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
echo -e "${BLUE} Regression Gate Summary${NC}"
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
local i
for i in "${!step_names[@]}"; do
local result="${step_results[$i]}"
local color
if [ "$result" = "PASS" ]; then
color="$GREEN"
else
color="$RED"
fi
printf " %-40s %s%-4s%s %s\n" \
"${step_names[$i]}" \
"$color" "$result" "$NC" \
"${step_times[$i]}"
done
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
# Count pending steps not yet run
local n_pass=0 n_fail=0
for r in "${step_results[@]}"; do
if [ "$r" = "PASS" ]; then
n_pass=$(( n_pass + 1 ))
else
n_fail=$(( n_fail + 1 ))
fi
done
if [ "$n_fail" -eq 0 ]; then
echo -e " ${GREEN}All ${n_pass} checks passed${NC}"
else
echo -e " ${RED}${n_fail} check(s) failed, ${n_pass} passed${NC}"
fi
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
}
# Step 0 — Game data schema validation
_verify_step 0 8 "game data JSON schemas" \
python3 "$REPO_ROOT/tools/validate-game-data.py"
# Step 1 — Rust build
_verify_step 1 8 "cargo build --workspace" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo build --workspace
# Step 2 — Rust tests
_verify_step 2 8 "cargo test --workspace" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo test --workspace
# Step 3 — Rust clippy
_verify_step 3 8 "cargo clippy --workspace -D warnings" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo clippy --workspace -- -D warnings
# Apply project-local gdlint config before linting.
# The lilith-gdtoolkit-sync tool keeps overwriting gdlintrc with defaults
# (max-public-methods: 20, no-else-return enabled, unused-argument enabled).
# Our project needs carveouts for GDExtension wrappers (99+ methods on
# DataLoader, city.gd bridge methods, etc.) and signal handler signatures.
# .project/gdlintrc.local is the source of truth — copy it over before lint.
cp "$REPO_ROOT/.project/gdlintrc.local" "$REPO_ROOT/gdlintrc" 2>/dev/null
# Step 4 — GDScript lint: engine/src/
_verify_step 4 8 "gdlint engine/src/" \
gdlint "$GAME_DIR/engine/src/"
# Step 5 — GDScript lint: scenes/tests/
_verify_step 5 8 "gdlint engine/scenes/tests/" \
gdlint "$GAME_DIR/engine/scenes/tests/"
# Step 6 — GDScript lint: tests/integration/
_verify_step 6 8 "gdlint engine/tests/integration/" \
gdlint "$GAME_DIR/engine/tests/integration/"
# Step 7 — Godot headless boot: GDExtension + script compilation
_verify_step 7 8 "godot headless boot (no script errors)" \
_godot_headless_boot
_verify_summary
return $overall_exit
}
_godot_headless_boot() {
## Boot Godot headless and check for SCRIPT ERRORs.
## Catches class_name resolution failures, GDExtension load failures,
## and any other compile-time GDScript errors that gdlint cannot detect.
local log="/tmp/godot_headless_boot_$$.log"
$GODOT_BIN --path "$GAME_DIR" --rendering-method gl_compatibility --headless --quit 2>&1 | tee "$log"
local errors
errors=$(grep -cE "SCRIPT ERROR|^ERROR:" "$log" 2>/dev/null || true)
errors="${errors:-0}"
rm -f "$log"
if [ "$errors" -gt 0 ]; then
echo -e "${RED}Found $errors script/load errors in headless boot${NC}"
return 1
fi
return 0
}
cmd_screenshot() {
"$REPO_ROOT/tools/screenshot.sh" "$@"
}
cmd_guide() {
echo -e "${BLUE}Starting guide dev server (port 5800)...${NC}"
pnpm --prefix "$GUIDE_DIR" dev
}
cmd_autoplay() {
# Single-seed fast feedback: ./run autoplay [seed]
local seed="${1:-1}"
local results_dir="/tmp/autoplay_single_${seed}"
bash "$(dirname "${BASH_SOURCE[0]}")/../../tools/autoplay-batch.sh" 1 500 "$results_dir" || return $?
python3 "$(dirname "${BASH_SOURCE[0]}")/../../tools/autoplay-report.py" "$results_dir"
}
cmd_autoplay_batch() {
# Multi-seed regression gate: ./run autoplay-batch [count]
local count="${1:-3}"
local results_dir="/tmp/autoplay_batch_$(date +%s)"
bash "$(dirname "${BASH_SOURCE[0]}")/../../tools/autoplay-batch.sh" "$count" 500 "$results_dir" || return $?
python3 "$(dirname "${BASH_SOURCE[0]}")/../../tools/autoplay-report.py" "$results_dir"
}