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

208 lines
11 KiB
Markdown
Raw Permalink 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.
# ✅ 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)
> 🔴 **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`](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
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`, …), 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.