Files
Sylpheed/tools/re-capture/extras_focus_persistence.py
sylph-decoder 9b7a8b3237 re: the focus reader was two items out -- initial focus is NEW GAME, and my labels were wrong
menu_focus.py's row centres are design-space rows from screenshot output; my
probes fed it whole-display x11grab frames carrying Xenia's chrome and a surface
scaled 1.060. Caught by ground truth, not by a control: the probe announced 'on
EXTRAS', pressed A, and opened OPTIONS.

Measured directly with ring_row.py: initial focus on a fresh boot is NEW GAME,
2/2 fresh boots, both the first menu entry. That agrees with boot_menu.sh's own
line and menu-state-in-memory.md's four-downs, and withdraws this page's
'TUTORIAL 2/2' as the outlier.

Persistence stands and is now geometry-free -- 384.0 vs 385.5, 1.5 px apart. An
equality test is immune to a constant offset, which is why the conclusion survived
a broken reader when the published item names did not.

The control was structurally blind: 'two DOWNs move two items' tests relative
motion, and a constant offset preserves it exactly.

EXTRAS remains unmeasured; that run navigated to OPTIONS.

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

186 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Does the EXTRAS submenu remember its cursor across leave -> re-enter?
sylpheed-port's contract-check asserted that EXTRAS does NOT persist. Nothing
measured that. The corpus has EXTRAS' initial focus as MISSION SELECT from a
SINGLE entry, and Ⓑ-from-a-submenu restoring the PARENT's focus 4/4 -- neither
says what a submenu's own cursor does on re-entry. Their label
`initial_focus: ptbtn11, kind: "measured"` is exposed to the same history problem
raised for the main menu: taken on one entry, it may be reading history.
The main menu was measured to PERSIST (focus-persists-across-title.txt), so the
question is live in both directions.
⚠️ NO HAND-GUESSED GEOMETRY. menu_focus.py's ys are the five-item main menu and
do not transfer to a three-item submenu, and the only EXTRAS capture in the
corpus is a 562x182 crop. So the ring is located by DIFFERENCE instead: the
pixels that change when the cursor moves ARE the ring's footprint. Persistence is
then decided inside that mask, which needs no coordinates at all.
🔴 CONTROL, and the run stops on it: the E1->E2 difference must be ONE COMPACT
CLUSTER. If moving the cursor changes the whole frame, the mask is not the ring
and nothing after it may be read. (sylpheed-port measured one 46x44 cluster,
0.155 % of frame, across a full ring cycle on their renderer -- an independent
expectation for what this should look like.)
extras_focus_persistence.py LOG OUTDIR [wait_s]
"""
import os, re, subprocess, sys, 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 600
W, H = 1280, 720
MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"]
YS = [166, 241, 315, 390, 465]
T0 = time.time()
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", 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 fresh():
"""A CURRENT frame. Reading the pipe after a sleep returns a buffered one."""
q = _open(); a = None
for _ in range(3):
buf = q.stdout.read(W * H * 3)
if len(buf) == W * H * 3:
a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(int)
q.kill()
return a
def glyph(a):
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def mainfocus(a):
g = np.asarray(Image.fromarray(a.astype(np.uint8)).convert("L"), dtype=float)
v = [g[y - 20:y + 20, 500:542].max() for y in YS]
return int(np.argmax(v)), v
def wait_menu(limit=90):
t = time.time(); streak = 0
while time.time() - t < limit:
a = fresh()
if a is None:
continue
if MENU_LO <= glyph(a) <= MENU_HI:
streak += 1
if streak >= 2:
return a
else:
streak = 0
return None
os.makedirs(OUT, exist_ok=True)
# ---- 1. on the main menu, walk to EXTRAS ------------------------------------
a = wait_menu()
if a is None:
sys.exit("never saw the main menu")
f, v = mainfocus(a)
print(f"[{time.time()-T0:7.1f}s] MAIN MENU focus = {NAMES[f]} ring "
+ " ".join(f"{x:5.0f}" for x in v), flush=True)
need = (4 - f) % 5
print(f" {need} DOWN to reach EXTRAS", flush=True)
for _ in range(need):
if not press("DOWN", "5811"):
sys.exit("a DOWN was never delivered")
time.sleep(0.8)
time.sleep(1.5)
a = fresh(); f2, v2 = mainfocus(a)
print(f" now on {NAMES[f2]} ring " + " ".join(f"{x:5.0f}" for x in v2), flush=True)
if f2 != 4:
sys.exit(f"CONTROL FAILED: wanted EXTRAS, on {NAMES[f2]}")
print("✅ on EXTRAS", flush=True)
# ---- 2. enter EXTRAS, capture E1, move, capture E2 --------------------------
if not press("A", "5800"):
sys.exit("A never delivered")
time.sleep(6.0)
E1 = fresh(); Image.fromarray(E1.astype(np.uint8)).save(f"{OUT}/E1.png")
print(f"[{time.time()-T0:7.1f}s] IN EXTRAS (glyph {glyph(E1)}) — E1 captured", flush=True)
if not press("DOWN", "5811"):
sys.exit("a DOWN was never delivered inside EXTRAS")
time.sleep(2.5)
E2 = fresh(); Image.fromarray(E2.astype(np.uint8)).save(f"{OUT}/E2.png")
print(f"[{time.time()-T0:7.1f}s] E2 captured after 1 DOWN", flush=True)
# ---- 3. CONTROL: the difference must be one compact cluster -----------------
d = (np.abs(E1 - E2).max(axis=2) > 24)
frac = d.mean()
ys, xs = np.nonzero(d)
if len(ys) == 0:
sys.exit("🔴 CONTROL FAILED: the cursor did not move at all (E1 == E2)")
bbox = (int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max()))
bw, bh = bbox[2] - bbox[0] + 1, bbox[3] - bbox[1] + 1
dens = len(ys) / (bw * bh)
print(f" diff: {100*frac:.3f}% of frame, bbox {bw}x{bh} at ({bbox[0]},{bbox[1]}), "
f"fill {100*dens:.0f}%", flush=True)
if frac > 0.05:
sys.exit(f"🔴 CONTROL FAILED: {100*frac:.1f}% of the frame changed — that is not a ring")
print("✅ CONTROL PASSED: the move changed one localised region", flush=True)
# ---- 4. leave, re-enter, capture E3 ----------------------------------------
if not press("B", "5801"):
sys.exit("B never delivered")
a = wait_menu()
if a is None:
sys.exit("B did not return to the main menu")
f3, _ = mainfocus(a)
print(f"[{time.time()-T0:7.1f}s] back on the MAIN MENU, focus = {NAMES[f3]}", flush=True)
if not press("A", "5800"):
sys.exit("A never delivered on re-entry")
time.sleep(6.0)
E3 = fresh(); Image.fromarray(E3.astype(np.uint8)).save(f"{OUT}/E3.png")
print(f"[{time.time()-T0:7.1f}s] RE-ENTERED EXTRAS — E3 captured", flush=True)
# ---- 5. decide, inside the ring mask only ----------------------------------
m = d
d1 = float(np.abs(E3 - E1).max(axis=2)[m].mean())
d2 = float(np.abs(E3 - E2).max(axis=2)[m].mean())
print(f"\n inside the ring mask ({int(m.sum())} px):")
print(f" mean |E3 - E1| = {d1:6.2f} (E1 = where EXTRAS opened)")
print(f" mean |E3 - E2| = {d2:6.2f} (E2 = where I left it)")
if d2 < d1 * 0.5:
print("\n=> EXTRAS PERSISTS: re-entry matches where I left the cursor")
elif d1 < d2 * 0.5:
print("\n=> EXTRAS RESETS: re-entry matches where it first opened")
else:
print(f"\n=> UNDECIDED: {d1:.2f} vs {d2:.2f} are too close to separate")
print("EXTRAS FOCUS RUN DONE", flush=True)