# Live unit definitions from a running Stage 02 (2026-08-12) The Route-B question — what value does a field that the disc leaves **defaulted** actually take at runtime — needs the game running. This is the first snapshot of the parsed definition objects taken straight out of guest RAM, plus the recipe that works in the box today, because getting there was most of the work. ## The recipe that works Everything must happen inside **one blocking foreground call**. A process that outlives the call that started it is reaped — `nohup`, a harness background job and a `wait`-holding wrapper were all tried and all died within a minute of the turn ending (see the session-lifetime notes). ```bash tools/re-capture/launch_mission.sh fly # title → LOAD → slot 01 → READY ROOM → TAKE OFF python3 tools/re-capture/gmem.py find hex:820af844 400 # live definition objects python3 tools/re-capture/gmem.py words 96 # dump each one ``` Two environment facts that cost an hour each: - **Audio must be `--audio --apu=sdl` with `SDL_AUDIODRIVER=dummy`.** There is no PulseAudio server in the box, so muting (`--apu=nop`, `run-canary`'s default) looks like the safe choice. It is not: the log then fills with `AudioSystem::RegisterClient: CreateDriver failed for index=0`, the guest never gets past the movie, and the window stays black for 8+ minutes. `nav_probe.sh` and `launch_mission.sh` already pass the right pair — do not "fix" them. - **Boot is fast once the caches are warm.** The first boot took minutes; with the shader/code cache populated, `launch_mission.sh` reached the flight HUD in **25 s**, which is what makes a launch-and-dump fit in a single call. `entities2.py`, `flight_probe.py` and the other analysis scripts **do not run here** — no `numpy` (and no `PIL` for image work). `gmem.py` is pure stdlib and works, so the pattern is: dump raw words to disc inside the call, analyse afterwards. ## What came out 14 live definition objects (vtable `0x820af844`), 96 words each: [`captures/stage02-live-unit-definitions.txt`](captures/stage02-live-unit-definitions.txt), with the flight screenshot they were taken from. | object | `+0x30` | `+0x34` | `+0x38` | `+0x54` | |---|---|---|---|---| | `0xbd3b6a00` | 2500 | 3100 | 2600 | 10000 | | `0xbd3d2d80` | 80 | 80 | 350 | 4000 | | `0xbd3da100` | 10 | 7 | 29 | 1500 | | `0xbd3db980` | 350 | 70 | 200 | 10000 | | `0xbd3dbd00` | 10 | 5 | 20 | 300 | | `0xbd3dea80` | 20 | 9 | 22 | 100 | | `0xbd3dfc00` | 10 | 7 | 29 | 1000 | | `0xbd3e0680` | 300 | 300 | 2800 | 3000 | | `0xbd3e1f00` | 400 | 400 | 1400 | 25000 | | `0xbd3e6f80` | 300 | 300 | 2100 | 10000 | | `0xbd3ee300` | 200 | 200 | 2000 | 10000 | | `0xbd3faa80` | 100 | 40 | 50 | 500 | | `0xbd3fd800` | 600 | 600 | 3800 | 30000 | | `0xbd40e200` | 700 | 700 | 3800 | 30000 | This **corroborates** the layout solved earlier from a single in-mission snapshot (`+0x054` = HP, `+0x30…0x38` = `Size_X/Y/Z`) rather than re-proving it: the `+0x54` column reads as hull values across three orders of magnitude (100 for something small, 30 000 twice for capital ships), and the sizes scale with them. It also gives independent support to the **`Size_Y` inherits `Size_X`** default rule — five of the fourteen have `+0x30 == +0x34` exactly (300/300, 400/400, 200/200, 600/600, 700/700) while the rest differ. ## Identity without a name: match on the values Nothing in the first 96 words is a name pointer, so identity comes from the numbers themselves. `examples/unit_signatures.rs` prints `(HP, Size_X, Size_Y, Size_Z)` for every craft unit and vessel on the disc; a live object is then the record whose *known* fields it reproduces exactly. Seven of the fourteen match a disc record on all four values — the player's Delta Saber (`10, 7, 29`, HP 1500 in its player form and 1000 otherwise), the ArrowHead, an ADAN turret, an Attacker. **The other seven match nothing, and that is the point:** their disc records leave a field defaulted, so there is nothing to match against. **18 of the 23 vessel records on disc are missing at least one of these four fields**, nearly always `Size_Y`. ## ✅ Route B: 13 defaulted fields read out of the running game Matching each disc record that defaults exactly one field against the live object that reproduces its remaining fields resolves the missing value: | unit | field | runtime value | live object (sx, sy, sz, hp) | |---|---|---|---| | `UN_e201_ADAN_ISCMissile` | `Size_Y` | **300** | `0xbd3e0680` (300, 300, 2800, 3000) | | `UN_f106_TCAF_Destroyer` | `Size_Y` | **200** | `0xbd3ee300` (200, 200, 2000, 10000) | | `UN_f106_TCAF_Destroyer_Inv` | `Size_Y` | **200** | `0xbd3ee300` | | `UN_e105_ADAN_Cruiser` | `Size_Y` | **600** | `0xbd3fd800` (600, 600, 3800, 30000) | | `UN_e105_ADAN_CruiserEX` | `Size_Y` | **600** | `0xbd3fd800` | | `UN_f105_TCAF_Cruiser` | `Size_Y` | **700** | `0xbd40e200` (700, 700, 3800, 30000) | | `UN_f105_TCAF_Cruiser_EX5` | `Size_Y` | **700** | `0xbd40e200` | | `UN_f105_TCAF_Cruiser_Inv` | `Size_Y` | **700** | `0xbd40e200` | | `UN_e108_ADAN_ASFrigate` | `Size_Y` | **80** | `0xbd3d2d80` (80, 80, 350, 4000) | | `UN_e108_ADAN_ASFrigateEX` | `Size_Y` | **80** | `0xbd3d2d80` | | `UN_e106_ADAN_Destroyer` | `Size_Y` | **300** | `0xbd3e6f80` (300, 300, 2100, 10000) | | `UN_e106_ADAN_DestroyerEX` | `Size_Y` | **300** | `0xbd3e6f80` | | `UN_f101_TCAF_Acropolis` | `Size_Y` | **400** | `0xbd3e1f00` (400, 400, 1400, 25000) | **Every resolved value equals that unit's `Size_X`** — so the running game confirms the `Size_Y` inherits `Size_X` rule that was derived statically from sibling records, this time from the engine's own parsed definitions. Two honest limits. Variants share a live object (`_Inv`, `_EX5`, `EX` map to the same definition as their base), so the snapshot cannot distinguish them — the value is right for the class, and per-variant differences in *other* fields are not addressed. And this run only reaches the units Stage 02 instantiates: the `…Ratio` / `…Count` family (`HQRatio`, `PowerRatio`, `ShieldRatio`, `HatchCount`, `NodeCount`, `SequencingCount`, …) is defaulted on 5–23 records each and is **not** resolved here, because those fields' offsets in the live object are not yet known. Finding them is the next run: dump deeper than 96 words and look for the constants the disc *does* set on the records that set them. ## Locating fields inside the live object The next step — reading the `…Ratio` / `…Count` family — needs to know *where* each field sits in the live object. The method is to anchor on a unit whose disc record **sets** a field and look for that value in its dumped words. Anchoring on `UN_f106_TCAF_Destroyer` (disc sets `Size_Radius 0.1`, `ThrusterRatio 0.8`, `ResistanceParalyze 0.97`) and then checking the same offsets across five capital ships and the player's fighter: | offset | reading | `f106` | `f105` | `e105` | `e106` | `f101` | Delta Saber | |---|---|---|---|---|---|---|---| | `+0x30` | `Size_X` ✅ | 200 | 700 | 600 | 300 | 400 | 10 | | `+0x34` | `Size_Y` ✅ | 200 | 700 | 600 | 300 | 400 | 7 | | `+0x38` | `Size_Z` ✅ | 2000 | 3800 | 3800 | 2100 | 1400 | 29 | | `+0x54` | `HP` ✅ | 10000 | 30000 | 30000 | 10000 | 25000 | 1500 | | `+0x74` | 🟡 `ThrusterRatio` | 0.8 | 0.8 | 0.8 | 0.8 | 0.8 | 1 | | `+0x84` | 🟡 `ResistanceParalyze` | 0.97 | 0.97 | 0.97 | 0.97 | 0.97 | 0.97 | | `+0x40` | ❔ | 0.1 | 0.1 | 1 | 1 | 1 | 0.1 | `+0x74` and `+0x84` are marked 🟡 because they rest on one anchoring unit: the value is right for the Destroyer and constant across the others, which is consistent with a field the rest default, but a second anchoring unit that sets them to something *different* is what would prove it. `+0x40` is 0.1 on two units and 1 on three, so it is a real per-unit field — just not identified. ⚠️ **The automated version of this does not work yet.** `examples/live_offsets.rs` correlates every `get_f32(key)` against every dumped word and keeps offsets all units agree on; over five units it produced exactly **one** hit (`YawDragFactor → +0x0c`), and that hit is **wrong** — `+0x0c` holds the integer 2 (as a denormal float, `2.8e-45`), which collided with `YawDragFactor = 2.0`. Two causes, both fixable: the IDXD pool's value-before-key interleaving makes `get_f32` unreliable on records that default many fields (the pairing shifts), so most keys yield nothing; and 96 words only reaches `+0x180`, while `RadarRange 60000` / `FCSRange 45000` — both set on the Destroyer — appear nowhere in that window, so much of the record lies beyond it. So the next run dumps **512 words** per object, and the disc side needs a reader that walks the pool with the default-aware rule rather than `get_f32` per key.