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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-31 06:22:53 +00:00
parent 7ae5597530
commit 7e717483e1
3 changed files with 42 additions and 17 deletions

View File

@@ -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<usize> = std::env::args().skip(1).filter_map(|a| a.parse().ok()).collect();
let builds: Vec<usize> = 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} ===");

View File

@@ -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

View File

@@ -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):