F1 TUTORIAL, two delivery-confirmed DOWNs to EXTRAS, B to the title, A back: F3 is EXTRAS. Re-entry restores the item you left. Reframes the initial-focus disagreement rather than settling it: if focus persists, any 'initial focus' reading not taken on a fresh boot's first menu entry measures history. It still says nothing about what the menu opens on -- this run's F1 was itself carried over from a prior probe's press. Reached on the plate-pulse gate, not boot_menu.sh, whose stillness test cannot fire on this title -- TITLE at 422.7 s on a boot skip_intro could not gate at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
197 lines
8.0 KiB
Python
Executable File
197 lines
8.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Does the main menu REMEMBER its cursor across menu -> title -> menu?
|
|
|
|
menu-navigation-semantics.md carries this 🟡: "Initial focus is reproducible but
|
|
not established as invariant. Both of my boots opened on TUTORIAL, and both used
|
|
boot_menu.sh." Two runs through one harness are not two samples. And the sources
|
|
DISAGREE: boot_menu.sh's own closing line says NEW GAME, and
|
|
menu-state-in-memory.md reaches EXTRAS in four downs, which only counts from NEW
|
|
GAME. Two say NEW GAME, one says TUTORIAL.
|
|
|
|
⚠️ THIS DELIBERATELY DOES NOT USE boot_menu.sh. Its title gate admits a "static"
|
|
screen at d <= 1500 between grabs 0.6 s apart, and the title never stills -- the
|
|
sweep leaves free-run. Minimum observed 1551 over 72 samples, 0 able to pass.
|
|
See harness-title-gate-assumes-a-static-title.md. Everything here is instead the
|
|
harness b_from_menu.py validated: plate-pulse title detector, glyph-327 menu
|
|
detector, delivery confirmed from [RE-INPUT] rather than from the pad.
|
|
|
|
SEQUENCE
|
|
F1 focus when the menu first appears <- re-measures initial focus
|
|
F2 focus after 2x DOWN <- CONTROL for the focus reader
|
|
F3 focus after B (to title) then A (back) <- persist or reset?
|
|
|
|
F3 == F2 => the menu restores where you were. F3 == F1 => it resets.
|
|
|
|
🔴 CONTROL GATE: if F2 is not exactly two items below F1 (with wrap), the reader
|
|
is not tracking the cursor and NOTHING after it may be read. The run says so and
|
|
stops rather than reporting a number it cannot justify.
|
|
|
|
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 900
|
|
W, H = 1280, 720
|
|
NEED, CEIL, HOLD = 500, 2500, 12 # title plate pulse band
|
|
MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6 # glyph-327 menu detector
|
|
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
|
|
|
|
# menu_focus.py's geometry, byte for byte: the focus RING sits in the gutter
|
|
# left of the label and nothing else is bright there.
|
|
NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"]
|
|
YS = [166, 241, 315, 390, 465]
|
|
|
|
|
|
def focus(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 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 fresh(proc):
|
|
"""A CURRENT frame, not a buffered one.
|
|
|
|
⚠️ Reading one frame from the pipe after a sleep returns whatever ffmpeg
|
|
buffered while we were not reading. Reopening the stream is the only cheap
|
|
way to be sure the frame is now.
|
|
"""
|
|
proc.kill()
|
|
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)
|
|
return q, a
|
|
|
|
|
|
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()
|
|
os.makedirs(OUT, exist_ok=True)
|
|
log = open(f"{OUT}/series.tsv", "w"); log.write("# t_s\tglyph\tphase\n")
|
|
phase, streak, mark = ("frommenu" if "--from-menu" in sys.argv else "wait"), 0, None
|
|
F1 = F2 = F3 = 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{phase}\n"); log.flush()
|
|
|
|
if phase == "frommenu":
|
|
# already sitting on the menu: fall straight into the menu handler
|
|
streak = MENU_HOLD; phase = "tomenu"
|
|
|
|
elif 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:
|
|
time.sleep(2.0) # let the menu settle
|
|
p, a = fresh(p)
|
|
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/1-F1.png")
|
|
F1, v = focus(a)
|
|
print(f"[{el:7.1f}s] MENU (glyph {c}) F1 = {NAMES[F1]} ring "
|
|
+ " ".join(f"{x:5.0f}" for x in v), flush=True)
|
|
# 🔴 Run 1 pressed DOWN twice through pad.py with NO delivery
|
|
# confirmation and the guest logged vk=5811 exactly ONCE. A and B
|
|
# were confirmed; the d-pad was not, so the run was unreadable.
|
|
# Confirm every press the same way.
|
|
ok = all(press("DOWN", "5811") for _ in range(2))
|
|
if not ok:
|
|
print("🔴 a DOWN was never delivered — refusing to read F2", flush=True)
|
|
break
|
|
time.sleep(1.5)
|
|
p, a = fresh(p)
|
|
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/2-F2.png")
|
|
F2, v = focus(a)
|
|
print(f"[{el:7.1f}s] after 2x DOWN F2 = {NAMES[F2]} ring "
|
|
+ " ".join(f"{x:5.0f}" for x in v), flush=True)
|
|
want = (F1 + 2) % len(NAMES)
|
|
if F2 != want:
|
|
print(f"🔴 CONTROL FAILED: 2x DOWN from {NAMES[F1]} should give "
|
|
f"{NAMES[want]}, read {NAMES[F2]}. The reader is not tracking "
|
|
f"the cursor; refusing to report F3.", flush=True)
|
|
break
|
|
print(f"✅ CONTROL PASSED: 2x DOWN moved {NAMES[F1]} -> {NAMES[F2]}", flush=True)
|
|
press("B", "5801"); mark = time.time(); phase, streak = "backtitle", 0
|
|
|
|
elif phase == "backtitle":
|
|
streak = streak + 1 if NEED <= c <= CEIL else 0
|
|
if streak >= HOLD:
|
|
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/3-title.png")
|
|
print(f"[{el:7.1f}s] BACK AT TITLE (glyph {c})", flush=True)
|
|
press("A", "5800"); phase, streak = "remenu", 0
|
|
elif time.time() - mark > 60:
|
|
print(f"[{el:7.1f}s] B did not reach the title in 60 s (glyph {c})", flush=True)
|
|
break
|
|
|
|
elif phase == "remenu":
|
|
streak = streak + 1 if MENU_LO <= c <= MENU_HI else 0
|
|
if streak >= MENU_HOLD:
|
|
time.sleep(2.0)
|
|
p, a = fresh(p)
|
|
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/4-F3.png")
|
|
F3, v = focus(a)
|
|
print(f"[{el:7.1f}s] MENU AGAIN F3 = {NAMES[F3]} ring "
|
|
+ " ".join(f"{x:5.0f}" for x in v), flush=True)
|
|
print(f"\nF1={NAMES[F1]} F2={NAMES[F2]} F3={NAMES[F3]}")
|
|
if F3 == F2:
|
|
print("=> FOCUS PERSISTS across menu -> title -> menu")
|
|
elif F3 == F1:
|
|
print("=> FOCUS RESETS to its initial item")
|
|
else:
|
|
print("=> NEITHER — F3 matches neither F1 nor F2; unexplained")
|
|
break
|
|
|
|
p.kill()
|
|
print("FOCUS PERSISTENCE RUN DONE", flush=True)
|