The doubt was my own artefact: the entry dump printed only the first two sprite names in HashMap order, making 11 and 14 look like different studios. Full sets are identical. 7 of 8 pairs declare identical sprite sets, control included. 4/7 does not: entry 7 carries nine sprites entry 4 lacks, including ptlogo_jp and ptlogo_jpeff, so the Japanese title is a different element inventory rather than the same screen localised. That matches the JP capture work from the other side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
183 lines
7.2 KiB
Python
183 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Does the EXTRAS submenu remember its cursor across leave -> re-enter?
|
|
|
|
sylpheed-port's contract-check asserts that EXTRAS does NOT persist. Nothing
|
|
measured that: the corpus has EXTRAS' initial focus 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. The main menu was measured to PERSIST, so
|
|
the question is live in both directions, and their `kind: "measured"` label on
|
|
`initial_focus: ptbtn11` turns on it.
|
|
|
|
🔴 RUN 1 (2026-08-30) WAS VOID AND ITS FAILURES SHAPE THIS FILE:
|
|
* it navigated to OPTIONS believing it was EXTRAS -- the focus reader used
|
|
design-space rows against x11grab frames (menu-focus-reader-offset.txt);
|
|
* its screen detector could not tell the main menu from a submenu, because
|
|
both sit inside the glyph 250..420 window (main menu 327, OPTIONS 317);
|
|
* its control checked only that the ring MOVED, which a wrong origin passes.
|
|
|
|
So this run: absolute row checks against a measured calibration, screen identity
|
|
against a REFERENCE FRAME captured in this same run, and raw ring ROWS compared
|
|
inside the submenu -- no submenu geometry is assumed or needed.
|
|
|
|
extras_focus_persistence.py LOG OUTDIR [wait_s]
|
|
"""
|
|
import os, re, subprocess, sys, time
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from ring_row import ring_row, main_menu_item, NAMES, ROW0, SPACING
|
|
|
|
LOG, OUT = sys.argv[1], sys.argv[2]
|
|
WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 600
|
|
W, H = 1280, 720
|
|
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
|
|
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} NEVER delivered", 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():
|
|
q = _open(); a = None
|
|
for _ in range(3):
|
|
b = q.stdout.read(W * H * 3)
|
|
if len(b) == W * H * 3:
|
|
a = np.frombuffer(b, np.uint8).reshape(H, W, 3).astype(int)
|
|
q.kill()
|
|
return a
|
|
|
|
|
|
def img(a):
|
|
return Image.fromarray(a.astype(np.uint8))
|
|
|
|
|
|
def differs(a, b):
|
|
return float((np.abs(a - b).max(axis=2) > 24).mean())
|
|
|
|
|
|
def glyph(a):
|
|
r, g, bl = a[:, :, 0], a[:, :, 1], a[:, :, 2]
|
|
return int(((g > 130) & (g - r > 45) & (g - bl > 45)).sum())
|
|
|
|
|
|
def wait_until(pred, what, limit=60):
|
|
t = time.time()
|
|
while time.time() - t < limit:
|
|
a = fresh()
|
|
if a is not None and pred(a):
|
|
print(f"[{time.time()-T0:7.1f}s] {what}", flush=True)
|
|
return a
|
|
print(f"[{time.time()-T0:7.1f}s] 🔴 TIMEOUT waiting for {what}", flush=True)
|
|
return None
|
|
|
|
|
|
os.makedirs(OUT, exist_ok=True)
|
|
|
|
# ---- 1. main menu: reference frame + calibrated focus ------------------------
|
|
MAIN = wait_until(lambda a: main_menu_item(ring_row(img(a))) is not None
|
|
and 250 <= glyph(a) <= 420, "MAIN MENU reference captured", 120)
|
|
if MAIN is None:
|
|
sys.exit("never identified the main menu")
|
|
img(MAIN).save(f"{OUT}/0-main-ref.png")
|
|
f = main_menu_item(ring_row(img(MAIN)))
|
|
print(f" focus = {NAMES[f]} (ring y {ring_row(img(MAIN))})", flush=True)
|
|
|
|
# ---- 2. walk to EXTRAS, checking the ABSOLUTE row after every press ----------
|
|
for step in range((4 - f) % 5):
|
|
if not press("DOWN", "5811"):
|
|
sys.exit("a DOWN was never delivered")
|
|
time.sleep(1.2)
|
|
a = fresh(); y = ring_row(img(a)); i = main_menu_item(y)
|
|
want = (f + step + 1) % 5
|
|
print(f" step {step+1}: ring y {y} -> {NAMES[i] if i is not None else '??'} "
|
|
f"(want {NAMES[want]})", flush=True)
|
|
if i != want:
|
|
sys.exit(f"🔴 CONTROL FAILED: after {step+1} DOWN the ring is not on {NAMES[want]}")
|
|
print("✅ on EXTRAS, verified by absolute row after every press", flush=True)
|
|
|
|
# ---- 3. enter EXTRAS -- and prove we LEFT the main menu ----------------------
|
|
if not press("A", "5800"):
|
|
sys.exit("A never delivered")
|
|
E1 = wait_until(lambda a: differs(a, MAIN) > 0.20, "left the main menu", 40)
|
|
if E1 is None:
|
|
sys.exit("A did not change the screen")
|
|
time.sleep(3.0)
|
|
E1 = fresh(); img(E1).save(f"{OUT}/E1.png")
|
|
y1 = ring_row(img(E1))
|
|
print(f"[{time.time()-T0:7.1f}s] E1 in the submenu: ring y = {y1}, "
|
|
f"glyph {glyph(E1)}, {100*differs(E1, MAIN):.1f}% from main", flush=True)
|
|
|
|
# ---- 4. move the cursor, control on an ABSOLUTE change ----------------------
|
|
if not press("DOWN", "5811"):
|
|
sys.exit("a DOWN was never delivered inside the submenu")
|
|
time.sleep(2.5)
|
|
E2 = fresh(); img(E2).save(f"{OUT}/E2.png")
|
|
y2 = ring_row(img(E2))
|
|
print(f"[{time.time()-T0:7.1f}s] E2 after 1 DOWN: ring y = {y2}", flush=True)
|
|
if y1 is None or y2 is None:
|
|
sys.exit("🔴 CONTROL FAILED: no ring found in the submenu — this reader does not work here")
|
|
if abs(y2 - y1) < 20:
|
|
sys.exit(f"🔴 CONTROL FAILED: the ring did not move ({y1} -> {y2})")
|
|
print(f"✅ CONTROL PASSED: the ring moved {y1} -> {y2} ({abs(y2-y1):.1f} px)", flush=True)
|
|
|
|
# ---- 5. leave, prove we are back on the main menu, re-enter -----------------
|
|
if not press("B", "5801"):
|
|
sys.exit("B never delivered")
|
|
back = wait_until(lambda a: differs(a, MAIN) < 0.15, "back on the MAIN MENU (vs reference)", 60)
|
|
if back is None:
|
|
sys.exit("🔴 B did not return to the main menu — refusing to read E3")
|
|
if not press("A", "5800"):
|
|
sys.exit("A never delivered on re-entry")
|
|
E3 = wait_until(lambda a: differs(a, MAIN) > 0.20, "left the main menu again", 40)
|
|
if E3 is None:
|
|
sys.exit("re-entry did not change the screen")
|
|
time.sleep(3.0)
|
|
E3 = fresh(); img(E3).save(f"{OUT}/E3.png")
|
|
y3 = ring_row(img(E3))
|
|
same_screen = differs(E3, E1) < 0.15
|
|
print(f"[{time.time()-T0:7.1f}s] E3 on re-entry: ring y = {y3}, "
|
|
f"{100*differs(E3, E1):.1f}% from E1 — same screen: {same_screen}", flush=True)
|
|
|
|
# ---- 6. decide ------------------------------------------------------------
|
|
print(f"\n E1 (opened on) ring y = {y1}")
|
|
print(f" E2 (left it on) ring y = {y2}")
|
|
print(f" E3 (re-entered) ring y = {y3}")
|
|
if not same_screen:
|
|
print("\n=> VOID: re-entry is not the same screen; nothing measured")
|
|
elif y3 is None:
|
|
print("\n=> VOID: no ring on re-entry")
|
|
elif abs(y3 - y2) < 20 and abs(y3 - y1) >= 20:
|
|
print("\n=> EXTRAS PERSISTS: re-entry is where I left the cursor")
|
|
elif abs(y3 - y1) < 20 and abs(y3 - y2) >= 20:
|
|
print("\n=> EXTRAS RESETS: re-entry is where it first opened")
|
|
else:
|
|
print(f"\n=> UNDECIDED: |E3-E1|={abs(y3-y1):.1f} |E3-E2|={abs(y3-y2):.1f}")
|
|
print("EXTRAS FOCUS RUN DONE", flush=True)
|