Files
Sylpheed/docs/re/structures/unit-substructure-records.md
Sylpheed RE agent a7cbb7342c re: every bound effect vs every declared effect; a Shift-JIS dev placeholder
The Explosion substructure's field list was already in the corpus, so the open
part was whether the effect names RESOLVE. Declared x used, sweeping 34 binder
field names (*FxModel*, *EffectName, Effect_*, ShellModel, CoverModel,
SilhouetteModel) against every name declared by a LOD_Effect_<n> or
GameModel_<n> field anywhere on the disc:

                 declared   not declared
  used              172          58
  not used          318           -

490 declared, 230 used. Top binders: Effect_Paralyze 2874, ShellModel 996,
HitFxModel 738, JetFxModel_000 408, MuzzleFlashFxModel_Loop 384.

The 58-cell is almost one field. 53 of the 58 are bound by SilhouetteModel
alone and are all rou_f###_wep* names - the arsenal item silhouettes already
documented in arsenal-item-weapon-chain. They are undeclared because they are
the wrong KIND: each resolves as its own standalone package, rou_f001_wep_01.xpr
and friends, never as a LOD-table entry. Nothing is missing; the sweep was
reading an asset-file name as though it were an effect name. That leaves four
genuinely undeclared effects - eff_e0044, eff_f0002, eff_f0002_barn, eff_h308 -
none of which resolves as a record or field name either.

One "effect name" in that cell is not a name at all. Bound by Effect_Explosion
and Effect_Flare, 72 occurrences = 12 users, its bytes are 95 B6 8E 9A 97 F1 -
Shift-JIS for the word "character string". A developer's placeholder. This
exposes a reader defect worth fixing before these strings reach a port:
unitgroup.py hands the value back as Latin-1 mojibake, so an IDXD string field
can carry Shift-JIS and our decode does not know it.

Following the silhouettes into the ISO tree turned up a convention that IS real:
59 of the 166 .xpr packages end _hangar.xpr, and 59 of 59 have a bare twin of
the same stem. The direct contrast to yesterday's refutation, where _all/_child
was 1 of 166 with a single stem. Suffix conventions in this corpus are worth
testing precisely because they are not all real.

Controls reproduced from the previous iteration: eff_n0071 is declared and used
6x (= one user under the per-pak-copy rule); eff_e0033 is declared and used 0x,
sitting in the 318-cell. That cell is expected rather than alarming - the
EnumLODSet/EnumGameModel family is overwhelmingly equipment, which no unit
datasheet binds.

All fifteen artefacts byte-identical.
2026-08-28 08:05:28 +00:00

12 KiB
Raw Blame History

The destructible-subsystem model — a unit's sub-records

