magicciv/ss
2026-04-29 13:06:45 -07:00

192 lines
6.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# ss — One-shot proof-scene screenshot capture.
#
# Usage:
# ./ss <name> # run scene locally (flatpak Godot on this host)
# ./ss --remote <host> <name> # rsync → ssh → weston+Godot → scp PNG back
# ./ss --remote <host> <name> [scene_path_override]
#
# `<name>` resolves a scene under `src/game/engine/scenes/tests/`:
# 1. `proof_<name>.tscn` (preferred — matches the project's proof-scene convention)
# 2. `<name>.tscn` (fallback)
#
# The chosen scene is launched as the main scene; it must self-capture
# its viewport into `user://screenshots/<name>.png`. (See proof scenes
# under `src/game/engine/scenes/tests/` for the pattern — they read
# `OS.get_environment("SCREENSHOT_NAME")` and call `Image.save_png`.)
#
# Output (local, after success): `screenshots/<name>.png` at repo root.
# `--remote` mode pulls the PNG back over scp.
#
# Why this wrapper exists:
# `tools/screenshot.sh` only knows two scenes (main_menu, game_setup) via
# the `capture_screenshot.gd` autoload. Proof scenes self-capture, so we
# bypass the autoload and launch the scene directly. `./ss --remote` is
# the path for mac-edit / apricot-run developers — the host check below
# prevents accidentally trying to flatpak-launch on macOS.
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GODOT_PROJECT_NAME="Magic Civilization"
SCENES_REL_DIR="src/game/engine/scenes/tests"
REMOTE=""
NAME=""
SCENE_OVERRIDE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--remote)
REMOTE="${2:-}"
if [[ -z "$REMOTE" ]]; then
echo "ss: --remote needs a host argument" >&2
exit 2
fi
shift 2
;;
-h|--help)
awk '/^# / { sub(/^# ?/, ""); print; next } NR>1 { exit }' "${BASH_SOURCE[0]}"
exit 0
;;
*)
if [[ -z "$NAME" ]]; then
NAME="$1"
elif [[ -z "$SCENE_OVERRIDE" ]]; then
SCENE_OVERRIDE="$1"
else
echo "ss: unexpected positional argument: $1" >&2
exit 2
fi
shift
;;
esac
done
if [[ -z "$NAME" ]]; then
echo "Usage: ./ss [--remote <host>] <name> [scene_path_override]" >&2
exit 2
fi
# Resolve scene path (relative to repo root).
if [[ -n "$SCENE_OVERRIDE" ]]; then
SCENE_REL="$SCENE_OVERRIDE"
elif [[ -f "$REPO_ROOT/$SCENES_REL_DIR/proof_$NAME.tscn" ]]; then
SCENE_REL="$SCENES_REL_DIR/proof_$NAME.tscn"
elif [[ -f "$REPO_ROOT/$SCENES_REL_DIR/$NAME.tscn" ]]; then
SCENE_REL="$SCENES_REL_DIR/$NAME.tscn"
else
echo "ss: scene not found — tried:" >&2
echo " $SCENES_REL_DIR/proof_$NAME.tscn" >&2
echo " $SCENES_REL_DIR/$NAME.tscn" >&2
echo "Pass a path explicitly: ./ss [--remote <host>] $NAME <scene_path>" >&2
exit 1
fi
if [[ ! -f "$REPO_ROOT/$SCENE_REL" ]]; then
echo "ss: scene file does not exist: $SCENE_REL" >&2
exit 1
fi
# `res://` paths are relative to `src/game/`, not the repo root.
SCENE_RES="res://${SCENE_REL#src/game/}"
LOCAL_OUT_DIR="$REPO_ROOT/screenshots"
LOCAL_OUT="$LOCAL_OUT_DIR/$NAME.png"
mkdir -p "$LOCAL_OUT_DIR"
run_remote() {
local host="$1"
local remote_repo
remote_repo="$(ssh -o ConnectTimeout=10 "$host" 'echo $HOME/Code/@projects/@magic-civilization')" || {
echo "ss: cannot reach $host" >&2
return 1
}
echo "ss: rsync → $host:$remote_repo"
rsync -az \
--exclude='target/' --exclude='pkg/' --exclude='.local/' \
--exclude='addons/*/*.so' --exclude='addons/*/*.dylib' --exclude='addons/*/*.dll' \
--exclude='node_modules/' --exclude='.git/' --exclude='screenshots/' \
"$REPO_ROOT/" "$host:$remote_repo/" >/dev/null
# Run scene under weston (virtual wayland → software-rendering Godot →
# viewport.get_texture().get_image() works under llvmpipe).
# The proof scene self-captures to user://screenshots/<NAME>.png and quits.
local remote_script
remote_script=$(cat <<REMOTE_EOF
set -uo pipefail
cd "$remote_repo/src/game"
SOCKET="ss-\$\$"
weston --backend=headless --socket="\$SOCKET" --width=1280 --height=720 \
>/tmp/ss-weston.log 2>&1 &
WESTON_PID=\$!
trap 'kill \$WESTON_PID 2>/dev/null || true' EXIT
sleep 1
timeout 90 flatpak run --user \
--filesystem=home \
--socket=wayland \
--env=WAYLAND_DISPLAY="\$SOCKET" \
--env=SCREENSHOT_NAME="$NAME" \
--filesystem="xdg-run/\$SOCKET" \
org.godotengine.Godot \
--path . \
--rendering-method gl_compatibility \
--quit-after 200 \
"$SCENE_RES" 2>&1 | tail -40
echo "ss: scene exit code \$?"
REMOTE_EOF
)
ssh "$host" bash -s <<< "$remote_script"
# The proof scene self-captures to user://screenshots/<NAME>.png AND the
# capture_screenshot.gd autoload also fires (it triggers on SCREENSHOT_NAME),
# writing user://screenshots/<NAME>_<timestamp>.png. Both end up under
# the flatpak userdata dir; we resolve the newest match by globbing both
# patterns on the remote and copying the most recent.
# Quote the project name on the remote side so the embedded space in
# "Magic Civilization" survives `ssh "$host" "<command>"`.
local remote_glob_cmd
printf -v remote_glob_cmd 'ls -t "$HOME/.var/app/org.godotengine.Godot/data/godot/app_userdata/%s/screenshots/"%s*.png 2>/dev/null | head -1' \
"$GODOT_PROJECT_NAME" "$NAME"
local remote_png
remote_png="$(ssh "$host" "$remote_glob_cmd")"
if [[ -z "$remote_png" ]]; then
echo "ss: no PNG matching ${NAME}*.png on $host. Check /tmp/ss-weston.log on $host and the Godot output above." >&2
return 1
fi
if scp "$host:$remote_png" "$LOCAL_OUT" 2>/dev/null; then
echo "ss: captured → $LOCAL_OUT (from $remote_png)"
return 0
fi
echo "ss: scp failed for $remote_png" >&2
return 1
}
run_local() {
if ! command -v flatpak >/dev/null 2>&1; then
echo "ss: local mode requires flatpak Godot. On macOS, use: ./ss --remote apricot $NAME" >&2
return 2
fi
cd "$REPO_ROOT/src/game"
timeout 90 flatpak run --user \
--env=SCREENSHOT_NAME="$NAME" \
org.godotengine.Godot \
--path . \
--rendering-method gl_compatibility \
--quit-after 200 \
"$SCENE_RES" 2>&1 | tail -40
local userdata_png="$HOME/.var/app/org.godotengine.Godot/data/godot/app_userdata/$GODOT_PROJECT_NAME/screenshots/$NAME.png"
if [[ -f "$userdata_png" ]]; then
cp "$userdata_png" "$LOCAL_OUT"
echo "ss: captured → $LOCAL_OUT"
return 0
fi
echo "ss: screenshot not written at $userdata_png" >&2
return 1
}
if [[ -n "$REMOTE" ]]; then
run_remote "$REMOTE"
else
run_local
fi