#!/usr/bin/env python3 """Do `ptloop01/02` animate on the SETTLED main menu? `sylpheed-port` found those two leaves free-running in their renderer on the menu path and pinned them, noting that pinning picks one pose rather than the game's -- "a capture question, not a harness one". On the title it is answered: two captures from different sessions are byte-identical over the loop rect (0 of 18 000). The MENU is a different bundle and is where their row actually drifted. ⚠️ A first attempt used `screen_id.py`'s single `title` classification as the cue to press Ⓐ, hit a title at t=146 s, and Ⓐ never took across six tries -- that was the ATTRACT loop's title, which accepts nothing, and the classifier cannot tell it from the boot title. This gates on the same signal `jp_title_capture.py` uses: the green Ⓐ-plate glyph count inside a band, HELD for 12 consecutive samples (~3 s). menu_loop_probe.py OUTDIR [wait_s] """ import subprocess import sys import time import numpy as np from PIL import Image OUT = sys.argv[1] WAIT = float(sys.argv[2]) if len(sys.argv) > 2 else 700 W, H = 1280, 720 SURF_Y = 45 NEED, CEIL, HOLD = 500, 2500, 12 LOOP = (441, 270, 200, 90) # ptloop01/02 rest rect, design space def _open(): return subprocess.Popen( ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", "-video_size", f"{W}x{H}", "-i", ":98", "-r", "4", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE, bufsize=W * H * 3 * 2) def glyph(a): r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2] return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum()) def tap(btn, secs=0.5): import os for state in (f"press={btn}", ""): with open("/tmp/xenia_pad.txt.tmp", "w") as f: f.write(state) os.replace("/tmp/xenia_pad.txt.tmp", "/tmp/xenia_pad.txt") if state: time.sleep(secs) T0 = time.time() p, n, seg, streak = _open(), W * H * 3, time.time(), 0 state, pressed_at, frames = "wait_title", None, [] while True: el = time.time() - T0 if el > WAIT: print("TIMEOUT in state " + state, flush=True); break if time.time() - seg > 30: p.kill(); p = _open(); seg = time.time() buf = p.stdout.read(n) if len(buf) < n: p.kill(); p = _open(); seg = time.time(); continue a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(np.uint8) c = glyph(a.astype(int)) if state == "wait_title": streak = streak + 1 if NEED <= c <= CEIL else 0 if streak >= HOLD: print(f"[{el:7.1f}s] BOOT TITLE SETTLED (plate pulse, glyph {c}) — pressing A", flush=True) tap("A", 0.5) pressed_at, state, streak = time.time(), "wait_menu", 0 elif state == "wait_menu": # the menu has no green plate; wait for the glyph count to fall and stay # down, which is the plate leaving, then let the build-in finish. streak = streak + 1 if c < NEED else 0 if streak >= HOLD and time.time() - pressed_at > 8: print(f"[{el:7.1f}s] MENU (glyph {c}) — taking 5 frames 2 s apart", flush=True) for k in range(5): t0 = time.time() while time.time() - t0 < 2.0: b2 = p.stdout.read(n) if len(b2) < n: p.kill(); p = _open(); break a = np.frombuffer(b2, np.uint8).reshape(H, W, 3).astype(np.uint8) Image.fromarray(a).save(f"{OUT}/menu-{k}.png") frames.append(a.astype(int)) print(f" frame {k}: glyph {glyph(a.astype(int))}", flush=True) break p.kill() if len(frames) == 5: x, y, w, h = LOOP rois = [f[SURF_Y + y:SURF_Y + y + h, x:x + w] for f in frames] print("\nptloop01/02 rect, 200x90 at design (441,270) — do they move?") for i in range(1, 5): d = np.abs(rois[i] - rois[0]) print(f" frame {i} vs 0: max |d| {d.max():3d}, px differing >8: " f"{int((d.max(axis=2) > 8).sum()):5d} / {d.shape[0]*d.shape[1]}", flush=True) print(" whole frame, as the CONTRAST CONTROL (if this is 0 too, the") print(" instrument is blind and the rect's 0 means nothing):") for i in range(1, 5): d = np.abs(frames[i] - frames[0]).max(axis=2) print(f" frame {i} vs 0: px differing >8: {int((d > 8).sum()):7d}", flush=True) else: print("NO FRAMES — nothing measured", flush=True)