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.
This commit is contained in:
76
docs/re/script-runtime-probe.md
Normal file
76
docs/re/script-runtime-probe.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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.
|
||||
|
||||
## 🟡 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.
|
||||
Reference in New Issue
Block a user