diff --git a/docs/game/navigation.md b/docs/game/navigation.md index 185250f8..947889dc 100644 --- a/docs/game/navigation.md +++ b/docs/game/navigation.md @@ -321,6 +321,16 @@ Traps that read as bugs but are not, all measured โœ…: if you must use `-9`, `rm -f /tmp/xenia-canary.lock` after. **And assert the emulator is alive before entering any wait loop** โ€” `ps -C xenia_canary` โ€” so the loop cannot spend its whole deadline on nothing. +* ๐Ÿ”ด **โ’ถ on a settled boot title does NOT reliably reach the menu.** This page's + ยง1 and `canary-scripted-input-traps.md` record "the boot title accepts a single + โ’ถ (2 of 2 runs)". A run on 2026-08-30 gated on the **plate pulse** (glyph in + [500, 2500] held 12 consecutive samples), fired at t=484.5 s with glyph 1723 โ€” + a verified settled boot title, not the attract one โ€” pressed โ’ถ, and **the press + was delivered** (`[file-pad] keystroke vk=5800 down` / `up`, 8 `[RE-INPUT]` + lines). Twenty seconds later every captured frame still classified as the title. + So the sample is no longer 2 of 2, and a script that presses once and proceeds + can be left on the title with nothing in its log to say so. **Confirm the screen + changed; do not infer it from a delivered press.** * **`screen_id.py` reports `menu` during the attract loop.** Two boots on 2026-08-30 logged `menu` at t=106 s and t=418 s while the game was still in the intro movie. A wait-loop that breaks on `menu` will act on the wrong screen; the diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 56bdf97f..26c7c46f 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -345,6 +345,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the precision against the quantity before quoting the number, and prefer comparing *frames* to comparing a statistic about them. +* **A declared rect can be an ANCHOR, not an extent.** A UI element's placement + `(x, y)` plus its `wร—h` reads like the box it draws in. For a `RATC` parent with + a nested record it is not: `ptloop01.rat` declares `(441,270)` 200ร—90 while its + **leaf** sweeps a 400 px-wide quad whose centre runs xโ‰ˆ921โ†’1041 โ€” 300 px outside + the parent's box. Measuring "does this element move" over the parent's rect + returned **0 differing pixels** across two sessions, in a region where *nothing* + moves, and that zero was published. โš ๏ธ Before diffing a region to ask whether an + element animates, confirm the element **draws there** โ€” from a leaf sweep, a draw + capture, or the rendered quad's own coordinates. A pivot is not a bounding box. + ## Runtime / emulator * **Look at the PNG** โ€” and check its dimensions. diff --git a/docs/re/structures/ui-resting-pose.md b/docs/re/structures/ui-resting-pose.md index 039d6480..5d13ccea 100644 --- a/docs/re/structures/ui-resting-pose.md +++ b/docs/re/structures/ui-resting-pose.md @@ -135,7 +135,30 @@ measurement lives in the box, and the in-box between-session noise is **0.32**, first stated โ€” the "strengthening" was reaching for a number that was to hand rather than the one that applies. -### โœ… And `ptloop01/02` do NOT free-run on the settled title +### ๐Ÿ”ด WITHDRAWN โ€” "`ptloop01/02` do not free-run on the settled title" + +**Retracted 2026-08-30, same day, by my own corpus.** The claim below measured the +parent's declared rect at `(441,270)` 200ร—90. **That rect is a PIVOT ANCHOR, not +the drawn extent.** [`data/ptloop-leaf-sweep-positions.txt`](../data/ptloop-leaf-sweep-positions.txt) +โ€” written earlier in this same corpus โ€” records `ptloop01.rat`'s nested record at +loop length **600**, whose leaf `pteff03.t32` sweeps a **400 px-wide** quad with +its centre running xโ‰ˆ921โ†’1041 over t=340..370. That is nowhere inside x 441..640. + +Checked against the two JP captures: the rect I measured differs by **0 px** โ€” but +so does the entire region y 270..450 ร— x 480..960 around it, a dead zone โ€” while +the band the sweep actually occupies (x 721..1241) differs by **44 025 px**. The +zero was measured where nothing happens and proves nothing about the loops. + +โœ… **`sylpheed-port`'s reading is right and is confirmed from the disc**: the +parent is static while the **leaf record animates**, and the two nested records +cycle at *different* lengths, **600 and 720**. My "single static keyframe" was a +statement about the parent only. + +โš ๏ธ The era adjudication is unaffected: its box is x 389..776, and the overlap with +the sweep band (x 721..776) shows no between-session differences โ€” the 645 in-box +differing pixels all sit at y 99..128. + +### ~~โœ… And `ptloop01/02` do NOT free-run on the settled title~~ (see above) The transfer above was flagged as uncertain because build 7 carries `ptloop01/02.rat`, which might animate inside the adjudication box where the EN diff --git a/tools/re-capture/menu_loop_probe.py b/tools/re-capture/menu_loop_probe.py new file mode 100755 index 00000000..134a092a --- /dev/null +++ b/tools/re-capture/menu_loop_probe.py @@ -0,0 +1,111 @@ +#!/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) diff --git a/tools/re-capture/menu_loop_session.sh b/tools/re-capture/menu_loop_session.sh new file mode 100755 index 00000000..d11cfa2b --- /dev/null +++ b/tools/re-capture/menu_loop_session.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Boot, then run menu_loop_probe.py. Asserts the emulator is ALIVE before the +# probe enters its wait loop -- a previous run polled a dead display for 484 s +# because run-canary had refused on a lockfile orphaned by `kill -9`, and +# "no emulator" is indistinguishable from "not the title yet" to a classifier. +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +SD="$(cd "$(dirname "$0")" && pwd)" +OUT="${1:-/sylph-home/re/menuloop3}"; mkdir -p "$OUT" +ps -o pid= -C xenia_canary | xargs -r kill # plain kill: it clears its own lock +rm -f /tmp/xenia-canary.lock +( cd "$OUT" && nohup run-canary --mem_watch=false \ + --logged_profile_slot_0_xuid=B13EBABEBABEBABE \ + >"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & ) +sleep 10 +if ! ps -C xenia_canary >/dev/null 2>&1; then + echo "EMULATOR DID NOT START -- stderr says:"; tail -3 "$OUT/canary.stderr"; exit 4 +fi +echo "emulator alive, starting probe" +timeout 900 python3 "$SD/menu_loop_probe.py" "$OUT" 780 +echo "MENU LOOP PROBE DONE"