Capturing the chapter-1 spine story-flag writers
Legaia records story progress as story flags - single bits in a RAM bitmap that scene scripts check to decide what
happens next. The "spine" is the chain of mandatory chapter-1 story beats, and the question this page answers is: which code sets each
spine flag? The writes happen inside overlays (code the game loads into RAM on demand) that were never imported into the disassembler,
so static analysis alone cannot name the writer. This runbook is the dynamic answer - one interactive PCSX-Redux play-forward session, human
at the controls, that arms watchpoints on all three spine writes at once and logs each writer's return address (ra - i.e.
who called the flag-setter). It is maintainer infrastructure, kept as a worked example of a live writer hunt.
What gets watched
Three targets, two watch methods: a plain write-watch on a RAM address, and an execution breakpoint on the game's own flag-setter function (0x8003CE08) filtered by the flag index in its a0 argument register - the latter names the calling code directly.
| Target | What it gates | Watch method |
|---|---|---|
DAT_8007b7fc = 0x4B | Write-watch 0x8007b7fc, width 1 | |
system flag 0x142 | dolk-dungeon clear | Exec-bp 0x8003CE08, a0 == 322 |
system flag 0x482 | Drake mist walls | Exec-bp 0x8003CE08, a0 == 1154 |
Correction - the
DAT_8007b7fcZeto leg is moot. A live firehose fromchapter2_garmel_pre_zeto(interpreter, this exact write-watch armed width-1 and width-4) stayed silent across three reproducible Zeto fights. Zeto (0x4B, byte-confirmed name in PROT 867) fights in scenegarmel, and its formation[0x4B,0,0,0]is installed by theFUN_801DA51Cactor[+0x94]record path - not theDAT_8007b7fcbattle-id path (which reads0everywhere and may be vestigial). Thejouscoping below is superseded. The flag legs (0x142/0x482/549) are unaffected; keep the runbook for them. Full detail: encounter.html.
The probe is autorun_spine_flag_writers.lua. It installs a bare Vsync listener with no self-quit (this is a human-at-the-controls session), arms the watches once the game settles into field mode, and streams every hit to a CSV.
Localise first with the fast poll tier. These exec-bp watches force the interpreter (~10 fps). Before a targeted session, a --fast dynarec run of autorun_state_poll.lua (the two-tier model) logs which scene each flag changes in at full speed - point the exec-bp firehose there for the writer ra. Both probes share the USA-build version guard (lib/probe/version.lua).
Derived watch addresses
Flag-bank geometry (SCUS-resident): base 0x80085758, byte = base + (idx >> 3), mask = 0x80 >> (idx & 7).
| Flag | idx (dec) | Bank byte | Mask | Fallback Write-watch |
|---|---|---|---|---|
0x142 (dolk clear) | 322 | 0x80085780 | 0x20 | byte 0x80085780 bit 0x20 |
0x482 (mist walls) | 1154 | 0x800857E8 | 0x20 | byte 0x800857E8 bit 0x20 |
The Exec-bp path is preferred: it isolates the exact flag AND names the writer ra directly, which a raw byte watch can't (eight flags share one bank byte). Set LEGAIA_FLAG_FALLBACK=1 to additionally arm the raw byte watches if a flag write is somehow missed by the setter breakpoint (then decode the ra by hand). The Zeto byte watch is width 1; if it never fires, set LEGAIA_ZETO_WIDTH=4 to widen it to the full word.
Which emulator
PCSX-Redux, not mednafen. These are live watchpoints, which need the interpreter + debugger. run_probe.sh sets -interpreter -debugger by default; do not pass --fast - Lua breakpoints do not fire under the recompiler.
Which card slot for which beat
The chapter-1 ladder card saves/library/cards/playthrough-ladder-pro00-14.mcr holds 15 active slots (PRO-00..14, blocks 1..15). Story-flag state was read directly from each SC block's bitmap (block offset 0x14C0, mirroring RAM 0x80085600). Load the slot nearest the beat you want, then play forward:
| Beat / write | Load save | Why | Fallback |
|---|---|---|---|
Zeto write (0x8007b7fc=0x4B) | PRO-01 | keikoku + dolk done, Zeto not beaten; tightest pre-trigger | PRO-05 |
flag 0x142 (dolk clear) | PRO-00 | everything unset; play through the dolk dungeon to its clear beat | — |
flag 0x482 (mist walls) | (not on this card) | 0x482 is unset in all 15 slots; no catalogued save reaches it | see note |
Load saves by name on the in-game load screen, never by block position: the card's physical block order does not follow the PRO numbering (verify with save-tool saves <card>; e.g. PRO-01 sits mid-card, after PRO-03/04/05).
PRO-00 is also a viable single start for a forward run past the Caruban beat (0x142). (The old jou-side Zeto battle-id write it once targeted was falsified live - see the correction note.)
Mist-wall caveat. Flag 0x482 is unset across every slot of this card (it matches the library-wide zero the backlog notes). It is not bracketable from this card: the operator must keep the watch armed and play forward past the Zeto beat to the Drake mist-wall story event, or source a different card that sits nearer it. Treat this as the one leg that needs fresh forward play - the other two are one short walk from their load slot.
The firehose variant - capture every flag write, not just the spine
autorun_flag_firehose.lua generalizes this session: instead of filtering to the three spine targets it logs every story-flag SET (FUN_8003CE08) and CLEAR (FUN_8003CE34) with the writer ra, every battle-id staging write (0x8007B7FC), and every scene-name / game-mode transition as a context timeline - one CSV (flag_firehose.csv: tick,kind,value,pc,ra,mode,scene,count).
Per-key repeat suppression (first 8 of each (kind,flag,ra), then every 64th with a running count) keeps hot per-tick callers from flooding the file; a long session stays in the hundreds of KB. Prefer it for any play-forward longer than a single beat: the same session that catches the spine writers also banks the full flag-provenance stream for later analysis, so nothing needs re-capturing. Same launch shape, same emulator constraints, same attribute_overlay_hits.py post-pass per row.
What the captures settled
- Flag
0x142- CAUGHT, then closed statically. The SET fires at the rikuroa post-Caruban beat ("dolk clear" was a mislabel),ra 0x801E3598= the field-VM dispatcher's own0x5xSET arm. The source is script bytes51 42in the scene's streaming variant MAN (PROT0157_rikuroa, recordsP1[10..12]+ the self-latching post-victoryP2[50]; carrier + census in script-vm). Save-state bracket catalogued asrikuroa_pre_caruban/rikuroa_post_caruban. - Story-flag provenance model (capture-proven). Across every chapter-1 scene traversed, story flags are written exclusively by the
0x5x/0x6xscript ops; every other setter caller is an engine system touching low indices (0/3entity-SM staging,0x35battle-end victory inFUN_8004E568,0xB/0xC/0x18interaction locks,0xEdispatcher spawn ops). - Attribution caveat: the static field-overlay image is WRONG at the writer VAs (over-read/alias) - attribute callers by disassembling the resident bytes from a same-mode save state, not from static overlay dumps.
- The carrier-complete census closed
0x1BEstatically (geremi's Jeremi-arrival one-shot - never a Zeto gate). Its0x482closure did NOT hold: every census site for0x482is a desynced-walker alias of SJIS text (hand-disasm-falsified per class; the census now flags decode coherence per site -GFlagSite::clean). TheDAT_8007B7FCbattle-id write was falsified live (see the correction note: silent at the Zeto trigger, Zeto is agarmelrecord-path fight) and is no longer a standing target. TWO capture targets stand: flag549(0x225) and flag0x482(Drake mist walls) - both direct code paths; for0x482arm a write-watch across the post-Zeto Drake-revival beat (chapter2_garmel_pre_zetobrackets it).
Running the probe
Wrap the launch in timeout --kill-after - the probe never exits on its own, so an unwrapped run would hang the emulator open indefinitely.
Card-save play (the primary mode). Configure PCSX-Redux to use the ladder .mcr as memory card 1 (persisted in its GUI settings, or copied into the emulator's memcards dir before launch), then cold-boot and load the slot from the in-game load screen:
LEGAIA_NO_SSTATE=1 \
xvfb-run -a timeout --kill-after=15s 1800s \
bash scripts/pcsx-redux/run_probe.sh \
--lua scripts/pcsx-redux/autorun_spine_flag_writers.lua
(Drop xvfb-run -a if you want to see the window and drive it yourself; raise the 1800s budget for a long forward run.)
Save-state seed (alternative). If you already hold a .sstate near a beat, skip LEGAIA_NO_SSTATE and point LEGAIA_SSTATE at it:
LEGAIA_SSTATE=/path/to/pre-zeto.sstate \
xvfb-run -a timeout --kill-after=15s 900s \
bash scripts/pcsx-redux/run_probe.sh \
--lua scripts/pcsx-redux/autorun_spine_flag_writers.lua
Beat order for one full session
With PRO-00 loaded (or the tightest slot per beat), the flags fall in story order, so a single armed session can sweep several:
- keikoku - walk the Kikoku Cliff leg (sets
0x193; not watched, but the marker that you're on the spine). - rikuroa (Caruban trigger) - enter Mt. Rikuroa; Caruban is
0x142-gated. (Thezeto_battle_idwatch onDAT_8007b7fcwas falsified live and stays silent - see the correction note; Caruban's own formation mechanism is still un-captured and worth re-checking with the firehose.) - Caruban victory - win the fight; the post-victory record
P2[50]sets0x142(the write the firehose caught). - dolk clear - clear the dolk dungeon to its clear beat; the
flag_0x142_dolk_clearrow fires. - mist-wall event - continue to the Drake mist-wall story event; the
flag_0x482_mist_wallsrow fires (this leg needs fresh forward play; see the caveat above).
What a “caught” hit looks like
Output lands under captures/spine_flag_writers/<run-ts>/:
spine_flag_writers.csv- one row per hit:tick,label,addr,pc,ra,value.spine_flag_writers.detail.txt- call-context (GPRs + code + stack) for the first N hits of each label (LEGAIA_MAX_DETAIL, default 8).
A caught write is a CSV row whose ra column is non-zero - that is the writer's return address, the whole point of the hunt. Concretely:
zeto_battle_id-valueshould read75(0x4B);pcis the store site andraits caller.flag_0x142_dolk_clear/flag_0x482_mist_walls-pcis the setter0x8003CE08,valueis the flag index (322/1154), andrais the game-logic caller that requested the set. Thatrais what to attribute by containment (attribute_overlay_hits.py) to find the owning overlay.
The [spine] HIT ... lines in pcsx.log mirror each CSV row live, so you can watch the hunt land without opening the CSV.
Catalogue the missing pre-write states
Each beat boundary is also a chance to close a gap in the state library: no catalogued state brackets any of these three writes. At each boundary (pre-Zeto rikuroa, pre-dolk-clear, pre/post-mist), save a PCSX-Redux state, then fingerprint and catalogue it so the library gains the missing brackets:
scripts/manage-states.py fingerprint # compute the RAM fingerprint
scripts/manage-states.py library # confirm it's backed up + catalogued
Load states by fingerprint, not slot number (slot indices are ephemeral and get overwritten). These additions are valuable regardless of what the watches catch this run.