Files
Sylpheed/docs/re/structures/unit-datasheet-static.md
Claude (auto) 909408f00d re: the object is PlayerParams, and sub_822F9498 is ITS loader -- not the unit loader
Resolving every string sub_822F9498 references, in code order, gives 90, and they
are exactly the 890e1be4 object's schema in its record order: Misc (5 effect
names), SpecialAttack (+17), TacticalManeuver (+7), SpecialWeapon (+8), Booster
(+50).  It never names Generic, Maneuver, Explosion, Shield or StructureCount, and
the string Maneuver has 0 xrefs.  One call site, from sub_821A6CF0, which is
itself called once and references the literal PlayerParams.

CORRECTION to unit-datasheet-static.md, which calls sub_822F9498 the
unit-definition loader.  It loads one object: the player parameter table.  Its
AA_/AV_ interleave still stands as a struct layout, but the struct is
PlayerParams's Booster record, not each unit's Maneuver.  What loads the 114 unit
Maneuver records is now an open question -- it uses none of these strings.

Ranking all 114 units by how many of Booster's 50 values they reproduce: the top
five are exactly the five _Player units, all at 39/50; the sixth drops to 13/50;
none matches 50/50.  The five players agree with each other on all 50 -- the three
player ships share one flight model.  Booster stands alone on 10 fields; the
eleventh, AA_Yaw_Max, is 65.0 vs 65, formatting not a value (last iteration's
"11 differ" over-counted).

Still open, and sharper: nothing selects Booster.  PlayerParams is loaded once,
unconditionally, no branch -- yet the measured flight tracks the unit Maneuver
(1.25/1.20/1.28) and misses Booster badly.  Both cannot govern the same craft; the
next test is a runtime watch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 20:48:56 +00:00

11 KiB
Raw Blame History

The STATIC unit datasheet and AI flight model — Generic, Maneuver, Effect

Found the same way as the weapon tables: IDXD field names are literal strings in the pool, so a field-shape search over all 190 782 records finds the carriers without guessing a filename. This is the static source for what the corpus had only measured at runtime — flight-speed-law, flight-controls-runtime, unit-struct-runtime.

CORRECTION (2026-08-27) — Generic is a per-FILE header, not a table

This page first read "Generic (394 per pak) is the unit datasheet". Wrong: only 114 of the 394 are. Generic is the record name every IDXD file gives its own header record — exactly one per pak entry — and its schema is set by what kind of file it is. Partitioned by field set, in each of the six GP_MAIN_GAME_*.pak identically:

Generic shape per pak what the file is
has HP 114 a unit datasheet (Maneuver, Effect, Shield, Mass, SE, Explosion, Turret_00N siblings)
{Count} only 204 a dialogue fileCount is the number of Message_NNN siblings
{ID, Name, SideID, Unique} 64 a character (Faces sibling) — 36 TCAF + 28 ADAN
{EnumAsteroidGroup} 10 an asteroid-group file
0 and 1 field 2 one empty, one naming eff_n0071

204 + 114 + 64 + 10 + 2 = 394, and 178 distinct Generic.ID = 114 unit + 64 character — the only two shapes that carry an ID. Positive control in the same loop: for the 204 dialogue headers, Count equals the number of Message_NNN siblings 204 / 204, no mismatches.

🔑 The 114 is not a coincidence: Maneuver = 114, Effect = 114, the entry sets are identical, every unit Generic has a Maneuver sibling, and Generic.Type splits 43 Craft + 71 Vessel — reproducing the count unit-struct-runtime reached from the other side. Artefact ../data/generic-record-partition.txt, tool tools/re-capture/generic_partition.py.

record per pak field names distinct IDs
Generic (unit shape) 114 67 max on one record 114
Generic (all shapes) 394 110 pooled 178
Maneuver 114 101
Effect 114 9

Repeated identically in every per-language GP_MAIN_GAME_*.pak. A unit's Generic, Maneuver and Effect share a pak entry — one entry is one unit's .tbl, which is why the runtime flattens all of them into a single object.

⚠️ Generic is the game's own record name, not a placeholder — it is also one of the 65 section names in data/config-keys.txt.

