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.<Kind>Count == #<Kind>_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.
This commit is contained in:
@@ -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.<Kind>Count == #<Kind>_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),
|
||||
|
||||
@@ -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 |
|
||||
|
||||
402
docs/re/data/unit-substructures.txt
Normal file
402
docs/re/data/unit-substructures.txt
Normal file
@@ -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.<Kind>Count == #<Kind>_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
|
||||
131
docs/re/structures/unit-substructure-records.md
Normal file
131
docs/re/structures/unit-substructure-records.md
Normal file
@@ -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 == #<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
|
||||
```
|
||||
|
||||
🟡 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.
|
||||
121
tools/re-capture/unit_substructures.py
Normal file
121
tools/re-capture/unit_substructures.py
Normal file
@@ -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.<Kind>Count == #<Kind>_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()
|
||||
Reference in New Issue
Block a user