Settled by elimination with two controls. The 15 loadout names appear as a field VALUE nowhere on the disc -- 0 occurrences across every pak -- while the control Arbalest_155KG, which is referenced, appears 150 times as a value in the same pak. They occur only as the field names of the UNITS record. They are not in the executable either; control: WEAPONS (a section key) is, Arbalest_155KG is not. So nothing references a loadout by name. The selection is which pak entry the Hangar loads. GP_HANGAR_ARSENAL.pak has 180 IDXD entries in 10 shapes; 168 carry a UNITS roster = 28 stages x 6 languages, and the other 12 = 2 x 6. Each entry is one stage's whole Hangar config, and its UNITS roster is the flight for that stage -- 5 distinct rosters over 15 / 5 / 5 / 2 / 1 stages, including one where Rhino2-Katana flies alone. Every count is a multiple of 6 and they sum to 28, the disc's stage count by a fourth independent route. Corrects the previous commit: it read the pak with setdefault, so '15 loadouts / 24 allow-lists / STANDARD_ARM1 has 11 entries' is the union of first-seen records, not one table. Contents vary per entry -- PlayerSET_ARM1 has 5 distinct contents, STANDARD_ARM1 3, ExSET_NOSE 4. The chain and both controls (60/60, 70/88) are per-record and unaffected; only the per-list sizes were over-generalised. Not settled: the entry filenames. name_hash probing over 8 templates x 40 indices x 6 languages resolved 0, while the same probe's controls <lang>\weapon.tbl and <lang>\strings.tbl resolved 12/12. Artefact +40 lines / 1 changed heading; the other four regenerate byte-identical.
141 lines
7.2 KiB
Markdown
141 lines
7.2 KiB
Markdown
# ✅ The Hangar loadout system — loadout → per-slot allow-list → arsenal item
|
||
|
||
Finishes the Hangar. [The item chain](arsenal-item-weapon-chain.md) resolved an
|
||
Arsenal item down to a `Weapon` record; this is the layer above it — **who flies
|
||
with what**, and **what the Hangar is allowed to offer for each slot**.
|
||
|
||
Artefact `../data/hangar-loadouts.txt`, regenerator
|
||
`tools/re-capture/hangar_loadouts.py`.
|
||
|
||
> ## ⚠️ CORRECTION (2026-08-27, same day) — the pak is STAGE-SCOPED, and the
|
||
> ## counts below are a UNION, not one table
|
||
>
|
||
> `GP_HANGAR_ARSENAL.pak` has **180 IDXD entries in 10 distinct shapes**, and
|
||
> **168 of them = 28 stages × 6 languages** carry a `UNITS` roster. Each entry is
|
||
> **one stage's Hangar configuration**: its own `UNITS` roster, its own loadout
|
||
> records, its own allow-lists. The first pass read the pak with `setdefault`,
|
||
> keeping only the **first-seen** copy of each record name — so "15 loadouts / 24
|
||
> allow-lists / `STANDARD_ARM1` has 11 entries" is the union of first-seen, not a
|
||
> single table. The allow-list contents genuinely vary: `PlayerSET_ARM1` has **5**
|
||
> distinct contents across entries, `STANDARD_ARM1` **3**, `ExSET_NOSE` **4**;
|
||
> the `SNNSET_*` and `NULL_*` sets have exactly 1 each.
|
||
>
|
||
> **The structure of the chain below is unaffected** — the 60/60 and 70/88
|
||
> controls are per-record and still hold on the first-seen slice — but treat the
|
||
> per-list sizes as "one stage's", not "the game's".
|
||
|
||
`GP_HANGAR_ARSENAL.pak` carries **15 loadout records** — `Bird1-Sandra`,
|
||
`Bird2-Billy`, `Bird3-Antonius`, `Bird4-Carl`, `Rhino1-Katana`,
|
||
`Rhino1-Raymond`, `Rhino2-Ellen`, `Rhino2-Gene`, `Rhino2-Katana`,
|
||
`Rhino3-Ellen`, `Rhino3-Gene`, `Rhino3-Katana`, `Rhino4-Brandon`,
|
||
`Rhino4-Ellen`, `Rhino4-Yoji` — one per **flight position × pilot**, each with
|
||
`Arm1`, `Arm2`, `Arm3`, `Nose` and `UnitID`.
|
||
|
||
🔑 **Same trap as `PlayerWeapon`, one level up: `Arm1`/`Arm2`/`Arm3`/`Nose` do
|
||
not name items.** They name a **per-slot ALLOW-LIST record** — one of the 24
|
||
whose only *named* field is `Type` (the slot kind: `Arm1`/`Arm2`/`Arm3`/`Nose`).
|
||
The candidate items live in that record's **positional, unnamed fields**, in
|
||
order. So the full path is four hops:
|
||
|
||
```
|
||
Rhino4-Yoji.Arm1 ──▶ STANDARD_ARM1 (an allow-list, Type=Arm1)
|
||
──▶ [Falcon_9AM, Condor_105AM, …] (arsenal item names, in order)
|
||
──▶ item.PlayerWeapon = Turret_NNN (a hardpoint slot)
|
||
──▶ slot.WeaponID = Weapon.ID
|
||
```
|
||
|
||
## 🧪 The controls
|
||
|
||
| | |
|
||
|---|---:|
|
||
| `loadout.Arm1/2/3/Nose` names an allow-list record | **60 / 60** |
|
||
| allow-list positional entries name an arsenal item | **70 / 88** |
|
||
| — every one of the 18 misses | the single sentinel `No_Equipment` |
|
||
| `loadout.UnitID` resolves | 5 a unit `Generic.ID`, 8 a character (as `Character`+value), **2 neither** |
|
||
|
||
`No_Equipment` is one of the four `WEAPONS`-roster values that have no item
|
||
record ([the item chain](arsenal-item-weapon-chain.md)), so the residual is the
|
||
empty-slot sentinel appearing once per list — not a decode failure.
|
||
|
||
## The six allow-list sets
|
||
|
||
| set | Arm1 | Arm2 | Arm3 | Nose | used by |
|
||
|---|---:|---:|---:|---:|---|
|
||
| `STANDARD_*` | 11 | 9 | 9 | 8 | Rhino 2/3/4 wingmen |
|
||
| `ExSET_*` | 10 | 8 | 4 | 7 | — (not referenced by any loadout here) |
|
||
| `S01SET_*` | 2 | 1 | 1 | 1 | Bird flight, `Rhino1-Raymond`, `Rhino3-Ellen` |
|
||
| `S02SET_*` | 3 | 2 | 2 | 1 | — |
|
||
| `PlayerSET_*` | 2 | 1 | 1 | 1 | the three `…-Katana` rows |
|
||
| `NULL_*` | 1 | 1 | 1 | 1 | — (`No_Equipment`, `No_Equipment`, `No_Equipment`, `Stiletto_BG1`) |
|
||
|
||
So a wingman's weapon choice is drawn from a small, ordered, *per-slot* list,
|
||
and the `SNNSET_` sets are stage-scoped variants of it — `S01SET_ARM1` offers
|
||
two Arm1 items, `STANDARD_ARM1` offers eleven.
|
||
|
||
## `UnitID` is two ID spaces at once
|
||
|
||
* **5 rows name a unit** — `UN_f001_TCAF_DeltaSaber_T`,
|
||
`…_T_Player`, `…_f002_W_Player`. The three `-Katana` rows are the **player**:
|
||
they carry an extra (empty) `PlayerUnit` field and point at a `_Player` craft.
|
||
* **8 rows name a character** — `SANDRA`, `BILLY`, `ANTONIUS`, `CARL`,
|
||
`RAYMOND`, `GENE` ×2, `YOJI`. None is a `Generic.ID` on its own; all resolve
|
||
as **`Character` + the value** into the 64-record character table
|
||
([the `Generic` partition](unit-datasheet-static.md)).
|
||
|
||
## 🟡 Two values that resolve to nothing
|
||
|
||
* **`Rhino2-Ellen.UnitID = UN_f001_TCAF_DeltaSaber_W`.** No such unit exists —
|
||
checked as a `Generic.ID` across *every* pak, and as a record name: 0 hits.
|
||
`UN_f002_TCAF_DeltaSaber_W` does exist, and `Rhino1-Katana` uses
|
||
`UN_f002_TCAF_DeltaSaber_W_Player`. **Consistent with a shipped `f001`/`f002`
|
||
typo** — but what the game does when the lookup fails is not shown here, so
|
||
this is reported, not diagnosed.
|
||
* **`Rhino4-Brandon.UnitID = BRANDON`** — no `CharacterBRANDON` among the 64.
|
||
`CharacterKATANA` does exist, and Katana is the player.
|
||
|
||
Both are single rows against 13 that resolve, so they are exceptions in the
|
||
data, not a wrong reading of the field.
|
||
|
||
## ✅ Who selects a loadout — the pak ENTRY does
|
||
|
||
Settled the same day, by elimination with two controls:
|
||
|
||
* The 15 loadout names appear as a field **value nowhere on the disc** — 0
|
||
occurrences across every pak. **Positive control in the same loop:** the item
|
||
name `Arbalest_155KG`, which *is* referenced, appears **150** times as a value
|
||
in the same pak. They appear only as the **field names of the `UNITS` record**,
|
||
i.e. `UNITS` is the index over them.
|
||
* They are **not in the executable** either. **Control:** `WEAPONS` (a section
|
||
key) *is* in the executable; `Arbalest_155KG` is not — so the executable holds
|
||
table keys, not row names.
|
||
|
||
So nothing references a loadout by name. The selection is **which pak entry the
|
||
Hangar loads**, and there is one per stage per language (168 = 28 × 6). The
|
||
entry's `UNITS` roster is the flight for that stage, and it changes across the
|
||
campaign exactly as the story does:
|
||
|
||
| stages | pilots in `UNITS` |
|
||
|---:|---|
|
||
| 15 | Bird 1–4 (Sandra, Billy, Antonius, Carl) + `Rhino1-Katana`, `Rhino2-Ellen`, `Rhino3-Gene`, `Rhino4-Yoji` |
|
||
| 5 | Bird 1–4 + `Rhino1-Raymond`, `Rhino2-Gene`, `Rhino3-Katana`, `Rhino4-Ellen` |
|
||
| 5 | `Rhino2-Katana` **alone** |
|
||
| 2 | Bird 1–4 + `Rhino1-Raymond`, `Rhino2-Katana`, `Rhino3-Ellen` |
|
||
| 1 | `Rhino1-Katana`, `Rhino2-Ellen`, `Rhino3-Gene`, `Rhino4-Brandon` |
|
||
|
||
Every count is a multiple of 6 and they sum to **28** — the disc's stage count,
|
||
reached here by a fourth independent route.
|
||
|
||
🟡 The entry FILENAMES are not recovered. Probing `name_hash` over 8 templates ×
|
||
40 indices × 6 languages resolved **0**; the same probe's controls
|
||
`<lang>\weapon.tbl` and `<lang>\strings.tbl` resolved **12/12**, so the method
|
||
works and the naming convention is simply not one of the eight guessed.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* Which entry is which stage — see the filename note above. The names encode flight position and pilot,
|
||
and [the stage definition table](stage-definition-table.md) has
|
||
`AI_TCAF_RhinoFlight` / `AI_TCAF_BirdFlight` — the join is not shown here.
|
||
* `ExSET_*`, `S02SET_*` and `NULL_*` are referenced by no loadout in this pak.
|
||
* The `PlayerUnit` field is present but empty on all three `-Katana` rows.
|
||
* `IGNORE` (13 item names) — a blocklist for something unread.
|