Files
Syplheed-Reborn/docs/re/live-unit-definitions.md
Claude (auto-RE) 3322202732 re(route-b): 13 defaulted Size_Y values read out of the running game
Live definition objects carry no name, so identity comes from the values:
unit_signatures.rs prints (HP, Size_X/Y/Z) for every disc unit and vessel, and a
live object is the record whose known fields it reproduces. 7 of 14 match a disc
record outright; the other 7 match nothing because their records default a field
-- 18 of 23 vessel records are missing at least one, nearly always Size_Y.

Matching each single-default record against the live object that reproduces its
remaining fields resolves 13 values (Destroyer 200, Cruiser 600/700, ASFrigate
80, ADAN Destroyer 300, Acropolis 400, ISCMissile 300, plus _Inv/_EX variants).
Every one equals that unit's Size_X, so the engine's own parsed definitions
confirm the statically-derived inheritance rule.

Limits stated: variants share one live object, and the ...Ratio/...Count family
is not resolved -- their offsets in the live object are still unknown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 20:28:55 +00:00

117 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <va> 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 523 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.