Files
Sylpheed/tools/re-capture
sylph-decoder f3d3594c9c re: the decaying quad is the INCOMING screen -- and (A) and (B) are different shapes
Last iteration left an unidentified full-screen untextured quad decaying
255->15 during a menu->title transition, which build 5's declaration does not
account for. Hypothesis: it is the INCOMING screen's own pteff00, which opens at
a255 and clears. 8 frames matching build 4's declared 16 units is a FIT, so the
test was a transition whose incoming screen declares something else: title->menu
brings in build 5, 0->12 = 12 units = 6 frames. Prediction recorded before the
run.

Measured: menu->title decay 8 frames (incoming build 4, declared 8), title->menu
decay 5 frames (incoming build 5, declared 6). Different incoming screen,
different decay, in the predicted direction. The second is one frame short of
prediction, inside the documented +-1.

The tell that clinches it: a screen contributes TWO primitives, pteff00 at 255
and pteff02 at 64. The settled menu's untextured set is [64]; at frame 34 it
becomes [64, 255, 64] -- build 4's opening pair, which no single element
explains.

Bonus, and it closes the alpha puzzle: in capture 2 the outgoing quad ramps with
no other untextured quad present -- 63, 127, 191, 255, steps of exactly 64, four
frames, against build 4's declared 261->269 = 8 units = 4 frames. Exact and
exactly linear. Capture 1's 102/127/255 was a composite of two overlapping
quads, as sylpheed-port proposed.

The thing neither of us predicted: the two directions are not the same shape.
(A) title->menu is SEQUENTIAL with a real black interval of 5 frames (~10 units,
against the port's authored 9). (B) menu->title is a CROSS-FADE with no black
interval at all -- the incoming title starts drawing at frame 34, before the
outgoing menu's quad begins ramping at 40. Authoring one hold for both directions
inserts black that (B) does not have.

Also fixed: fade_pair.py's automatic rising/decaying classifier worked on capture
1 and produced nonsense on capture 2, where the title has no full-screen
primitive at rest and the heuristic latched onto a transient. It now prints and
does not decide.

Refutation attempted: sylpheed-port's structural prediction of a 6-frame decay
for an incoming menu. Measured 5. Survives as direction, one frame short as
duration; recorded as both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 13:51:48 +00:00
..

Runtime-capture harness (sylph-re container)

Screenshot-driven scripts for reading the running retail game's menus under Xenia Canary + lavapipe, headless. They assume the container helpers screenshot, vgamepad, pad are on $PATH and HOME=/sylph-home/re.

Script What it does
skip_intro.sh Boot → main menu, unattended. Taps A only while the intro movie is actually playing (frame-to-frame RMSE), then once at the PRESS Ⓐ BUTTON title. Static logo screens are left alone, so a stray tap can never land on NEW GAME.
wait_title.sh Older variant: wait for the title (green Ⓐ glyph at px 625,618) and tap A. Superseded by skip_intro.sh.
step.sh One Arsenal navigation step (down/up/next/prev/none) + a compact capture: weapon list stacked over the DATA SHEET.
sweep.sh Walk a whole weapon-type list, capturing only rows that show a DATA SHEET — locked rows (a "Conditions to Develop" panel) are detected by the brightness of the Range Class label box and skipped.
type.sh Change weapon-type tab N times (RB) and report the header strip.
hp.sh / cyc.sh Hangar hard-point carousel: cyc.sh steps it (d-pad down, not left/right) and captures the Name + DATA SHEET.

Input timing under lavapipe: the game polls input at its own low frame rate, so a 60 ms d-pad tap is dropped roughly half the time. 200 ms is reliable; 300 ms starts to auto-repeat (two rows per press).

Findings produced with these: docs/re/weapon-datasheet-runtime.md.

Guest-memory tools (no screenshots)

Script What it does
gmem.py Read the live guest address space out of /dev/shm/xenia_memory_* (Xenia's backing file), addressed by guest VA. find / read / words.
weapon_runtime.py Solve the Weapon/Shell struct layouts against the disc records and read the fields the disc defaults.
unit_discover.py Find which runtime class carries a set of ID strings, assuming no vtable: tallies the word at pointer_site - k across distinct IDs.
unit_runtime.py Same solver for the unit\UN_*.tbl definition objects (vtable 0x820af844). Unions several snapshots — unit definitions are per-stage.
schema_order.py Merge a sub-record's field-declaration order across all tables (topological sort); the layout check that pins fields no table ever values.
order_check.py Test offset = base + 4*index for one table's sub-record against solver output.
grab_tutorial.sh Cold-boot Canary, walk to the Nth TUTORIAL entry, wait for the stage load, snapshot guest RAM. One emulator per capture — backing out of a loaded mission wedges it.

Snapshot first — cp --sparse=always /dev/shm/xenia_memory_* snap.bin (~2 s) — and point $GMEM_FILE at the copy; the running emulator pegs every core under lavapipe.

🔴 pgrep -f / pkill -f match YOUR OWN shell

An agent driving this toolkit runs its commands through a wrapper shell whose command line contains the pattern being searched for. So

pgrep -f 'pilot\.py'        # matches the wrapper running this very command
pkill -f 'fly_session|pilot\.py'   # kills that wrapper — the script dies mid-way

This has cost four separate mistakes in one session: two scripts killed mid-execution, and twice a "is it already running?" guard that answered yes because it had found itself. The [p]ilot bracket trick does not help when the literal invocation (python3 pilot.py …) also appears on the wrapper's command line.

What works:

pgrep -x xenia_canary                       # exact NAME match, no -f
ps -eo pid,args | grep 'python3 pilot.py' | grep -v snapshot-bash
kill -9 <explicit pid>                      # look it up first, then kill by pid

The snapshot-bash filter is the reliable tell: the wrapper's command line always contains the shell-snapshot path.

🔴 The emulator does not survive the end of an agent turn

/work/.claude/settings.json defines a Stop hook that kill -9s every xenia_canary when a turn ends, printing "Stop hook killed N stale xenia process(es)".

So:

  • never launch a run intending to read it in a later turn — it will be dead;
  • an experiment has to produce its evidence within the turn that starts it;
  • prefer measurements that land in the first minutes of flight. REMAINING OB steps 4 → 8 → 12 inside the first few minutes, which is why a two-pass differential works in one turn while a 25-minute freeze watch does not.

This cost three iterations of investigating a "mysterious external SIGKILL", complete with cgroup and host memory forensics, before the hook was found. When a process dies at a session boundary, check the harness first.