Chasing the 17 unlocated eff_l### turned up their shape first: they come in
_e/_f PAIRS - eff_l101_e + eff_l101_f, and the same for l102, l104, l105, l106,
l201, plus _e-only l010/l011/l107/l108 and _f-only l002.
Partitioning every eff_<letter><digits>_<e|f> binding by the ID letter of the
OWNING unit (one GP_MAIN_GAME_* pak = one user):
effect _e effect _f
UN_e### 33 0
UN_f### 0 61
94 of 94 agree and both off-diagonal cells are empty. The control reads the
factions straight off the IDs: UN_e### -> ADAN (42 objects), UN_f### -> TCAF
(26), UN_n### -> TTRL (2, tutorial, binding neither). So an effect ending _e
belongs to an ADAN ship and one ending _f to a TCAF ship - the same visual is
authored twice, once per faction, which is exactly why eff_l### arrives in pairs.
What the 34 unlocated ARE is now also clear, even though where they live is not.
They are one job, not a scatter: Generic binds 32 of the 34, Explosion 19,
Shell 9, Level_0 and Weapon 2 each. The binder fields rank LowerHPFxModel 252,
HitFxModel 144, then JetFxModel_00N and AfterBurnerFxModel_00N. They sit in the
six GP_MAIN_GAME_* paks at 130 bindings each plus 32 in DefTables.pak. Since
LowerHPFxModel is the damaged-ship effect, the residual is largely the
per-faction battle-damage and hit visuals. None of the 34 is a record name and
only one is a field name, so they are asset references.
Stated plainly: they remain unlocated AS ASSETS. Knowing the family and its
naming law does not say where the geometry lives - the .xpr route is exhausted
for them and the parsed pak payloads hold references, not meshes.
Also fixes a defect in the artefact shipped last commit. effect-homes.txt came
back with two equal-count lines swapped: Counter.most_common() breaks ties by
insertion order, so the package listing was not deterministic. Now sorted by
(-count, name) and verified to regenerate byte-identical twice running. This is
the corpus's own rule - any map built by iterating a set or Counter needs
sorted() - and the new tool had violated it.
The other sixteen artefacts are byte-identical; effect-homes.txt changes only in
the tie-break ordering of the five 1-count rows, with every line pairing.
20 KiB
✅ 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 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. 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 |
| 2–6 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 01–04 / 05–08 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_NoseGunandTurret_001…Turret_Missile, both mask0x01, both with an emptyFrame.UN_e107_ADAN_AAFrigate_EX4:TurretCount 8, nine records — eightGN_GunXS_0N_ContHgun turrets, plus one…Ship_AAMissileat mask0x0cwith noFrameat 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_NNNhas 0 populated records in this pak; only the???template row exists, so its schema is known and its instances are not.Opened above: a 6-bit mount mask. Which sector each bit means is still open.CoverAreareads as a hex word — a bit mask, bits unknown.- The 6
no fieldcomparisons are one unit whoseStructureCountomits 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 effects — eff_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.
❌ An .xpr byte search cannot answer "does this effect ship"
Testing the four genuinely-undeclared effects against the 166 .xpr packages
gave eff_f0002 and eff_f0002_barn in Base.xpr, and nothing for eff_e0044
or eff_h308. But the control refutes the instrument, not the effects:
eff_n0071 — measured live, bound by an Explosion record's
ExplosionFxModel — also returns nothing from the same search.
A known-live name the test cannot find means the test has no power here. So:
eff_f0002/eff_f0002_barndo have a mesh inBase.xpr(presence is still evidence when it is positive);- nothing follows about
eff_e0044oreff_h308, and the earlier remark thateff_e0058,eff_e0059andeff_e0060"have no mesh" should be read the same way — absent from a byte search over.xpris not absent from the disc.
Whatever ships eff_n0071 is not a plain name string inside those 166 files,
so effect meshes must be reached some other way. Finding that route is the open
question; until then, only the positive half of this test counts.
🔑 Effects live in two families, split by digit-width — ptc_pack.xpr holds one
The open question from the last iteration was how an effect mesh is reached at
all. The corpus already had the pointer: xbg7-mesh mentions ptc_pack.xpr
and its eff_* entries. It is a 20 MB shared particle package in
hidden/resource3d/, and it names 532 distinct eff_* resources — 527 of
them with a three-digit number, 3 with four digits, 2 unnumbered.
Joining it against the effect names the datasheets actually bind:
| bound name | in ptc_pack |
not |
|---|---|---|
3-digit (eff_h308, eff_m…) |
71 | 39 |
4-digit (eff_e0033, eff_n0071) |
0 | 27 |
🔑 Zero of the 27 four-digit names resolve in ptc_pack, and the four-digit
series is a closed three-letter set — e (10), f (12), n (5). Meanwhile
eff_e0033 was found in Base.xpr. So the disc carries two effect
families: a shared three-digit particle library in ptc_pack.xpr, and a
four-digit series that lives in the per-model and base packages instead.
⚠️ The instrument passes its own control this time. The previous iteration's
.xpr byte search was refuted because a known-live name returned nothing. Here
the same kind of search demonstrably reads names out of this file — 532 of
them — so a zero within ptc_pack is meaningful in a way the earlier
disc-wide zero was not. That is the difference between a search that has been
shown to work on the population being searched and one that has not.
Among the 39 three-digit misses the letters cluster hard: j 22, t 14, m 2,
h 1 — and ptc_pack contains just one j name against 149 m and 81 s.
So eff_j### is a third grouping that is almost entirely elsewhere.
🟡 This does not yet close the question — 66 of the 137 bound effects still have
no located home, eff_n0071 among them. But the route is now a real one with a
number on it, and the next step is the eff_j### family and the four-digit
series rather than another disc-wide grep.
✅ The effect→package map — 103 of 137 located, and a correction
Enumerating eff_* names per .xpr across all 166 packages and matching the
bound names exactly gives a real home for 103 of the 137, up from the 71
ptc_pack alone accounted for. Only 36 packages carry an effect name at
all, and two dominate:
| package | bound effects it holds |
|---|---|
Base.xpr |
53 |
ptc_pack.xpr |
46 |
Stage_S28.xpr |
2 |
five rou_f001_wep_NN.xpr |
1 each |
🔑 There are two shared effect libraries, not one. ptc_pack.xpr is the
particle library the previous iteration found; Base.xpr is the larger home
for bound effects. ptc_pack.xpr is also the only *_pack library on the
disc — no other shared bundle exists.
By digit-width: 3-digit 80 resolved of 110, 4-digit 23 of 27. So the
previous iteration's reading survives and sharpens — the four-digit series
really does live outside ptc_pack (that zero stands), and now we can say where:
Base.xpr.
❌ Correction: eff_f0002 does not ship — that was a substring hit
The previous iteration reported eff_f0002 and eff_f0002_barn as present in
Base.xpr. Both were substring artefacts. What the file actually contains
is eff_f0002_barnhaze — one longer resource name that grep -l eff_f0002
and grep -l eff_f0002_barn both match inside. Neither bound name is there.
This is the corpus's own paid-for lesson arriving from the other direction: a
prefix hit is not an exact hit (cf. rot_n001 vs rot_n001_break), and here it
was the bound name that was a prefix of the stored one. The map above is
exact-keyed, so it does not have this failure; the earlier positive is withdrawn.
🟡 The 34 that remain
The unlocated residual is dominated by a family the previous iteration did not
single out — eff_l### with 17 of the 34 — then eff_h### 4, eff_s### 4,
eff_j### 2, eff_m### 2, eff_t### 1, and four four-digit names
(eff_e0044, eff_f0002, eff_f0002_barn, eff_n0071).
⚠️ Note the scope shift: the j 22 / t 14 clustering reported last time was
the residual against ptc_pack alone; against all packages those families
are largely accounted for and l is what is left. Both numbers are right for
their own population — which is the reason to say which population a residual
is measured against.
Regenerate with tools/re-capture/effect_homes.py →
docs/re/data/effect-homes.txt, which lists all 34
by name.
✅ _e / _f on an effect name is the FACTION of the unit that binds it
Chasing the 17 unlocated eff_l### turned up their shape: they come in
_e/_f pairs — eff_l101_e + eff_l101_f, and the same for l102,
l104, l105, l106, l201, plus _e-only l010, l011, l107, l108
and _f-only l002.
Partitioning every eff_<letter><digits>_<e|f> binding by the ID letter of the
owning unit (one GP_MAIN_GAME_* pak = one user):
effect _e |
effect _f |
|
|---|---|---|
UN_e### |
33 | 0 |
UN_f### |
0 | 61 |
94 of 94 agree; both off-diagonal cells are empty. And the unit IDs name the
factions themselves — the control reads UN_e### → ADAN (42 objects),
UN_f### → TCAF (26), UN_n### → TTRL (2, tutorial, binding neither).
🔑 So an effect ending _e belongs to an ADAN ship and one ending _f to a
TCAF ship: the same visual effect is authored twice, once per faction. That
is why the eff_l### family arrives in pairs.
🟡 What binds the 34 unlocated, and what they are
The 34 are not scattered — they are one job. Generic binds 32 of the 34,
Explosion 19, Shell 9, and two apiece for Level_0 and Weapon. The
binder fields rank LowerHPFxModel 252, HitFxModel 144, then
JetFxModel_00N and AfterBurnerFxModel_00N at 48/30 each. They sit in the six
GP_MAIN_GAME_* paks (130 bindings each — one user) plus 32 in DefTables.pak.
LowerHPFxModel is the damaged-ship effect, so the unlocated set is largely
the per-faction battle-damage and hit visuals. None of the 34 resolves as a
record name and only one as a field name, so they are asset references, not
records.
⚠️ Still unlocated as assets. Knowing what they are for does not say where
their geometry lives; the .xpr route is exhausted for them and the parsed pak
payloads hold references, not meshes. Worth stating plainly: this iteration
identified the family and its naming law, not the missing package.