An Arsenal item does not reference a Weapon record. It references a Turret_NNN HARDPOINT SLOT on the player craft's own unit table, and the slot is what carries the WeaponID. Three hops: Arbalest_155KG.PlayerWeapon -> Turret_050 (a slot on UN_f001_TCAF_DeltaSaber_T_Player) -> .WeaponID -> Weapon_DSaber_P_wep_50_Cannon Controls, both in the same loop: 0/59 distinct PlayerWeapon values are a Weapon.ID; 59/59 are a Turret_NNN slot id; the full chain lands on a Weapon.ID 59/59. WingmanWeapon resolves identically. The WEAPONS roster's 59 = 55 item names + 4 empty-slot sentinels. Wingmen fly a cheaper gun: following the same 59 slots across craft variants, the _Player tables give each item its own weapon record (59 distinct) while the AI tables collapse all 59 onto 10 generic classes. That is most of the 131. Upgrades yesterday's 'hardpoint catalogue' reading from 21 to adopted, proved from an independent file, and corrects its '10 distinct WeaponID' figure -- that was the AI variant, not the player's. Also adds an __main__ guard to unit_substructures.py so importing pak_entries from it does not run its report; its artefact is unchanged and still byte-identical.
137 lines
6.4 KiB
Markdown
137 lines
6.4 KiB
Markdown
# ✅ The destructible-subsystem model — a unit's sub-records
|
||
|
||
The corpus has named the sub-records of a unit `.tbl` since
|
||
[unit-struct-runtime](unit-struct-runtime.md) ("`Generic`, `Maneuver`,
|
||
`Shield`, `Explosion`, `Mass`, `Effect`, `SE`, `Turret_00N` … all flattened
|
||
into that single object") but has never **opened** them. This does.
|
||
|
||
One unit = one IDXD pak entry. 114 of them
|
||
([the `Generic` partition](unit-datasheet-static.md)). Artefact
|
||
`../data/unit-substructures.txt`, regenerator
|
||
`tools/re-capture/unit_substructures.py`.
|
||
|
||
| family | records | max/unit | non-indexed fields |
|
||
|---|---:|---:|---:|
|
||
| `Turret_NNN` | **835** | 63 | 38 |
|
||
| `Generic` / `Maneuver` / `Effect` / `Shield` / `Mass` / `SE` / `Explosion` / `StructureCount` | 114 each | 1 | — |
|
||
| `NS_Body` | 68 | 1 | 12 |
|
||
| `ShieldGenerator_NNN` | 46 | 4 | 21 |
|
||
| `Thruster_NNN` | 38 | 4 | 21 |
|
||
| `Hatch_NNN` | 26 | 4 | 23 |
|
||
| `Bridge_NNN` | 25 | 2 | 20 |
|
||
|
||
Seven singletons named with a literal `???` (`Turret_???`, `Versatile_???`, …)
|
||
are the schema's own template rows.
|
||
|
||
## The shared destructible-part base
|
||
|
||
`Turret`, `ShieldGenerator`, `Thruster`, `Hatch` and `Bridge` are the **same
|
||
record shape** with per-kind extras. Every one of the 970 part records carries:
|
||
|
||
```
|
||
ID Name ParentStructureID Frame NomalModel CollisionModel Radius
|
||
HP IsDestructible IsShielded IsInvolved IsRadarVisible SpreadDamage
|
||
DamagedMotionName DestroyMotionName DestroyMotionTime
|
||
Effect_Explosion Effect_Flare Effect_Paralyze
|
||
```
|
||
|
||
(`NomalModel` is the game's own spelling.) `Frame` is a **mesh node name** —
|
||
`GN_mnt` on the player craft — so a part is bolted to a named bone of the
|
||
`Generic.Model` mesh the corpus already decodes.
|
||
|
||
Per-kind, on top of that base:
|
||
|
||
| kind | extra fields |
|
||
|---|---|
|
||
| `Turret` | `WeaponID`, `AngularVelocity`, `YawLimit`, `PitchLimit_Elevation`, `PitchLimit_Depression`, `CoverArea`, `IsAuto`, `HasBarrel`, `CommonCannonModel`/`Frame`, `CannonModel_NNN`/`CannonFrame_NNN` (up to 80), `SequencingCount`, `SequencingInterval`, and on a few: `CurveAngle`, `FCSAngle`, `FollowView`, `ShellModel`, `CoverModel` |
|
||
| `ShieldGenerator` / `Thruster` / `Bridge` | `PowerRatio` — losing the part degrades the parent |
|
||
| `Thruster` | `NozzleCount` |
|
||
| `Hatch` | `SquadronID`, `LoadedCount`, `MaxAvailableCount`, `TakeoffInterval` — **a carrier's launch bay** |
|
||
|
||
🔑 That is a complete component model: named bone, own mesh and collision mesh,
|
||
own HP and destruction motion + effects, and a `PowerRatio` saying what the
|
||
parent loses when it dies.
|
||
|
||
## The one-record blocks
|
||
|
||
| record | fields |
|
||
|---|---|
|
||
| `Shield` | `MaxValue`, `ChargeSpeed`, `ChargeDelay`, `ChargeDelay_Break` |
|
||
| `Mass` | `DryMass`, `GrossMass` |
|
||
| `SE` | `ExplosionSE`, `Thruster`, `SideThruster`, `JumpIn`, `JumpOut`, `ShipEnvironmentSE`, `LowerHPSE`, `LowerHPThresholdRatio` |
|
||
| `Explosion` | `Delay`, `DelayAdjustment`, `ExplosionFxModel`, `ExplosionMotionName`, `LowerHPFxModel`, `DestroyMotionName`, `DestroyMotionTime` |
|
||
| `StructureCount` | `TurretCount`, `BridgeCount`, `HatchCount`, `ShieldGeneratorCount`, `ThrusterCount`, `VersatileCount` |
|
||
| `NS_Body` | `Model`, `NodeCount`, `Interval`, `StartingVelocity`, `Deceleration`, `Volume`, `Color_R/G/B`, `AttenuationVolume`, `AttenuationAlpha`, `DeleteFadeSpeed` — an engine-trail ribbon, on 68 of 114 |
|
||
|
||
Every field the runtime doc reached for `Shield` (`MaxValue +0x238`,
|
||
`ChargeSpeed +0x244`) and `Mass` (`DryMass +0x274`, `GrossMass +0x278`) is
|
||
here, at the same names.
|
||
|
||
## 🧪 Control 1 — `StructureCount` counts the part records
|
||
|
||
`declared == #<Kind>_NNN records`, over 114 units × 6 kinds = 684:
|
||
|
||
| | |
|
||
|---|---:|
|
||
| equal | **612** |
|
||
| declared 0, one blank placeholder record | 55 |
|
||
| differs | **11** |
|
||
| kind absent from `StructureCount` | 6 |
|
||
|
||
The 55 are a real sub-rule, not slack: a kind with a count of **0 still emits
|
||
exactly one record**, and all 55 have empty `Name`, `NomalModel` *and* `Frame`.
|
||
So the file always carries at least one slot per kind.
|
||
|
||
Every one of the 11 exceptions is a **`Turret`**, and every one has
|
||
`declared < records` — never the other way round. Nine are the player's
|
||
DeltaSaber family:
|
||
|
||
```
|
||
declared=4 records=63 x7 f001_T, f001_T_EX5, f001_T_EX5_el, f001_T_Player,
|
||
f002_W, f002_W_Player, f004_A_Player
|
||
declared=4 records=5 x2 f001_T_Player_Ttrl1 / _Ttrl2
|
||
declared=1 records=2 x1 UN_e001_ADAN_Elan_EX4
|
||
declared=8 records=9 x1 UN_e107_ADAN_AAFrigate_EX4
|
||
```
|
||
|
||
✅ **ADOPTED the next day** — on the player craft the `Turret_NNN` list is a
|
||
*hardpoint catalogue* and `TurretCount 4` is how many mount at once. The
|
||
Arsenal table proves it from an independent file: **59/59 of its items name a
|
||
`Turret_NNN` slot in `PlayerWeapon`**, see
|
||
[arsenal-item-weapon-chain](arsenal-item-weapon-chain.md). The two `+1`
|
||
warship/craft cases are still unexplained.
|
||
|
||
⚠️ The "10 distinct `WeaponID`s" measured here is the **AI** variant
|
||
`UN_f001_TCAF_DeltaSaber_T`. The `_Player` variants give the same 59 slots
|
||
**59 distinct** weapon records — wingmen fly a collapsed 10-class set.
|
||
|
||
## 🧪 Control 2 — turret weapons resolve into the weapon datasheet
|
||
|
||
**835 / 835 `Turret_NNN.WeaponID` values are an `ID` in the 131-record
|
||
`Weapon` table. Zero unresolved.** 105 distinct values are used; **26 of the
|
||
131 weapons are never mounted on a turret** — the player-only arsenal.
|
||
|
||
That is a hard cross-table link: [the weapon
|
||
datasheet](weapon-datasheet-static.md) is what arms every gun on every ship.
|
||
|
||
## ❌ Refuted in the same pass — "the 59 hardpoints are the 59-name arsenal roster"
|
||
|
||
The DeltaSaber's 63 turret slots hold **59 non-`Weapon_NULL`** entries, and the
|
||
`WEAPONS` roster in `GP_HANGAR_ARSENAL.pak` has exactly **59** names. Tempting,
|
||
and wrong: the two sets **overlap in 0 values**, and the 59 slots carry only
|
||
**10 distinct** `WeaponID`s. The roster is display names
|
||
(`Machiene_Cannon_MG1` — the game's spelling); the hardpoints reference
|
||
`Weapon.ID` (`Weapon_TCAF_DeltaSaber_Gun`). Two namespaces, one coincidence of
|
||
count.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* What links a roster entry to a `Weapon.ID` — the 59-of-131 question is still
|
||
open, and this pass shows the answer is **not** the hardpoint list.
|
||
* `Versatile_NNN` has 0 populated records in this pak; only the `???` template
|
||
row exists, so its schema is known and its instances are not.
|
||
* `CoverArea` reads as a hex word (`0x00000000` / `0x00000001`) — a bit mask,
|
||
bits unknown.
|
||
* The 6 `no field` comparisons are one unit whose `StructureCount` omits every
|
||
count.
|