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.
567 lines
27 KiB
Markdown
567 lines
27 KiB
Markdown
# ✅ 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
|
||
```
|
||
|
||
✅ **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](arsenal-item-weapon-chain.md). The two `+1`
|
||
warship/craft cases are still unexplained.
|
||
|
||
⚠️ The "10 distinct `WeaponID`s" 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](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.
|
||
|
||
## ✅ `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_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 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_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 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`](../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.
|
||
|
||
### ✅ 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_f0002` ⊂ `eff_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 00–84 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.
|