#!/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()