From 698409fdfecd31c259861b733775f91d5b7e5230 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Mon, 31 Aug 2026 06:22:53 +0000 Subject: [PATCH] tools: walk to EXTRAS by cursor movement, and size-match against several builds menu_blend_capture.sh counted two DOWNs to reach EXTRAS, which is wrong twice over -- EXTRAS is the fifth item, and on 2026-08-31 four DOWNs landed on OPTIONS because one press was dropped. It now presses until the cursor stops moving, which needs no item count and no row calibration. Its title deadline follows the same change as title_blend_capture.sh, 1200 s not 420. ui_blend_map.py takes a comma-separated build list, because the live title is TWO builds composited -- 4 draws the art, 2 draws the PRESS (A) plate -- and a one-build size table cannot name the elements of a title capture. frame_alpha_census takes its builds from argv for the same reason. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- .../examples/frame_alpha_census.rs | 6 +++- tools/re-capture/menu_blend_capture.sh | 28 +++++++++++++++---- tools/re-capture/ui_blend_map.py | 25 +++++++++-------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/crates/sylpheed-formats/examples/frame_alpha_census.rs b/crates/sylpheed-formats/examples/frame_alpha_census.rs index 5a2bda9d..3a80f974 100644 --- a/crates/sylpheed-formats/examples/frame_alpha_census.rs +++ b/crates/sylpheed-formats/examples/frame_alpha_census.rs @@ -39,7 +39,11 @@ fn census(name: &str, img: &t8ad::T8adImage) { fn main() { let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE"); - for build in [5usize, 6] { + // Builds default to the two the port ships and can be overridden, so the + // same census serves the title (4) and the `PRESS (A)` plate (2). + let args: Vec = std::env::args().skip(1).filter_map(|a| a.parse().ok()).collect(); + let builds: Vec = if args.is_empty() { vec![5, 6] } else { args }; + for build in builds { let by = ar.read(&ar.entries()[build]).expect("entry"); let b = ui_layout::parse_build(&by).expect("build"); println!("=== GP_TITLE build {build} ==="); diff --git a/tools/re-capture/menu_blend_capture.sh b/tools/re-capture/menu_blend_capture.sh index e950b5b9..9caaf984 100755 --- a/tools/re-capture/menu_blend_capture.sh +++ b/tools/re-capture/menu_blend_capture.sh @@ -51,7 +51,7 @@ echo "WINDOW=$win" # gets there on its own; a run that tapped every 4 s delivered 88 presses and # ended on a black screen. s="" -deadline=$(( SECONDS + 420 )) +deadline=$(( SECONDS + ${DEADLINE:-1200} )) while [ $SECONDS -lt $deadline ]; do s="$(screen)"; echo "t=${SECONDS}s $s" [ "$s" = "title" ] && break @@ -70,10 +70,28 @@ shot "$OUT/menu.png" echo "MENU at ${SECONDS}s" if [ "${SCREEN:-menu}" = "extras" ]; then - # EXTRAS is the second item; the menu opens on NEW GAME. - python3 "$SD/pad.py" tap DOWN 0.2; sleep 1 - python3 "$SD/pad.py" tap DOWN 0.2; sleep 1 - python3 "$SD/pad.py" tap A 0.3; sleep 4 + # EXTRAS is the LAST of the five items and the menu opens on NEW GAME. Do not + # count presses: on 2026-08-31 four DOWNs landed on OPTIONS because one was + # dropped. Press until the cursor STOPS MOVING instead, which needs no item + # count and no row calibration -- `ring_row.py`'s ROW0/SPACING are x11grab + # constants and are 45 px out on a `screenshot` grab (see METHOD.md), but the + # raw row it returns is still a monotone function of the item. + row(){ shot /tmp/mbc_row.png; python3 - <<'PY' +from PIL import Image +import sys; sys.path.insert(0,"/work/tools/re-capture") +import ring_row +try: print("%.1f" % ring_row.ring_row(Image.open("/tmp/mbc_row.png"))) +except Exception: print("nan") +PY + } + prev="$(row)"; echo " cursor row $prev" + for _ in 1 2 3 4 5 6 7 8; do + python3 "$SD/pad.py" tap DOWN 0.2; sleep 2 + cur="$(row)"; echo " cursor row $cur" + [ "$cur" = "$prev" ] && break + prev="$cur" + done + python3 "$SD/pad.py" tap A 0.3; sleep 5 s="$(screen)"; echo " after EXTRAS attempt: $s"; shot "$OUT/extras.png" fi diff --git a/tools/re-capture/ui_blend_map.py b/tools/re-capture/ui_blend_map.py index 3285698c..89f0a233 100755 --- a/tools/re-capture/ui_blend_map.py +++ b/tools/re-capture/ui_blend_map.py @@ -45,29 +45,32 @@ def blend_name(raw): return "%s+%s" % (FACTOR.get(src, src), FACTOR.get(dst, dst)) -def sprite_sizes(build): - """name -> (w, h), straight off the disc via the formats crate.""" +def sprite_sizes(builds): + """name -> (w, h), straight off the disc via the formats crate. + + `builds` is a list because the live title is TWO builds composited -- 4 draws + the art and 2 draws the `PRESS (A)` plate over it -- so a capture of the + title contains elements from both and a one-build size table cannot name + them all. + """ out = subprocess.run( ["cargo", "run", "--release", "-q", "-p", "sylpheed-formats", - "--example", "frame_alpha_census"], + "--example", "frame_alpha_census", "--"] + [str(b) for b in builds], capture_output=True, text=True, cwd="/work", env={**__import__("os").environ, "SYLPHEED_DISC": "/disc"}).stdout - sizes, cur = {}, None + sizes = {} for line in out.splitlines(): - m = re.match(r"=== GP_TITLE build (\d+) ===", line) - if m: - cur = int(m.group(1)) - continue m = re.match(r"(\S+\.t32)\s+(\d+)x(\d+)", line) - if m and cur == build: + if m: sizes[m.group(1)] = (int(m.group(2)), int(m.group(3))) return sizes def main(): path = sys.argv[1] - build = int(sys.argv[sys.argv.index("--build") + 1]) if "--build" in sys.argv else 5 - sizes = sprite_sizes(build) + spec = sys.argv[sys.argv.index("--build") + 1] if "--build" in sys.argv else "5" + builds = [int(b) for b in spec.split(",")] + sizes = sprite_sizes(builds) lines = open(path).read().splitlines() rows = [] for i, line in enumerate(lines):