Watching [ScriptMission+40] and the three phase-1 objective squadrons together: all of ADN110/111/112 flip state 1 -> 2 at ~143s, while records in state 2 climb 24 -> 35 over four minutes. So state 1 means 'not yet deployed' for these, not 'gone'. The built-in table's '1/3/4 = gone/dead/invalid' shorthand is incomplete, and reading state != 2 as destroyed would have been wrong exactly as flagged last iteration. This also answers a much older question: mission-arrival-watch.md and the wave work recorded '0 confirmed arrivals' across many runs by watching the CRAFT population. The script's own unit table shows arrivals plainly -- eleven records enter state 2 within four minutes. The old negative measured the wrong structure; craft counts conflate deployment with attrition, the per-unit state field does not. Both attempts froze (at ~70s and ~253s), so no phase advance was reached. The freeze witness caught both immediately, which is why the truncation is visible instead of a silently flat line. New harness tools/re-capture/phase_watch.py.
5.7 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
- Find the
.ssbheader 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.
- the two symbol-table offsets, distinctive enough to hit once. For a Stage 02
run it sat at
code base = file base + header code offset(0x24) →0xAB840034.[ScriptMission+24]is that code base, so scan for a word equal to it.- Validate arithmetically, not by eye:
[ScriptMission+44]must equalfile base + symtab1 offset + 4. Measured0xAB874C94; predicted0xAB840010 + 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.
✅ PINNED: state 1 = not yet deployed, state 2 = active — and arrivals are real
Watching [ScriptMission+40] and the three objective squadrons together across a
live run (data/phase-watch-s02.txt):
[ 1.8s] phase=1 finished=0 active= 24 ADN110:1 ADN111:1 ADN112:1
[ 58.2s] phase=1 finished=0 active= 27 ADN110:1 ADN111:1 ADN112:1
[ 90.7s] phase=1 finished=0 active= 30 ADN110:1 ADN111:1 ADN112:1
[ 120.8s] phase=1 finished=0 active= 29 ADN110:1 ADN111:1 ADN112:1
[ 143.4s] phase=1 finished=0 active= 32 ADN110:2 ADN111:2 ADN112:2 <-- arrive
[ 223.3s] phase=1 finished=0 active= 35 ADN110:2 ADN111:2 ADN112:2
All three flip 1 → 2 at ~143 s, and the count of records in state 2 climbs
24 → 35 over the same window. So for these squadrons state 1 is
"not yet deployed", not "gone" — the built-in table's shorthand
"1/3/4 = gone/dead/invalid" is incomplete, and reading state != 2 as
"destroyed" would have been wrong in exactly the way flagged last iteration.
Good that it was flagged rather than assumed.
✅ This also answers a much older question: arrivals DO happen
mission-arrival-watch and the wave work recorded "0 confirmed arrivals" after many runs, measured by watching the craft population. The script's own unit table shows arrivals plainly: eleven more records enter state 2 within four minutes, three of them the phase-1 objective squadrons at a distinct moment.
The old negative was not wrong about what it measured — it was measuring the wrong structure. Craft counts conflate deployment with attrition; the per-unit state field does not.
⚠️ Both runs of this experiment froze — at ~70 s and ~253 s — so the window above is all that was observed, and no phase advance was reached. The freeze witness caught both immediately, which is the only reason the truncation is visible rather than silently producing a flat line.
🟡 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 — and it has
now partly run: the arrival of the three objective squadrons is directly
observed. What is still missing is a run that survives long enough (no freeze)
for them to be destroyed, which is when [ScriptMission+40] should step to
2. Two attempts froze first.
tools/re-capture/phase_watch.py is the harness: it samples the real counter and
the watched squadrons together, witnesses the freeze every 60 s, and prints only
on change.