Files
Sylpheed/docs/re/structures/unit-substructure-records.md
Sylpheed RE agent 7ba415fbae re: the last 9 effects are genuinely unshipped; wep_85 accounts for two
EF_IDX_ proved that prefixes exist, so the residual deserved the same treatment
across every package rather than one. Censusing prefixes over all 36
effect-carrying .xpr gives seven in use:

  EF_IDX_       223 names
  mdl_           45
  EF_IDX_mdl_    45
  VolumeLine_    10
  GN_            10
  GN__            6
  bare           the rest

Testing all nine unlocated names against all seven prefixes: 0 of 9 resolve.

That zero has force where the earlier disc-wide zero did not, and for the stated
reason - the control shows each prefix genuinely carries names the same search
reads (223, 45, 45, 10, 10, 6), so the instrument demonstrably works on the
population it is being asked about. The nine are bound by a datasheet field and
shipped in no package.

Two of them turn out to belong to one cut asset. eff_m010_wep_85 and
eff_m011_wep_85 name a weapon, and the weapon is real in the data:
Weapon_DSaber_P_wep_85_Beam with 24 uses, its Shell_, WeaponCannonName_ and
WeaponShellName_ siblings, and GameModel_eff_m010_wep_85 / _m011_ declaring the
two effects. But the weapon packages stop at 84 - hidden/resource3d/ holds 59
rou_f001_wep_NN.xpr files numbering 00 to 84 with gaps, and no wep_85. So wep_85
is a declared-but-unshipped weapon and its two effects go missing with it.

This does not contradict "every weapon is placed - 131 = 105+22+0+4". That
partition is declared x MOUNTED IN A LOADOUT, which is a different question from
whether a package ships.

Seven remain with no account: eff_e0044, eff_f0002, eff_f0002_barn, eff_h308,
eff_j002_e01, eff_j002_e02, eff_n0071. The .xpr route is now exhausted for them
under every prefix the disc uses; a different container or a runtime generator is
what is left.

All seventeen artefacts byte-identical.
2026-08-28 10:17:20 +00:00

