Files
Sylpheed/tools/re-capture/menu_state.py
Sylpheed RE agent 534da24658 re: find the menu screen id and cursor in guest memory
Menu navigation here has always been screenshot-driven, which is unusable under
--gpu=null -- the only configuration where the game does not hit the
software-rasterizer freeze.  Without a memory signal, the one backend that runs
is the one that cannot be steered.

Snapshotting 0x82800000+3 MB at each menu of a rendered run and keeping the
4-byte words that differ between screens and hold small integers leaves exactly
four of 786 432.  One has the property that matters -- it changes on a screen
transition and holds steady when only the highlight moves:

    0x828A690C  screen id     1 title, 3 main menu, 4 extras
    0x828F38AC  menu cursor   (second copy at 0x828F38BC)
    0x828F37B4  per-menu value

Verified on a fresh --gpu=null run with no display at all, driving the same keys
blind: title 1/2/12, main menu 3/4/45, after 4x down 3/12/45, extras 4/14/49 --
4/4 exact against the rendered run, across two runs and two GPU backends.  That
is the check that matters, since this corpus has already had to mark one runtime
address run-dependent.

tools/re-capture/menu_state.py reads them; `menu_state.py watch` prints on
change.

Open: the rest of the sequence into a mission.  Blind driving reached extras
(screen 4) and a further A did not move it, so MISSION SELECT needs a cursor
move first.  Screen ids beyond 4 are unmapped, and the rendered run freezes on
entering that screen -- so map ids up to the freeze, then step blind past it.
2026-08-26 18:21:04 +00:00

62 lines
1.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Read the guest's menu state — screen id and cursor — from memory.
Menu navigation in this corpus has always been driven by screenshots. That
breaks under `--gpu=null`, which is the only configuration where the game does
not hit the software-rasterizer freeze
(`docs/re/mission-freeze-heap-exhaustion.md`), so a memory-based signal is what
makes that backend usable as an oracle.
./menu_state.py # print screen/cursor once
./menu_state.py watch [n] # poll n times, one line per change
Found by snapshotting `0x82800000`+3 MB at each menu of a *rendered* run and
keeping the words that change on a screen transition but hold steady when only
the highlight moves.
"""
import os
import struct
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem # noqa: E402
import gworld # noqa: E402
SCREEN = 0x828A690C # 1 title, 3 main menu, 4 extras
CURSOR = 0x828F38AC # menu highlight; a second copy lives at 0x828F38BC
MISC = 0x828F37B4 # tracks the screen, distinct values per menu
SCREEN_NAMES = {0: "booting", 1: "title", 3: "main-menu", 4: "extras"}
def read(w, va):
return struct.unpack(">I", os.pread(w.fd, 4, gmem.va_to_off(va)))[0]
def state(w):
s, c, m = read(w, SCREEN), read(w, CURSOR), read(w, MISC)
return s, c, m, SCREEN_NAMES.get(s, f"unknown({s})")
def main():
w = gworld.World()
if len(sys.argv) > 1 and sys.argv[1] == "watch":
n = int(sys.argv[2]) if len(sys.argv) > 2 else 60
last = None
for _ in range(n):
cur = state(w)
if cur != last:
print(f"screen={cur[0]} ({cur[3]}) cursor={cur[1]} misc={cur[2]}",
flush=True)
last = cur
time.sleep(1)
else:
s, c, m, name = state(w)
print(f"screen={s} ({name}) cursor={c} misc={m}")
if __name__ == "__main__":
main()