sub_822F9498 (the unit-definition loader, called only from sub_821A6CF0) is the only function referencing the AA_/AV_ name strings. Mapping each name to the stfs that follows gives an exact interleave: AV_ at X, AA_ at X+8, for all five axes -- PitchPlus 196/204, PitchMinus 212/220, Yaw 228/236, Roll 244/252, AxisMode 320/328. A selector is an offset of 0 or 8, not two lookups. The 20 strings exist twice in the image; the first block is referenced by nothing. Data control: the ten suffixes match exactly between families. The selection stays a reading: functions loading two or more of 196/204/320/328 number 39 image-wide, and only two are call-graph-reachable from the loader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
174 lines
8.2 KiB
Markdown
174 lines
8.2 KiB
Markdown
# ✅ 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 still not found
|
||
|
||
Looking for the consumer by offset does not work here: functions loading two or
|
||
more of `+196`/`+204`/`+320`/`+328` number **39** across the image, and those
|
||
offsets are far too common to identify anything — the same trap the corpus has
|
||
paid for before. Only two of the 39 are call-graph-reachable from the loader's
|
||
owner (`sub_821A6CF0` itself, and `sub_821AB650`); that is the next handle, and
|
||
it was not followed this iteration. **So "anti-air vs anti-vessel" remains a
|
||
reading.**
|
||
|
||
## `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.
|