# `stage\UnitGroup_S.tbl` β€” the per-stage squadron roster Status: βœ… container format and field semantics, validated across all 28 stage tables present on the disc; 🟑 one member field; ❔ the record key and the arrival-interval *values*. Reached from the per-stage definition record's `EnumerateSquadron` field β€” see [stage-definition-table.md](stage-definition-table.md). Tool: `tools/re-capture/unitgroup.py` (pure static; runs no emulator). ``` python3 tools/re-capture/unitgroup.py S02 # full roster python3 tools/re-capture/unitgroup.py --all --check # self-check every stage ``` A committed dump of Stage 02 is at [`../data/unitgroup-s02.txt`](../data/unitgroup-s02.txt). ## βœ… Container ``` 0x00 "IDXD" 0x04 u32 nrec 0x08 nrec x 16 record: (key, squadron_off, field_lo, field_hi) u32 npool npool x 12 property entry: (tag, name_off | 0xffffffff, value_off) u32 strsize -- and STR + strsize == filesize, exactly strsize string pool; [0] = "Enumerate_Squadrons", [0x14] = "" ``` **Every offset in the file is relative to `STR`**, the string-pool base. A record's fields are pool entries `[field_lo, field_hi)`. An entry with `name_off == 0xffffffff` is *positional*; otherwise the entry carries its own field name inline, so the table is self-describing and the tag hash never has to be inverted. Exactly one record per file is not a squadron: it is the **`Enumerate_Squadrons` roster**, whose entries map `key -> squadron id` for every other record. It is usually the last record but not always β€” in S05, S06, S07, S08, S09, S11, S13, S28 and S29 it sits elsewhere, so find it by its missing `Count`, not by position. ## βœ… A squadron record Five named fields, always present and always these five: | field | values | |---|---| | `Count` | 1 (Γ—1082), 2 (Γ—50), 3 (Γ—8), 4 (Γ—20) | | `SideID` | `ADAN` (745), `TCAF` (294), `Neutral` (121) | | `AIID` | 31 distinct, e.g. `AI_ADAN_CraftSquadron_Veteran`, `AI_TCAF_BirdFlight`, `AI_ADAN_Fleet`, `AI_Structure`, `AI_TraceRoute` | | `FormationID` | e.g. `Formation_4_Bird`, `Formation_ADAN_Turret07_30`, `Formation_1_only` | | `DisableInterval` | `No` (1129), **`Yes` (31)** | …preceded by `Count` **member tuples** of four positional entries: ``` (unit model, message set, n, identity) ``` * unit model β€” 122 distinct, and they are our XBG7 mesh names: `UN_f001_TCAF_DeltaSaber_T_Player`, `UN_e010_ADAN_Attacker_S`, `UN_e007_ADAN_Turret`, `UN_bf001_TCAF_SchlosBase`. * message set β€” the squadron's radio chatter, e.g. `MessageSet_Ellen`, `MessageSet_ADAN_plA`. * `n` β€” 🟑 **unidentified**; integer 1…30, dominated by 1 (735) and 9 (197). It is *not* the `_NN` suffix of `FormationID` (`Formation_ADAN_Turret07_30` pairs with `n = 9`). Plausibly a spawn or wing size, untested. * identity β€” 63 distinct: named pilots (`ELLEN`, `SANDRA`, `RAYMOND`, `YOJI`), ship nameplates (`NP_Charon`, `NP_Amalthea`, `NP_Olympus`), carrier tags (`Carrier_ADAN01`), cargo tags (`CARGO_1`), or empty. Worked example β€” Stage 02, squadron `TCN004`, the player's own flight: ``` TCN004 TCAF Count=2 DisableInterval=No Formation_2_Rhino1 AI_TCAF_RhinoFlight unit=UN_f001_TCAF_DeltaSaber_T_Player msg=MessageSet_Katana n=1 id=Character_Player_Test unit=UN_f001_TCAF_DeltaSaber_T msg=MessageSet_Ellen n=1 id=ELLEN ``` ## βœ… Two independent self-checks, both corpus-wide 1. **`len(fields) == Count * 4 + 5`** β€” holds for **1160 of 1160** squadrons across all 28 stage tables. This is what pins the member-tuple width at 4 and the named-field count at 5; it was not assumed. 2. **Roster agreement** β€” the `Enumerate_Squadrons` record's `key -> id` mapping agrees with the name each record resolves independently through its own `squadron_off`, for **1160 of 1160**. S17 has no `stage\UnitGroup_S17.tbl` in `GP_MAIN_GAME_E.pak`, matching the missing S17 stage record. ## ❌ Refuted along the way * **The record key is not the squadron id's name hash.** `name_hash("TCN001") = 0xd639f1a4`, but the keys run `0x659aff47, 0x659b0046, 0x669b0047, …` β€” 0 of 112 match. What the key encodes is still ❔. Its byte structure (`b0` ramping, `b2` taking small signed values) looks like a packed tuple, untested. * **Squadron ids do not use a separate string base.** An earlier reading here put them at `STR + 0x15` and scored "109 of 111", which looked like a near-fit but was an artefact of the uniform 7-byte id stride β€” it silently shifted every name by three entries (record 0 read as `ADN104` instead of `ADN101`, and 17 `TC*`-named squadrons came out with `SideID = ADAN`). The roster record refuted it outright, and with the correct base β€” plain `STR` β€” agreement is 111/111. The lesson is that a 98 %-looking score on a self-consistent stride is not evidence; the roster was an independent oracle and should have been consulted first. * **The 20-byte "section header"** described in an earlier draft of [stage-definition-table.md](stage-definition-table.md) does not exist. What looked like `(tag, 0, 0, count, size)` was the file's last 16-byte record followed by the `npool` word. The corrected layout above is uniform across all 28 files, which the earlier reading was not β€” it failed on 9 of them. ## What is still open * The meaning of the 4-byte record key. * The member tuple's third field `n`. * **The interval values.** `DisableInterval` is a per-squadron *flag* and 31 squadrons set it, but the interval durations, spawn triggers and arrival positions are not in this file. The next places to look are `Formation_*.tbl` (the formation geometry and possibly its timing) and `EnumSquadron_Test.tbl`, both named by the stage record.