Files
Sylpheed/tools/re-capture/b_from_menu.py
sylph-decoder 78cb1d4e9b re: B on the main menu goes to the TITLE -- measured, filling an empty evidence cell
menu-navigation-semantics.md had this row at yellow with an EMPTY evidence cell,
and it is what the port still authors as on_cancel.

Delivery-confirmed via [RE-INPUT] (B is kXInputPadB = 0x5801), change detected
rather than timed. B delivered at 331.2 s; the glyph leaves 327 by 331.6 and
73.5 % of pixels differ. Both captures name themselves: PROJECT SYLPHEED with the
(C)2006,2007 SQUARE ENIX line.

Three things measured:
  * B on the main menu goes to the title;
  * latency <= 0.4 s at a 4 Hz sample rate, where the corpus previously had this
    as 'not measured (a backlogged probe void)';
  * NO loading screen in between -- the disc carries four pgloading_* bundles and
    none appears on this path.

What the run CANNOT say, recorded in the table rather than glossed: 'B on the
title -> nothing' is still unevidenced. The second B was delivered during the
title's build-in, so the glyph 0 -> 154 change after it is the build-in
completing, not a response. A run that answers that row must wait for the title
to settle before pressing.

The 're-draws PRESS A after a beat' half of the first row is also still
unevidenced -- the run ended with the plate absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 11:06:18 +00:00

126 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""What does Ⓑ do on the MAIN MENU, and then on the TITLE?
`menu-navigation-semantics.md` has those two rows at 🟡 with **empty evidence
cells** — "goes to the title, which re-draws PRESS Ⓐ after a beat" and "nothing"
while `Ⓑ on a submenu` is 4/4 measured. They are what the port still authors as
`on_cancel`.
Everything here is the harness validated in `tbm-submenu-not-reached.md`:
the plate-pulse title detector, the glyph-327 menu detector, **delivery confirmed
from `[RE-INPUT]` rather than from the pad**, and change detected rather than timed.
Ⓑ is `kXInputPadB = 0x5801` (`ui/virtual_key.h:323`).
b_from_menu.py LOG OUTDIR [wait_s]
"""
import os
import re
import subprocess
import sys
import time
import numpy as np
from PIL import Image
LOG, OUT = sys.argv[1], sys.argv[2]
WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 480
W, H = 1280, 720
NEED, CEIL, HOLD = 500, 2500, 12
MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
def deliveries(vk):
pat = re.compile((r"RE-INPUT\] XamInputGetKeystrokeEx -> user=\d+ vk=%s flags=0001" % vk).encode())
try:
return len(pat.findall(open(LOG, "rb").read()))
except FileNotFoundError:
return 0
def press(btn, vk, tries=5):
for k in range(tries):
before = deliveries(vk)
subprocess.run([sys.executable, PAD, "tap", btn, "0.5"], check=False)
for _ in range(20):
time.sleep(0.25)
if deliveries(vk) > before:
print(f"[{time.time()-T0:7.1f}s] {btn} delivered (attempt {k+1})", flush=True)
return True
print(f"[{time.time()-T0:7.1f}s] {btn} NOT delivered (attempt {k+1})", flush=True)
return False
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())
T0 = time.time()
p, n, seg = _open(), W * H * 3, time.time()
log = open(f"{OUT}/series.tsv", "w"); log.write("# t_s\tglyph\tmean\tphase\n")
phase, streak, base, stable, mark = "wait", 0, None, 0, None
while True:
el = time.time() - T0
if el > WAIT:
print(f"TIMEOUT in phase {phase}", 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(int)
c = glyph(a)
log.write(f"{el:.3f}\t{c}\t{a.mean():.3f}\t{phase}\n"); log.flush()
if phase == "wait":
streak = streak + 1 if NEED <= c <= CEIL else 0
if streak >= HOLD:
print(f"[{el:7.1f}s] TITLE (glyph {c})", flush=True)
press("A", "5800"); phase, streak = "tomenu", 0
elif phase == "tomenu":
streak = streak + 1 if MENU_LO <= c <= MENU_HI else 0
if streak >= MENU_HOLD:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/1-menu.png")
base = a.astype(float)
print(f"[{el:7.1f}s] MENU (glyph {c}) — pressing B", flush=True)
time.sleep(2.0)
press("B", "5801"); mark = time.time(); phase, stable = "afterB1", 0
elif phase == "afterB1":
diff = float((np.abs(a - base).max(axis=2) > 12).mean())
if diff > 0.25:
stable += 1
if stable >= 8:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/2-after-B-on-menu.png")
print(f"[{el:7.1f}s] B ON MENU CHANGED THE SCREEN: {100*diff:.1f}% differ, glyph {c}", flush=True)
base = a.astype(float); time.sleep(3.0)
press("B", "5801"); mark = time.time(); phase, stable = "afterB2", 0
else:
stable = 0
if time.time() - mark > 30:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/2-after-B-on-menu.png")
print(f"[{el:7.1f}s] B ON MENU: NO CHANGE in 30 s ({100*diff:.1f}% differ, glyph {c})", flush=True)
base = a.astype(float); press("B", "5801"); mark = time.time(); phase, stable = "afterB2", 0
elif phase == "afterB2":
diff = float((np.abs(a - base).max(axis=2) > 12).mean())
if diff > 0.25:
stable += 1
if stable >= 8:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/3-after-B-again.png")
print(f"[{el:7.1f}s] SECOND B CHANGED THE SCREEN: {100*diff:.1f}% differ, glyph {c}", flush=True)
break
else:
stable = 0
if time.time() - mark > 30:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/3-after-B-again.png")
print(f"[{el:7.1f}s] SECOND B: NO CHANGE in 30 s ({100*diff:.1f}% differ, glyph {c})", flush=True)
break
p.kill(); log.close()