#!/usr/bin/env bash # Which button is focused in a screenshot of the real game? # # tools/port/which-focus SHOT.png # main_menu (5 buttons) # tools/port/which-focus SHOT.png extras # extras (3 buttons) # # Renders the port's own screen with each button focused in turn and reports # which one differs least from the shot. Answers a question the Decoder needs to # drive the game -- `newgame_path.sh` assumed NEW GAME is focused at boot, drove # on that, and landed in a tutorial mission, because HANDOFF Q5 measured focus as # UNSTABLE across boots. Counting presses cannot substitute: up from the first # item wraps to the last, so no fixed number of presses lands on a known item # from an unknown start. # # ⚠️ IT RUNS ITS OWN CONTROL FIRST AND REFUSES TO ANSWER IF THE CONTROL FAILS. # `docs/re/captures/title-builds/live-main-menu-options-focused.png` has the # answer in its filename, so the method can be tested on every invocation rather # than once when it was written. A brightness-per-row detector was tried for this # job and picked NEW GAME on that capture; this method picks OPTIONS by 4.7x. # A control that does not execute is not a control. # # 🔴 IT NEEDS GODOT AND THE PORT'S EXPORT TREE, so it does NOT run in the RE # container -- no engine there, and rendering this project is outside that # agent's role. It reads a capture, but it answers by RENDERING the candidates. # `tools/re-capture/focus_from_capture.py` is the capture-only alternative; note # that its offline controls are its own calibration inputs, which is # self-consistency rather than validation, so it is the live transition test # (NEW GAME -> down -> LOAD GAME, expected LOAD GAME) that validates it. # # WHAT IT IS NOT. It identifies the focus in ONE FRAME. It says nothing about # what selects focus -- Q5's four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW # GAME and that instability stands. set -euo pipefail cd "${PROJECT_DIR:-/work}" export DISPLAY="${DISPLAY:-:97}" shot="${1:?usage: which-focus SHOT.png [screen]}" screen="${2:-main_menu}" OUT="${OUT:-$(mktemp -d)}"; mkdir -p "$OUT" CAPS=docs/re/captures/title-builds # Buttons, in the order ui_down walks them. case "$screen" in main_menu) BUTTONS=(ptbtn01:NEW_GAME ptbtn02:LOAD_GAME ptbtn03:TUTORIAL ptbtn04:OPTIONS ptbtn05:EXTRAS) ;; extras) BUTTONS=(ptbtn11:MISSION_SELECT ptbtn12:MOVIE_THEATER ptbtn13:THIRD) ;; *) echo "which-focus: no button list for $screen" >&2; exit 2 ;; esac n=${#BUTTONS[@]} downs=$(python3 -c "print(','.join(['down']*($n-1)))") render_all() { # render_all godot --path port --resolution 1280x720 -- "--menu=$screen" "--script=$downs" \ "--shots=$OUT/$1" >"$OUT/$1.log" 2>&1 || true } # Normalise any input to the captures' 1279x675 top-left crop. A 1280x720 guest # frame and a 1279x675 screenshot are the same pixels; the difference is the # crop the screenshot tool applies, not a scale. norm() { convert "$1" -crop 1279x675+0+0 +repage "$2"; } score() { # score ; prints "