27 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.

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 ExplosionFxModelalso 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_barn do have a mesh in Base.xpr (presence is still evidence when it is positive);
  • nothing follows about eff_e0044 or eff_h308, and the earlier remark that eff_e0058, eff_e0059 and eff_e0060 "have no mesh" should be read the same way — absent from a byte search over .xpr is 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 sete (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.pydocs/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 pairseff_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.

The faction law holds across all six paks and four fields — 564 of 564

The previous section measured _e/_f on one pak and only through the effect binders. Widening to every string field of every unit object in all six GP_MAIN_GAME_* paks:

value _e value _f
UN_e### 198 0
UN_f### 0 366

564 of 564 agree; the mismatch residual is empty. And the law is not confined to one field — it holds separately, at 100%, in each of four:

field agree
LowerHPFxModel 252 of 252
ShieldHitEffectName 210 of 210
ShieldRecoverEffectName 84 of 84
ExplosionFxModel 18 of 18

Two of those — ShieldHitEffectName and ShieldRecoverEffectName — were not in the earlier measurement at all, so the law reaches further than the *FxModel family that suggested it.

⚠️ But state the scope exactly: this is a law about effects, not about assets in general. The sweep covered every field, and every _e/_f-suffixed value a unit binds turns out to live in those four effect fields. No model, motion or SE value carries the suffix at all — so the faction pairing is not shown for those kinds; there was simply nothing to test. "General across fields" here means general across the fields that use it.

UN_n### (TTRL) binds no _e/_f value in any of the six paks — 12 objects (= 2 users) with nothing on either side, confirming the earlier single-pak observation over the whole population.

⚠️ A correction to the ID grammar, and a reconciliation. The earlier section counted 42 + 26 + 2 = 70 unit objects using a regex that required UN_<letter>###_<FACTION>_; the looser UN_<letter>###_ finds 71. The one extra is UN_e910_core_ADAN_GeneratorCore, which inserts a sub-kind token before the faction tag. So the ID grammar is UN_<letter>###_[<subkind>_]<FACTION>_<name>, and both counts were right for their own pattern.

ptc_pack holds 727 names, not 532 — an EF_IDX_ prefix the pattern truncated

Censusing the package's own naming vocabulary turned up a third variant of the prefix trap, and this one had been corrupting a number the corpus carried.

268 of ptc_pack's names do not start with eff_ — they start with EF_IDX_, e.g. EF_IDX_eff_d001_f. A regex anchored at eff_ chops that prefix off and merges distinct names, which is where the earlier figure of 532 came from. Enumerating maximal [A-Za-z0-9_] runs gives 727.

⚠️ The two earlier traps were a stored name being longer (rot_n001_break) and a bound name being a prefix (eff_f0002eff_f0002_barnhaze). This is the third: a prefix the pattern cannot see at all, because the anchor sits in the middle of the real name.

The map improves to 128 of 137 — and a claim of mine is withdrawn

Looking each bound name up bare and under EF_IDX_ resolves 25 of the 34 that were unlocated. The map is now:

package bound effects held
ptc_pack.xpr 71
Base.xpr 53
Stage_S28.xpr 2
five rou_f001_wep_NN.xpr 1 each

This withdraws the previous section's correction. I had recorded that Base.xpr holds more bound effects than ptc_pack (53 vs 46) and struck out "ptc_pack is the effect library". With the prefixed keys counted, ptc_pack holds 71 — it is the larger library, and the 46 was an undercount from the same truncating pattern. Two shared libraries remains right; which is bigger does not.

🟡 Nine names remain unlocated, small enough to print in full: eff_e0044, eff_f0002, eff_f0002_barn, eff_h308, eff_j002_e01, eff_j002_e02, eff_m010_wep_85, eff_m011_wep_85, eff_n0071.

The suffix vocabulary, and _root as the outermost token

Splitting all 727 names on _ gives 106 distinct tokens. The frequent ones are IDX 223 (the prefix above), _f 137, _e 119, _root 87, _col 54, _mdl 45, _break 43, _ring 38, _ALL 17, _haze 14, _thunder 10 — a mix of one structural marker, the faction pair, and descriptive part names.

⚠️ Method note: that census counts all tokens, not trailing ones — which is exactly how the EF_IDX_ prefix surfaced in what I first labelled a suffix list. The mislabel found the bug.

Testing the two structural candidates the way _hangar was tested — does the suffixed name have a bare parent?

token names bare parent present
_ALL 17 17 of 17
_root 87 64 of 87
_break 30 15 of 30
_e 74 0 of 74
_f 61 0 of 61

🔑 _root is strictly terminal — 87 of 87, and _root never appears mid-name. The compound shapes place it outermost: _e_root 19, _f_root 18, _break_root 13, bare _root 30. So the order is <stem>_[<faction>|<break>]_root, and _root reads as a hierarchy marker rather than a variant — though 64 of 87 having a bare parent means it is not simply "the parent of an existing node", and _break at 15 of 30 is likewise not a plain "destroyed twin of every effect".

🔑 _e/_f never have a bare parent — 0 of 135. That is independent, asset-side confirmation of the faction law: an effect is authored per faction, and there is no faction-neutral original for either side to derive from.

The last 9 are genuinely unshipped — a controlled zero over seven prefixes

EF_IDX_ proved prefixes exist, so the residual deserved the same treatment across every package rather than one. Censusing prefixes over all 36 effect-carrying .xpr gives seven in use:

prefix names it carries
EF_IDX_ 223
mdl_ 45
EF_IDX_mdl_ 45
VolumeLine_ 10
GN_ 10
GN__ 6
(bare) the rest

Testing all nine unlocated names against all seven: 0 of 9 resolve.

⚠️ This zero has force where the earlier disc-wide zero did not, and for the stated reason: the control shows each prefix genuinely carries names the same search reads — 223, 45, 45, 10, 10, 6. The instrument demonstrably works on the population it is being asked about.

So the nine are bound by a datasheet field and shipped in no package: eff_e0044, eff_f0002, eff_f0002_barn, eff_h308, eff_j002_e01, eff_j002_e02, eff_m010_wep_85, eff_m011_wep_85, eff_n0071.

🔑 Two of the nine belong to wep_85 — a declared weapon with no package

eff_m010_wep_85 and eff_m011_wep_85 name a weapon, and the weapon is real in the data: Weapon_DSaber_P_wep_85_Beam (24 uses) with its Shell_, WeaponCannonName_ and WeaponShellName_ siblings, plus GameModel_eff_m010_wep_85 / _m011_ declaring the two effects.

But the weapon packages stop at 84. hidden/resource3d/ holds 59 rou_f001_wep_NN.xpr files numbering 0084 with gaps, and no wep_85. So wep_85 is a declared-but-unshipped weapon, and its two effects go missing with it — one cut asset explaining two of the nine.

⚠️ This does not contradict "every weapon is placed — 131 = 105+22+0+4" (weapon-struct-runtime): that partition is declared × mounted in a loadout, which is a different question from whether a package ships.

🟡 Seven remain with no account: eff_e0044, eff_f0002, eff_f0002_barn, eff_h308, eff_j002_e01, eff_j002_e02, eff_n0071. The .xpr route is now exhausted for them under every prefix the disc uses.