Files
Sylpheed/docs/re/script-runtime-probe.md
Sylpheed RE agent 273690cf56 re: locate the live ScriptMission/ScriptPhase without a debugger
Unblocks the phase experiment, which was stuck because '38 enemies died' could
not say whether the right ones did. Chasing craft->squadron was the wrong angle:
the script VM keeps that table itself, indexed by the .ssb symbol-table-2 index.

Route: find the .ssb header in guest memory (0xAB840010 for a Stage 02 run),
code base = filebase + 0x24, scan for a word equal to it, then VALIDATE
arithmetically -- [ScriptMission+44] must equal filebase + symtab1 offset + 4.
Measured 0xAB874C94, predicted 0xAB874C94, exact. A second candidate that also
pointed at the code base failed that check and was discarded; without it either
would have looked plausible.

ScriptPhase+324 -> +4 is an array of 122 per-unit records -- exactly the size of
Stage 02's symbol table 2, an independent confirmation of the index space.

[ScriptMission+40] reads 1 in a phase-1 mission. The mirror at
[*(0x828F35F8)+236] that three earlier runs polled reads 0, because ChangePhase
only posts once the ordinal exceeds 1 -- so +40 is the real counter and is
reachable from /dev/shm with no debugger.

Flagged rather than asserted: the three objective squadrons read state=1 with a
LIVE object pointer in a mission where nothing has been shot, which does not fit
the built-in table's '1 = gone'. Reading state != 2 as destroyed would be a
plausible-but-wrong inference; the encoding needs pinning first.

New tool tools/re-capture/squadron_state.py, verified end to end against the
manual reading.
2026-08-25 14:25:46 +00:00

3.5 KiB

Reading the live script state — the real phase counter, and per-squadron liveness

Status: ScriptMission and ScriptPhase located in a running mission with no debugger, validated arithmetically; the true phase ordinal read live; 🟡 the per-unit state encoding needs care.

This unblocks mission-phase-membership, which was stuck because "38 enemies died" could not say whether the right ones did. Chasing craft→squadron was the wrong angle: the script VM already keeps that table, indexed by the .ssb symbol-table-2 index.

Tool: tools/re-capture/squadron_state.py.

Locating the objects, without gdb

  1. Find the .ssb header in guest memory — 20 bytes of version + code offset
    • the two symbol-table offsets, distinctive enough to hit once. For a Stage 02 run it sat at 0xAB840010.
  2. code base = file base + header code offset (0x24) → 0xAB840034.
  3. [ScriptMission+24] is that code base, so scan for a word equal to it.
  4. Validate arithmetically, not by eye: [ScriptMission+44] must equal file base + symtab1 offset + 4. Measured 0xAB874C94; predicted 0xAB840010 + 0x34C80 + 4 = 0xAB874C94. Exact.

That check is what makes this trustworthy — the candidate is confirmed against a number taken from the file on disc, not against "it looks like a pointer". A second candidate that also pointed at the code base failed it and was discarded.

ScriptMission 0xBC7A2A20
  +4  ScriptPhase*  = 0xBE14DD80      +20 state     = 1  ("phase running")
  +24 code base     = 0xAB840034      +28 pc        = 0xAB84007C
  +40 PHASE ORDINAL = 1               +44 symtab1   = 0xAB874C94  ✓
ScriptPhase 0xBE14DD80
  +196 finished = 0    +244 symtab1 = 0xAB874C94    +324 unit array = 0xBC43B560

ScriptPhase+324+4 → an array of per-unit records. It holds 122 records — exactly the size of Stage 02's symbol table 2, which is an independent confirmation that the index space is the one the bytecode uses.

The real phase counter reads 1 — the mirror was the wrong field

[ScriptMission+40] reads 1 in a phase-1 mission. The runtime mirror at [*(0x828F35F8)+236], which three earlier runs polled, reads 0 — because ChangePhase is only posted once the ordinal exceeds 1.

So the mirror is not a phase readout at all in phase 1, and +40 is. It is reachable from /dev/shm with no debugger, which is what made three runs of polling the wrong address avoidable in hindsight.

🟡 The per-unit state encoding is not what the summary implies

For the three phase-1 objective squadrons, early in a fresh mission:

ADN110  idx=1  obj=True  state=1
ADN111  idx=2  obj=True  state=1
ADN112  idx=5  obj=True  state=1
records with state==2 (active): 27-29 of 122

The built-in table describes +16 as "2 = active; 1/3/4 = gone/dead/invalid". But these three have a live object pointer and state 1, in a mission that has barely started and where nothing has been shot. So either state 1 does not mean "gone", or it means "not yet deployed" — not settled, and worth pinning before any conclusion is drawn from it. Reading state != 2 as "destroyed" would be exactly the kind of plausible-but-wrong inference this corpus keeps catching.

What this makes possible

The decisive phase experiment is no longer blocked on attribution: sample [ScriptMission+40] and the three squadrons' records together over a run, and a phase advance becomes directly observable along with the state change that caused it. That run has not been done yet.