Files
Sylpheed/docs/re/structures/unit-substructure-records.md
Sylpheed RE agent 4d70b66fa7 re: effects split into two families by digit-width; ptc_pack.xpr holds one
The open question was how an effect mesh is reached at all, after last
iteration's .xpr byte search was refuted by its own control. The corpus already
held the pointer: xbg7-mesh names ptc_pack.xpr, a 20 MB shared particle package
in hidden/resource3d/. It lists 532 distinct eff_* resources - 527 three-digit,
3 four-digit, 2 unnumbered.

Joining it against the 137 effect names the datasheets actually bind:

                    in ptc_pack   not
  3-digit                71        39
  4-digit                 0        27

Zero of the 27 four-digit names resolve in ptc_pack, and that series is a closed
three-letter set: e (10), f (12), n (5). Since eff_e0033 was found in Base.xpr,
the reading is 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, which is the difference from
last iteration. The same kind of byte 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.

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.

Not closed: 66 of the 137 bound effects still have no located home, eff_n0071
among them. But the route is now real and has a number on it, and the next step
is the eff_j### family and the four-digit series rather than another disc-wide
grep.

All sixteen artefacts byte-identical.
2026-08-28 08:55:41 +00:00

311 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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.