Generic — the unit datasheet

Sample UN_e006_ADAN_Vindicator_MargrasF:

ID / Name / Type Craft / UncertainName Craft
HP 6400.0        ShieldRatio 1.0      DefencePoint 0.0004
AttackCraftPoint 1.0                  AttackVesselPoint 0.001
ResistanceToShell 0.1   ResistanceToExplosion 0.5   ResistanceToOptics 0.8
ResistanceToPlayer 1.0  ResistanceParalyze 0.99
Size_X/Y/Z 12.0 / 5.0 / 30.0          Size_Radius 15.0
RadarRange 10000.0      FCSRange 8500.0
MountedFCS No           MountedShieldGenerator Yes
ScorePoint 4000   DamageScore 640   MassScore 30
HQRatio 1.0       ThrusterRatio 1.0   IsDestructible Yes
Model rou_e006    CollisionModel      Color_R/G/B 1.0 / 0.5 / 0.1
NozzleCount 3 + per-nozzle Jet / AfterBurner / ReverseThruster / Contrail
                 Fx model & frame names, NozzleSpec_00N

🔑 Model rou_e006 ties the datasheet straight to the mesh names the corpus already decodes, and Size_* / RadarRange / FCSRange are in the corpus's 1 metre world unit — a 10 km radar, an 8.5 km fire-control range, a 12 × 5 × 30 m craft.

Maneuver — the AI flight model

The single richest find for the port. Sample, same unit:

