# ✅ 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-speed-law.md), [flight-controls-runtime](../flight-controls-runtime.md), [unit-struct-runtime](unit-struct-runtime.md). > ## ❌ 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 file** — `Count` 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](unit-struct-runtime.md) reached from the other side. > Artefact `../data/generic-record-partition.txt`, tool > `tools/re-capture/generic_partition.py`. | record | per pak | field names | distinct `ID`s | |---|---|---|---| | `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) 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 larger** — `AA_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 loader** — `RankScore_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`, …), 120–144 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.