The corpus has named the sub-records of a unit .tbl since unit-struct-runtime ("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). 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 nameGN_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, TakeoffIntervala 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. The two +1 warship/craft cases are still unexplained.

⚠️ The "10 distinct WeaponIDs" 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 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 WeaponIDs. 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.

CoverArea is a 6-bit mount mask — and the other three leftovers

Artefact ../data/turret-coverarea.txt, regenerator tools/re-capture/turret_coverarea.py.

Six bits, never more. Across all 835 turrets: 41 distinct values, max 0x3f, bits 6 and 7 set on none. Per-bit counts 709 / 117 / 274 / 274 / 260 / 237; popcounts 0…6.

mask n what
0x00 34 the empty Weapon_NULL hardpoints — YawLimit 0 on all 34
0x01 (one bit) 439 craft hardpoints — YawLimit 2.5 / 1.0 / 0
26 bits 362 warship mounts — YawLimit 45…180, real pitch limits

🔑 It tracks the MOUNT, not the weapon. UN_e107_ADAN_AAFrigate carries eight identical ..._AAFrigate_AAGun turrets: GN_GunXS_01…04 are 0x1d and GN_GunXS_05…08 are 0x2d — same gun, same YawLimit 120, different mask. UN_f104_TCAF_Battleship spreads five masks (0x1d, 0x39, 0x35, 0x27, 0x2b) across GN_TGunL_01…05 / GN_TGunM_02…03, all firing the same CAF_Ship_ASGun.

🟡 Which physical sector each bit denotes is NOT determined. Six bits and a field called CoverArea invite ±X/±Y/±Z or six hull faces, and the 0104 / 0508 split above looks like a side — but nothing static fixes the axis convention. A reading, not adopted. Also refuted on the way: bits 2 and 3 are not a mutually-exclusive pair — 188 turrets set both.

The 26 weapons no turret mounts

Weapon.ID 131, mounted 105, never 26 — and they are a coherent set, not strays: 11 _P player variants (…_Beam_P, _Gun_P, _Bomb_P, …), the nose and twin mounts (_NoseGun, _NoseGun_P, _NoseGun_None, _NoseGun_Ttrl, _TwinGun, _TwinGun_P), two _Child sub-munitions (wep_13, wep_29), the three S16Boss_* weapons, three Weapon_Test_*, and ADAN_Attacker_S_GunTurret.

Versatile_NNN ships nowhere

0 populated records across all 41 archives — only the ??? template row. The schema declares a sixth destructible-part kind with zero instances.

The two units one turret short — both extras are MISSILE mounts with no frame

  • UN_e001_ADAN_Elan_EX4: TurretCount 1, two records — Turret_000 …Turret_NoseGun and Turret_001 …Turret_Missile, both mask 0x01, both with an empty Frame.
  • UN_e107_ADAN_AAFrigate_EX4: TurretCount 8, nine records — eight GN_GunXS_0N_ContH gun turrets, plus one …Ship_AAMissile at mask 0x0c with no Frame at all.

🔑 In both, the uncounted extra is a missile launcher with no mesh node — so TurretCount counts frame-mounted guns, and a frameless launcher rides in the same record family without being counted. Consistent across both, n=2, so stated as the observed pattern, not a rule.

🟡 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 — a bit mask, bits unknown. Opened above: a 6-bit mount mask. Which sector each bit means is still open.
  • The 6 no field comparisons are one unit whose StructureCount omits every count.

Every effect a datasheet binds, against every effect the LOD tables declare

The Explosion substructure's field list is already above; what was untested is whether its ExplosionFxModel — and the whole effect-binder family — actually resolves. Run as a declared × used partition, sweeping 34 binder field names (*FxModel*, *EffectName, Effect_*, ShellModel, CoverModel, SilhouetteModel) against every name declared by a LOD_Effect_<n> or GameModel_<n> field anywhere on the disc:

declared not declared
used 172 58
not used 318

490 declared, 230 used. The top binders are Effect_Paralyze (2 874 values), ShellModel (996), HitFxModel (738), JetFxModel_000 (408), MuzzleFlashFxModel_Loop (384).

🔑 The 58-cell is almost one field. 53 of the 58 are bound by SilhouetteModel alone, and all 53 are rou_f###_wep* names — the arsenal item silhouettes already documented in arsenal-item-weapon-chain. They are undeclared because they are the wrong kind: each resolves as its own standalone package, e.g. rou_f001_wep_01.xpr, and never as a LOD-table entry. Nothing is missing — the sweep was reading an asset-file name as if it were an effect name.

That leaves four genuinely undeclared effectseff_e0044 (ExplosionFxModel), eff_f0002 + eff_f0002_barn (JetFxModel_00N / AfterBurnerFxModel_00N) and eff_h308 (HitFxModel) — plus one placeholder (below). None of the four resolves as a record name or a field name either.

⚠️ Controls, both from the previous iteration and both reproduced: eff_n0071 is declared and used 6× (= one user, per the per-pak-copy rule); eff_e0033 is declared and used 0×, i.e. it sits in the 318-cell. The 318 is expected rather than alarming — the EnumLODSet/EnumGameModel family is overwhelmingly equipment, so most of what it declares no unit datasheet binds.

🔑 A Shift-JIS dev placeholder: 文字列

One "effect name" in the 58-cell is not a name at all. Bound by Effect_Explosion and Effect_Flare, 72 occurrences = 12 users, its bytes are 95 B6 8E 9A 97 F1 — Shift-JIS for 文字列, literally "character string". It is the placeholder a developer left in the field.

⚠️ Reader defect this exposes: tools/re-capture/unitgroup.py hands the value back as Latin-1 mojibake (¶ñ), so an IDXD string field can carry Shift-JIS and our decode does not know it. Worth fixing before any of these strings reach a port; harmless for name-matching, because every other value in the corpus is ASCII.

_hangar is a package-naming convention — 59 of 166, zero partials

Following the silhouettes into the ISO tree: 59 of the 166 .xpr packages end _hangar.xpr, and 59 of 59 have a bare twin of the same stem. So a model shown in the hangar ships as a second package beside the in-flight one.

This is the direct contrast to the previous iteration's refutation — _all / _child was 1 of 166 with one stem and is not a convention; _hangar is 59 of 166 with zero partials and is. Suffix conventions in this corpus are worth testing precisely because they are not all real.