MaximumVelocity 1200   MinimumVelocity 100.0   CruisingVelocity 700
Acceleration 600       Deceleration 400
SideThrustAcceleration 1000.0            SideThrustVelocity_Max 1000.0
Turn_AngularVelocity 180.0               MaximumBank_Normal 60.0
YawDragFactor / RollDragFactor / PitchDragFactor 3.0
DragFactorThreshold 0.5
ArterBurner_Vc 2.5  ArterBurner_Acc 2.0      (the game's own spelling)
ReverseThrust_Vc -0.5   ReverseThrust_Acc 2.0
UsingChaffRatio 0.5     HomingResistAdjustment 0.8

and a set of named manoeuvres, each a Yes/No switch plus its own timing, ratio and length bounds:

TurnAttack   + _CutoffRatio _DoubleRatio _DoubleTimeMin/Max
TurnAway     + _Time_Minimum/Maximum
BarrelRoll   + _CountMinimum/Maximum _Radius _Time
SideRoll     + _Time _Length
Slalom       + _CutoffRatio _TurnCount_Min/Max
Through      + _Angle/Length Minimum/Maximum _Time1/2 Min/Max _CutoffRatio
HoldPosition + _MinimumTime _MaximumTime _CancelTime _CutoffRatio
               _BackRatio _SideRatio _LengthMin/Max
SolidCutoff  + _LengthMin/Max _Ratio
BoostAway    + _Time_Minimum/Maximum
CutoffTimeMin/Max
AA_* and AV_* Pitch/Yaw/Roll Min/Max pairs

🟡 The AA_ / AV_ prefixes pair with AttackCraftPoint / AttackVesselPoint, so anti-air vs anti-vessel rate limits is the obvious reading — still not adopted. See below for what the executable does say.

The two families are one interleaved block (2026-08-27)

🔴 Corrected 2026-08-27 — sub_822F9498 is not the unit-definition loader. Every one of the 90 strings it references is a field or record name of the PlayerParams object (Misc / SpecialAttack / TacticalManeuver / SpecialWeapon / Booster); it never names Generic, Maneuver or StructureCount, and Maneuver has 0 xrefs. It is called once, from sub_821A6CF0, which references the literal PlayerParams. The interleave below is real — but it is the layout of PlayerParams's Booster record, not of each unit's Maneuver. See player-tuning-tables.md. What loads the 114 unit Maneuver records is now an open question.

Exactly one function in the image references the AA_/AV_ name strings: sub_822F9498, the unit-definition loader, itself called only from sub_821A6CF0. Mapping each addi rN, r29, -offset (the name) to the stfs that follows gives the definition-object layout, and it is a clean interleave — AV_ at X, AA_ at X+8, for all five axes:

axis AV_Max / AV_Min AA_Max / AA_Min
PitchPlus +196 / +200 +204 / +208
PitchMinus +212 / +216 +220 / +224
Yaw +228 / +232 +236 / +240
Roll +244 / +248 +252 / +256
AxisMode +320 / +324 +328 / +332

The constant +8 displacement is the structural point: whatever selects between the families does it as an offset of 0 or 8, not as two separate lookups. The loader reads 78 named float fields in total.

Data side: the ten suffixes match exactly between the two families (checked as a list equality, not a count), with 690 records each for the eight rate fields and 36 each for AxisMode. Where the values differ, AA_ is consistently the largerAA_AxisMode_Min 100 vs AV_ 35, AA_Yaw_Max 50 vs AV_ 25, AA_PitchPlus_Min 100 vs AV_ 200 being the exception that keeps this a description, not a rule.

⚠️ The 20 strings exist twice in the image, at 0x82085B98… and 0x8209F674…. The loader uses the second block; the first 20 are referenced by nothing.

🔴 The selection is BLOCKED for static RE — four routes, all with controls

route result
1. string xref on the field names only sub_822F9498 references them — this gave the layout above, and nothing else in the image touches an AA_/AV_ string
2. unbounded offset intersection (+196/+204/+320/+328) 🔴 39 functions image-wide; pure coincidence
3. call-graph bound — the loader's owner sub_821A6CF0, and sub_821AB650 🔴 both read 196/200/204 off three different base registers in three consecutive instructions: three unrelated objects contributing one float each, not the block
4. data-flow bound — the definition object lives in the global at [0x828F358C] (lwz r3, 13708(r26) feeds the loader); 18 functions touch it, 9 also touch block offsets 🔴 the two best are both refuted, below

Why route 4's best candidates fail, and it is instructive:

  • sub_8230D1F8 is a different loader entirely — a scoring table. Resolving its field names the same way gives +256 CraftScore_Adjustment, +320 FFPenalty_Zessel_Maximum, +324 RankScore_S, +328 RankScore_A, +332 RankScore_B. It stores at the very offsets the definition object uses for AA_Roll_Min and the four AxisMode fields.
  • sub_82398CC0 uses r19 as a float-constant pool (lfs f0, 320(r19) alongside 272(r19) and -656(r19)), not as a definition pointer.

So the offset region is shared by at least two unrelated objects and by a constant pool. That is why every offset-based discriminator has failed here: the contamination is structural, not bad luck.

🔴 Marking this blocked for static RE. The selection would have to be caught at runtime — a watch on the definition object's +196+332 during flight — which is emulator work, not a disc or image read. "Anti-air vs anti-vessel" stays a reading, as unit-datasheet-static.md has always had it; what is new is the layout, and that the selector must be a +0/+8 offset.

Side finding, unowned by the corpus: sub_8230D1F8 is the rank/score table loaderRankScore_S/_A/_B, FFPenalty_Zessel_Maximum, CraftScore_Adjustment. Nothing in docs/re/ mentions those names. Not pursued here.

Effect

ShieldHitEffectName, ShieldRecoverEffectName, Effect_Paralyze, plus JumpIn / JumpOut / FadeIn / FadeOut slots.

🟡 Not settled

  • No cross-check against the runtime captures — this locates and reads the tables; it does not reconcile them field by field.
  • HP_CLASS / HP_ID turned out not to be ship stats: they sit on records named for wingmen and ship classes (CARL, ELLEN, SANDRA, ACROPOLIS, Carrier_TCAF1, NP_Facility, …), 120144 instances each, alongside HPGauge / HPGaugeSub / RadarCursorType on the ObjectiveMarker_* and TutorialMarker_* records — a HUD gauge binding table, not a datasheet.
  • 178 distinct Generic IDs against 394 records per pak — the surplus is unexplained here. Settled 2026-08-27 — see the correction at the top.
  • The Generic field-name count of 110 is a POOL across all four shapes; no single record carries more than 67.