From 9281aa74567e60f985397f39c2edbdd2da059c1f Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Thu, 27 Aug 2026 12:39:50 +0000 Subject: [PATCH] re: the destructible-subsystem model -- a unit's sub-records The corpus has named these since unit-struct-runtime.md but never opened them. Per unit table: Turret_NNN 835 records (max 63 on one unit), ShieldGenerator_NNN 46, Thruster_NNN 38, Hatch_NNN 26, Bridge_NNN 25, plus one each of Shield/Mass/SE/Explosion/ StructureCount and NS_Body on 68 of 114. Turret/ShieldGenerator/Thruster/Hatch/Bridge are ONE record shape: a shared 19-field destructible-part base (ID, Name, ParentStructureID, Frame = a mesh NODE name, NomalModel, CollisionModel, Radius, HP, the four Is* flags, SpreadDamage, damaged/destroy motion + time, and the three Effect_*), with per-kind extras. Turrets add WeaponID, AngularVelocity, YawLimit, PitchLimit_Elevation/_Depression, CoverArea, IsAuto, HasBarrel and up to 80 CannonModel_NNN/CannonFrame_NNN. Shield generators, thrusters and bridges add PowerRatio. Hatches add SquadronID, LoadedCount, MaxAvailableCount, TakeoffInterval -- a carrier's launch bay. Control 1: StructureCount.Count == #_NNN records, over 684 comparisons -- 612 equal, 55 "0 declared, one blank placeholder" (55/55 blank in Name AND NomalModel AND Frame), 11 differ, 6 kind absent. All 11 exceptions are Turret and all are declared < records. Control 2: 835/835 Turret_NNN.WeaponID resolve to an ID in the 131-record Weapon datasheet, zero unresolved; 26 weapons are never mounted on a turret. Refuted in the same pass: "the DeltaSaber's 59 non-NULL hardpoints are the 59-name WEAPONS arsenal roster". The counts match exactly and the sets overlap in 0 values -- two namespaces, one coincidence. New structure doc, artefact and regenerator; other artefacts unchanged. --- docs/re/BACKLOG.md | 42 ++ docs/re/INDEX.md | 1 + docs/re/data/unit-substructures.txt | 402 ++++++++++++++++++ .../structures/unit-substructure-records.md | 131 ++++++ tools/re-capture/unit_substructures.py | 121 ++++++ 5 files changed, 697 insertions(+) create mode 100644 docs/re/data/unit-substructures.txt create mode 100644 docs/re/structures/unit-substructure-records.md create mode 100644 tools/re-capture/unit_substructures.py diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 393c592f..b79ac2a2 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -827,6 +827,48 @@ unknown, what evidence exists, and what the first step would be. Move an item in field; the stat keys in records not opened here (`MegaTons`, `BulletLimit`, `CrewCount`, `HP_CLASS`); why the roster lists 59 of 131. +* โœ… **(2026-08-27) THE DESTRUCTIBLE-SUBSYSTEM MODEL โ€” a unit's sub-records, + opened at last. 835 turrets, and every turret's weapon resolves.** + [structures/unit-substructure-records](structures/unit-substructure-records.md), + artefact `data/unit-substructures.txt`, regenerator + `tools/re-capture/unit_substructures.py`. + The corpus has *named* these since `unit-struct-runtime.md` but never opened + them. Per unit table: `Turret_NNN` **835** records (max 63 on one unit), + `ShieldGenerator_NNN` 46, `Thruster_NNN` 38, `Hatch_NNN` 26, `Bridge_NNN` 25, + plus one each of `Shield`/`Mass`/`SE`/`Explosion`/`StructureCount` and + `NS_Body` on 68 of 114. + ๐Ÿ”‘ **`Turret`/`ShieldGenerator`/`Thruster`/`Hatch`/`Bridge` are ONE record + shape** โ€” a shared 19-field destructible-part base (`ID`, `Name`, + `ParentStructureID`, **`Frame`** = a mesh NODE name, `NomalModel`, + `CollisionModel`, `Radius`, `HP`, `IsDestructible`, `IsShielded`, + `IsInvolved`, `IsRadarVisible`, `SpreadDamage`, damaged/destroy motion + + time, `Effect_Explosion`/`_Flare`/`_Paralyze`) with per-kind extras: + turrets add `WeaponID`, `AngularVelocity`, `YawLimit`, + `PitchLimit_Elevation`/`_Depression`, `CoverArea`, `IsAuto`, `HasBarrel` and + up to 80 `CannonModel_NNN`/`CannonFrame_NNN`; shield generators, thrusters + and bridges add **`PowerRatio`** (what the parent loses when the part dies); + hatches add `SquadronID`, `LoadedCount`, `MaxAvailableCount`, + `TakeoffInterval` โ€” **a carrier's launch bay**. + ๐Ÿงช **Control 1** โ€” `StructureCount.Count == #_NNN records`: + **612 equal / 55 "0 declared, one blank placeholder" / 11 differ / 6 kind + absent**, over 684 comparisons. The 55 are a real sub-rule (a zero count + still emits exactly one record, blank in `Name` *and* `NomalModel` *and* + `Frame`, 55/55). All 11 exceptions are `Turret` and all are + `declared < records`; nine are the player DeltaSaber family (4 declared, + 63 slots โ€” ๐ŸŸก reads as a hardpoint catalogue, **not adopted**). + ๐Ÿงช **Control 2** โ€” **835 / 835 `Turret_NNN.WeaponID` values resolve to an + `ID` in the 131-record `Weapon` datasheet, zero unresolved**; 105 distinct + used, **26 weapons never mounted on a turret**. A hard cross-table link. + โŒ **Refuted in the same pass**: "the DeltaSaber's 59 non-NULL hardpoints are + the 59-name `WEAPONS` arsenal roster". The counts match exactly and the sets + **overlap in 0 values** โ€” the roster is display names + (`Machiene_Cannon_MG1`), the hardpoints reference `Weapon.ID` + (`Weapon_TCAF_DeltaSaber_Gun`), and the 59 slots carry only **10 distinct** + weapon IDs. So the 59-of-131 question stays open, minus one wrong answer. + ๐ŸŸก Not settled: `CoverArea` is a hex bit mask with unknown bits; + `Versatile_NNN` has only its `???` template row in this pak; the two `+1` + turret cases (`Elan_EX4`, `AAFrigate_EX4`). + * โœ… **(2026-08-27) CORRECTION โ€” `Generic` is a per-FILE HEADER record, not a table. Only **114** of the 394 are unit datasheets.** [structures/unit-datasheet-static](structures/unit-datasheet-static.md), diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 03050244..33381d0d 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -99,6 +99,7 @@ files, which is how the same ground got covered twice. | [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` โ€” message id -> cue -> sound id -> `.slb` bank | โœ… CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files | | [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue โ€” speaker, portrait, on-screen seconds, audio cue per page | โœ… CONFIRMED, field count = 9ยทPageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways | | [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer โ€” two sequential countdowns, not a limit and a warning | โœ… CONFIRMED โ€” read from `sub_822639B8`; the threshold reading is | +| [`structures/unit-substructure-records.md`](structures/unit-substructure-records.md) | The destructible-subsystem model โ€” a unit's `Turret`/`Bridge`/`Hatch`/`ShieldGenerator`/`Thruster` sub-records | โœ… CONFIRMED โ€” 835 turrets, 835/835 `WeaponID` resolve, `StructureCount` control 667/684 | | [`structures/unit-datasheet-static.md`](structures/unit-datasheet-static.md) | The static unit datasheet and AI flight model โ€” `Generic`, `Maneuver`, `Effect` | โœ… CONFIRMED โ€” 394 Generic + 114 Maneuver + 114 Effect records read | | [`structures/weapon-datasheet-static.md`](structures/weapon-datasheet-static.md) | The static weapon datasheet โ€” `Weapon`, `Shell`, `AssortMissileParam` | โœ… CONFIRMED โ€” 131 Weapon + 131 Shell + 9 AssortMissileParam records read | | [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | โœ… CONFIRMED โ€” the advance is read from `sub_822710D0`, the unit from | diff --git a/docs/re/data/unit-substructures.txt b/docs/re/data/unit-substructures.txt new file mode 100644 index 00000000..5fe14378 --- /dev/null +++ b/docs/re/data/unit-substructures.txt @@ -0,0 +1,402 @@ +# Sub-records inside the 114 unit tables of GP_MAIN_GAME_E.pak +# Regenerate: python3 tools/re-capture/unit_substructures.py +# See docs/re/structures/unit-substructure-records.md + +## families (records, max per unit, distinct non-indexed field names) + Turret_NNN 835 max/unit 63 fields 38 + Maneuver 114 max/unit 1 fields 101 + Effect 114 max/unit 1 fields 9 + Shield 114 max/unit 1 fields 4 + Mass 114 max/unit 1 fields 2 + SE 114 max/unit 1 fields 8 + Generic 114 max/unit 1 fields 37 + Explosion 114 max/unit 1 fields 9 + StructureCount 114 max/unit 1 fields 6 + NS_Body 68 max/unit 1 fields 12 + ShieldGenerator_NNN 46 max/unit 4 fields 21 + Thruster_NNN 38 max/unit 4 fields 21 + Hatch_NNN 26 max/unit 4 fields 23 + Bridge_NNN 25 max/unit 2 fields 20 + NS_Body_2 1 max/unit 1 fields 12 + Hatch_??? 1 max/unit 1 fields 19 + ShieldGenerator_??? 1 max/unit 1 fields 16 + NS_* 1 max/unit 1 fields 10 + Bridge_??? 1 max/unit 1 fields 16 + Thruster_??? 1 max/unit 1 fields 19 + Turret_??? 1 max/unit 1 fields 25 + Versatile_??? 1 max/unit 1 fields 15 + +## Turret_NNN -- non-indexed fields (records carrying it, of 835) + AngularVelocity 835 + CoverArea 835 + Effect_Explosion 835 + Effect_Flare 835 + Frame 835 + HP 835 + ID 835 + IsAuto 835 + IsDestructible 835 + IsInvolved 835 + IsRadarVisible 835 + IsShielded 835 + Name 835 + NomalModel 835 + ParentStructureID 835 + PitchLimit_Depression 835 + PitchLimit_Elevation 835 + Radius 835 + SequencingCount 835 + SequencingInterval 835 + SpreadDamage 835 + WeaponID 835 + YawLimit 835 + CollisionModel 831 + CommonCannonFrame 831 + CommonCannonModel 831 + HasBarrel 831 + DamagedMotionName 829 + DestroyMotionName 829 + DestroyMotionTime 829 + Effect_Paralyze 824 + DisableCollision_Lost 240 + CurveAngle 120 + FCSAngle 120 + FollowView 40 + ShellModel 40 + CoverModel 26 + DamagedModel 6 + +## Effect -- non-indexed fields (records carrying it, of 114) + FadeIn 113 + FadeOut 113 + JumpIn 113 + JumpOut 113 + Shield 79 + Effect_Paralyze 74 + ShieldHitEffectName 35 + ShieldRecoverEffectName 35 + Thruster 1 + +## Shield -- non-indexed fields (records carrying it, of 114) + ChargeDelay 114 + ChargeDelay_Break 114 + ChargeSpeed 114 + MaxValue 114 + +## Mass -- non-indexed fields (records carrying it, of 114) + DryMass 114 + GrossMass 114 + +## SE -- non-indexed fields (records carrying it, of 114) + ExplosionSE 114 + Thruster 114 + JumpIn 113 + JumpOut 113 + LowerHPSE 113 + LowerHPThresholdRatio 113 + ShipEnvironmentSE 113 + SideThruster 113 + +## Explosion -- non-indexed fields (records carrying it, of 114) + Delay 114 + DelayAdjustment 114 + ExplosionFxModel 113 + LowerHPFxModel 113 + DestroyMotionTime 112 + DestroyMotionName 109 + ExplosionMotionName 109 + ExplosionFxMotion 3 + LowerHPFxMotion 3 + +## StructureCount -- non-indexed fields (records carrying it, of 114) + BridgeCount 114 + HatchCount 114 + ShieldGeneratorCount 114 + ThrusterCount 114 + TurretCount 114 + VersatileCount 114 + +## NS_Body -- non-indexed fields (records carrying it, of 68) + AttenuationAlpha 68 + AttenuationVolume 68 + Color_B 68 + Color_G 68 + Color_R 68 + Deceleration 68 + DeleteFadeSpeed 68 + Interval 68 + Model 68 + NodeCount 68 + StartingVelocity 68 + Volume 68 + +## ShieldGenerator_NNN -- non-indexed fields (records carrying it, of 46) + CollisionModel 46 + DamagedMotionName 46 + DestroyMotionName 46 + DestroyMotionTime 46 + Effect_Explosion 46 + Effect_Flare 46 + Effect_Paralyze 46 + Frame 46 + HP 46 + ID 46 + IsDestructible 46 + IsInvolved 46 + IsRadarVisible 46 + IsShielded 46 + Name 46 + NomalModel 46 + ParentStructureID 46 + PowerRatio 46 + Radius 46 + SpreadDamage 46 + DisableCollision_Lost 25 + +## Thruster_NNN -- non-indexed fields (records carrying it, of 38) + CollisionModel 38 + DamagedMotionName 38 + DestroyMotionName 38 + DestroyMotionTime 38 + Effect_Explosion 38 + Effect_Flare 38 + Effect_Paralyze 38 + Frame 38 + HP 38 + ID 38 + IsDestructible 38 + IsInvolved 38 + IsRadarVisible 38 + IsShielded 38 + Name 38 + NomalModel 38 + NozzleCount 38 + ParentStructureID 38 + PowerRatio 38 + Radius 38 + SpreadDamage 38 + +## Hatch_NNN -- non-indexed fields (records carrying it, of 26) + CollisionModel 26 + DamagedMotionName 26 + DestroyMotionName 26 + DestroyMotionTime 26 + Effect_Explosion 26 + Effect_Flare 26 + Effect_Paralyze 26 + Frame 26 + HP 26 + ID 26 + IsDestructible 26 + IsInvolved 26 + IsRadarVisible 26 + IsShielded 26 + LoadedCount 26 + MaxAvailableCount 26 + Name 26 + NomalModel 26 + ParentStructureID 26 + Radius 26 + SpreadDamage 26 + SquadronID 26 + TakeoffInterval 26 + +## Bridge_NNN -- non-indexed fields (records carrying it, of 25) + CollisionModel 25 + DamagedMotionName 25 + DestroyMotionName 25 + DestroyMotionTime 25 + Effect_Explosion 25 + Effect_Flare 25 + Effect_Paralyze 25 + Frame 25 + HP 25 + ID 25 + IsDestructible 25 + IsInvolved 25 + IsRadarVisible 25 + IsShielded 25 + Name 25 + NomalModel 25 + ParentStructureID 25 + PowerRatio 25 + Radius 25 + SpreadDamage 25 + +## NS_Body_2 -- non-indexed fields (records carrying it, of 1) + AttenuationAlpha 1 + AttenuationVolume 1 + Color_B 1 + Color_G 1 + Color_R 1 + Deceleration 1 + DeleteFadeSpeed 1 + Interval 1 + Model 1 + NodeCount 1 + StartingVelocity 1 + Volume 1 + +## Hatch_??? -- non-indexed fields (records carrying it, of 1) + DamagedModel 1 + Effect_Explosion 1 + Effect_Flare 1 + Frame 1 + HP 1 + ID 1 + IsDestructible 1 + IsInvolved 1 + IsRadarVisible 1 + IsShielded 1 + LoadedCount 1 + MaxAvailableCount 1 + Name 1 + NomalModel 1 + ParentStructureID 1 + Radius 1 + SpreadDamage 1 + SquadronID 1 + TakeoffInterval 1 + +## ShieldGenerator_??? -- non-indexed fields (records carrying it, of 1) + DamagedModel 1 + Effect_Explosion 1 + Effect_Flare 1 + Frame 1 + HP 1 + ID 1 + IsDestructible 1 + IsInvolved 1 + IsRadarVisible 1 + IsShielded 1 + Name 1 + NomalModel 1 + ParentStructureID 1 + PowerRatio 1 + Radius 1 + SpreadDamage 1 + +## NS_* -- non-indexed fields (records carrying it, of 1) + AttenuationAlpha 1 + AttenuationVolume 1 + Color_B 1 + Color_G 1 + Color_R 1 + Deceleration 1 + DeleteFadeSpeed 1 + Interval 1 + NodeCount 1 + StartingVelocity 1 + +## Bridge_??? -- non-indexed fields (records carrying it, of 1) + DamagedModel 1 + Effect_Explosion 1 + Effect_Flare 1 + Frame 1 + HP 1 + ID 1 + IsDestructible 1 + IsInvolved 1 + IsRadarVisible 1 + IsShielded 1 + Name 1 + NomalModel 1 + ParentStructureID 1 + PowerRatio 1 + Radius 1 + SpreadDamage 1 + +## Thruster_??? -- non-indexed fields (records carrying it, of 1) + DamagedModel 1 + Effect_Explosion 1 + Effect_Flare 1 + Frame 1 + HP 1 + ID 1 + IsDestructible 1 + IsInvolved 1 + IsRadarVisible 1 + IsShielded 1 + Name 1 + NomalModel 1 + NozzleCount 1 + NozzleFrame_??? 1 + NozzleSpec_??? 1 + ParentStructureID 1 + PowerRatio 1 + Radius 1 + SpreadDamage 1 + +## Turret_??? -- non-indexed fields (records carrying it, of 1) + AngularVelocity 1 + CannonFrame_??? 1 + DamagedModel 1 + Effect_Explosion 1 + Effect_Flare 1 + Frame 1 + HP 1 + ID 1 + IsAuto 1 + IsDestructible 1 + IsInvolved 1 + IsRadarVisible 1 + IsShielded 1 + MuzzleFrame_??? 1 + Name 1 + NomalModel 1 + ParentStructureID 1 + PitchLimit_Depression 1 + PitchLimit_Elevation 1 + Radius 1 + SequencingCount 1 + SequencingInterval 1 + SpreadDamage 1 + WeaponID 1 + YawLimit 1 + +## Versatile_??? -- non-indexed fields (records carrying it, of 1) + DamagedModel 1 + Effect_Explosion 1 + Effect_Flare 1 + Frame 1 + HP 1 + ID 1 + IsDestructible 1 + IsInvolved 1 + IsRadarVisible 1 + IsShielded 1 + Name 1 + NomalModel 1 + ParentStructureID 1 + Radius 1 + SpreadDamage 1 + +## CONTROL StructureCount.Count == #_NNN records +## (a 0 count still emits one blank slot) + 684 comparisons (114 units x 6 kinds) + equal 612 + 0 declared, 1 blank placeholder 55 + DIFFERS 11 + no field 6 + exceptions: + kind=Turret declared=4 records=63 x7 + UN_f001_TCAF_DeltaSaber_T + UN_f001_TCAF_DeltaSaber_T_EX5 + UN_f001_TCAF_DeltaSaber_T_EX5_el + UN_f001_TCAF_DeltaSaber_T_Player + UN_f002_TCAF_DeltaSaber_W + UN_f002_TCAF_DeltaSaber_W_Player + UN_f004_TCAF_DeltaSaber_A_Player + kind=Turret declared=4 records=5 x2 + UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 + UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 + kind=Turret declared=1 records=2 x1 + UN_e001_ADAN_Elan_EX4 + kind=Turret declared=8 records=9 x1 + UN_e107_ADAN_AAFrigate_EX4 + +## CONTROL Turret_NNN.WeaponID resolves into the Weapon datasheet + Weapon.ID values: 131 + turrets: 835, distinct WeaponID: 105 + resolve: 835 do NOT resolve: 0 + weapons never mounted on a turret: 26 diff --git a/docs/re/structures/unit-substructure-records.md b/docs/re/structures/unit-substructure-records.md new file mode 100644 index 00000000..e16584ac --- /dev/null +++ b/docs/re/structures/unit-substructure-records.md @@ -0,0 +1,131 @@ +# โœ… 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 == #_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 +``` + +๐ŸŸก The reading โ€” **not adopted**: on the player craft the `Turret_NNN` list is +a *hardpoint catalogue* and `TurretCount 4` is how many mount at once. It fits +(63 slots, all `Frame=GN_mnt`, all `IsAuto=No`, 10 distinct `WeaponID`s across +them, and the tutorial variants carry a cut-down 5) but nothing here shows the +selection. The two `+1` warship/craft cases are unexplained. + +## ๐Ÿงช 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. diff --git a/tools/re-capture/unit_substructures.py b/tools/re-capture/unit_substructures.py new file mode 100644 index 00000000..9a27661c --- /dev/null +++ b/tools/re-capture/unit_substructures.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Census the SUB-RECORDS inside the 114 unit tables, and check the counts. + +A unit `.tbl` is one IDXD pak entry. Besides `Generic`/`Maneuver`/`Effect` it +carries a `StructureCount` record and a variable number of destructible-part +records: `Turret_NNN`, `Bridge_NNN`, `Hatch_NNN`, `ShieldGenerator_NNN`, +`Thruster_NNN`, `Versatile_NNN`. Regenerates docs/re/data/unit-substructures.txt. +""" +import sys, os, glob, re, collections + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import unitgroup as U + +# pak_entries lives in the scratch decoder; fall back to a local copy of the recipe +import struct, zlib +def pak_entries(path): + idx = open(path, 'rb').read() + n = struct.unpack_from('>I', idx, 4)[0] + parts = sorted(glob.glob(re.sub(r'\.pak$', '', path) + '.p[0-9][0-9]')) + data = b''.join(open(p, 'rb').read() for p in parts) + out = [] + for i in range(n): + h, off, csz = struct.unpack_from('>III', idx, 16 + i * 12) + s = data[off:off + csz] + if s[:2] == b'Z1': + try: s = zlib.decompress(s[10:]) + except Exception: continue + out.append((h, s)) + return out + +KIND = {'Turret': 'TurretCount', 'Bridge': 'BridgeCount', 'Hatch': 'HatchCount', + 'ShieldGenerator': 'ShieldGeneratorCount', 'Thruster': 'ThrusterCount', + 'Versatile': 'VersatileCount'} + +def blank(n): + return not n.get('Name') and not n.get('NomalModel') and not n.get('Frame') + +def units_of(pak): + out = [] + for h, b in pak_entries(pak): + if b[:4] != b'IDXD': continue + try: recs = U.parse(b) + except Exception: continue + g = [r for r in recs if r['squadron'] == 'Generic'] + if g and 'HP' in U.named(g[0]): + out.append((recs, U.named(g[0]))) + return out + +def main(): + pak = glob.glob('/work/sylph_extract/**/GP_MAIN_GAME_E.pak', recursive=True)[0] + units = units_of(pak) + print("# Sub-records inside the %d unit tables of %s" % (len(units), os.path.basename(pak))) + print("# Regenerate: python3 tools/re-capture/unit_substructures.py") + print("# See docs/re/structures/unit-substructure-records.md") + + norm = lambda n: re.sub(r'_\d{3}$', '_NNN', n) + fam = collections.Counter(); per = collections.Counter() + core = collections.defaultdict(collections.Counter) + for recs, gn in units: + seen = collections.Counter() + for r in recs: + n = norm(r['squadron']); fam[n] += 1; seen[n] += 1 + for _, fn, _ in r['fields']: + if fn and not re.search(r'_\d{3}$', fn): core[n][fn] += 1 + for n, c in seen.items(): per[n] = max(per[n], c) + print("\n## families (records, max per unit, distinct non-indexed field names)") + for n, c in fam.most_common(): + print(" %-22s %5d max/unit %3d fields %3d" % (n, c, per[n], len(core[n]))) + + for n, _ in fam.most_common(): + if n in ('Generic', 'Maneuver'): continue + print("\n## %s -- non-indexed fields (records carrying it, of %d)" % (n, fam[n])) + for k, v in sorted(core[n].items(), key=lambda x: (-x[1], x[0])): + print(" %-32s %5d" % (k, v)) + + res = collections.Counter(); exc = collections.defaultdict(list) + for recs, gn in units: + sc = U.named([r for r in recs if r['squadron'] == 'StructureCount'][0]) + for k, f in KIND.items(): + subs = [r for r in recs if re.fullmatch(k + r'_\d{3}', r['squadron'])] + w = sc.get(f, '').strip() + if not w.isdigit(): res['no field']+= 1; continue + d = int(w) + if d == len(subs): res['equal']+= 1 + elif d == 0 and len(subs) == 1 and blank(U.named(subs[0])): + res['0 declared, 1 blank placeholder'] += 1 + else: + res['DIFFERS'] += 1 + exc[(k, d, len(subs))].append(gn.get('ID')) + print("\n## CONTROL StructureCount.Count == #_NNN records") + print("## (a 0 count still emits one blank slot)") + print(" %d comparisons (%d units x %d kinds)" % (len(units) * len(KIND), len(units), len(KIND))) + for k in ('equal', '0 declared, 1 blank placeholder', 'DIFFERS', 'no field'): + print(" %-34s %4d" % (k, res[k])) + print(" exceptions:") + for kk in sorted(exc, key=lambda x: -len(exc[x])): + print(" kind=%-8s declared=%-3d records=%-3d x%d" % (kk[0], kk[1], kk[2], len(exc[kk]))) + for i in sorted(exc[kk]): print(" %s" % i) + + wids = collections.Counter(); wnames = set() + for h, b in pak_entries(pak): + if b[:4] != b'IDXD': continue + try: recs = U.parse(b) + except Exception: continue + for r in recs: + if r['squadron'] == 'Weapon': + i = U.named(r).get('ID') + if i: wnames.add(i) + for recs, gn in units: + for r in recs: + if re.fullmatch(r'Turret_\d{3}', r['squadron']): + wids[U.named(r).get('WeaponID', '')] += 1 + hit = sum(v for k, v in wids.items() if k in wnames) + print("\n## CONTROL Turret_NNN.WeaponID resolves into the Weapon datasheet") + print(" Weapon.ID values: %d" % len(wnames)) + print(" turrets: %d, distinct WeaponID: %d" % (sum(wids.values()), len(wids))) + print(" resolve: %d do NOT resolve: %d" % (hit, sum(wids.values()) - hit)) + print(" weapons never mounted on a turret: %d" % len(wnames - set(wids))) + +main()