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.
120 lines
5.7 KiB
Markdown
120 lines
5.7 KiB
Markdown
# 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](mission-phase-membership.md), 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.
|
|
|
|
## ✅ 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](mission-arrival-watch.md) 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.
|