Files
Syplheed-Reborn/docs/re/live-unit-definitions.md
Claude (auto-RE) 892f1e3979 re: partial field map inside the live definition object, and why the automation failed
Anchoring on a unit whose disc record sets a field locates it in the live object:
Size_X/Y/Z (+0x30/34/38) and HP (+0x54) confirmed across five capital ships and
the player fighter; +0x74 (0.8) and +0x84 (0.97) are probable ThrusterRatio and
ResistanceParalyze but rest on a single anchoring unit; +0x40 varies per unit and
is unidentified.

live_offsets.rs automates the correlation and currently produces one hit which is
false -- YawDragFactor 2.0 collided with the integer 2 at +0x0c stored as a
denormal. Causes recorded: get_f32 is unreliable on default-heavy IDXD records
because the value-before-key pairing shifts, and 96 words only reaches +0x180
while RadarRange/FCSRange sit beyond it. Next run dumps 512 words.

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

8.6 KiB
Raw Blame History

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).

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, 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.

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.