re: find the mission wave data — stage records and the UnitGroup squadron roster

Sweeping the 811 unnamed IDXD objects in GP_MAIN_GAME_E.pak by schema turned up
schema 3c9ae32e: the per-stage definition record. 23 of them, one per stage,
each naming its background, resource package, collision set, message set,
nameplates, MapMesh/MapPath and EnumerateSquadron = UnitGroup_S<NN>.tbl.

That resolves two open threads at once:

  - MapPath = test.rgn hashes to 0x3506e972, a REGN object in MiscBin.pak, and
    MapMesh = test.col to 0x2cf7eb47, an MCOL object. REGN is a stage's map
    path data; MCOL is its collision mesh.

  - stage\UnitGroup_S02.tbl (0x019fd129, in all six language paks) is the
    Stage 02 squadron roster: 112 records, 112 squadron IDs, and a field
    vocabulary of FormationID / AIID / SideID / Count / DisableInterval, plus
    the unit model (UN_e010_ADAN_Attacker_S and friends, which match the XBG7
    mesh names we already decode), the MessageSet and the pilot character.

DisableInterval is the first direct evidence of the arrival-timing knob, which
is what the user's reframing predicted: the mission has a schedule with
parameters, not a fixed roster.

Container layout is only partly read. The 112x16 entry array was confirmed by
its boundary — keys increase for exactly 112 entries and break at 0x708, where
the next section header sits — not assumed. pak dump mislabels this file's
first key as its schema.

Refuted and recorded: the 16-byte record key is not the squadron ID's name
hash. name_hash("TCN001") = 0xd639f1a4 but the keys start 0x659aff47; 0 of 112
match.

Still open: the per-record payload fields, the meaning of the key, where the
interval values actually live, and the missing S17-S23 stage records.
This commit is contained in:
Sylpheed RE agent
2026-08-24 10:52:35 +00:00
parent bd9e12c091
commit 7482142abf
3 changed files with 169 additions and 5 deletions

View File

@@ -130,3 +130,16 @@ thing is indexed *by*.
🔴 **But nothing here shows spawn parameters yet.** The header is a spatial
partition and no more; the sections are unread. Treat this as the location of the
world's spatial data, not as the wave table.
## ✅ Update: `REGN` is a stage's **MapPath**
The per-stage definition record (see
[stage-definition-table.md](stage-definition-table.md)) has a field
`MapPath = test.rgn`, and `name_hash("test.rgn") = 0x3506e972`, which is one of
the `REGN` objects in `MiscBin.pak`. The sibling field `MapMesh = test.col`
hashes to `0x2cf7eb47`, an `MCOL` object in the same pak.
So `.rgn`/`REGN` is stage navigation/path data referenced by the stage record,
and `.col`/`MCOL` is the stage collision mesh. This does not by itself validate
either of the two refuted cell-payload readings recorded above, but it does
explain why the payload looks like a grid of route data.

View File

@@ -0,0 +1,137 @@
# Stage definition table and the squadron (`UnitGroup`) roster
Status: ✅ for the record vocabulary and the stage→table wiring;
🟡 for the `UnitGroup` record payload; ❔ for the arrival/interval schedule itself.
This is the answer to "where does the game say *which* enemies appear in a
mission, and when". It is data, not code, and it lives in the language paks.
## ✅ The per-stage definition record
`GP_MAIN_GAME_<lang>.pak` contains one IDXD object per stage. Dumped with
`sylpheed-cli pak dump`, a record reads (Stage 02, hash `0x90822a39`):
```
BackGroundPackage = game:\hidden\Resource3D\BG_Earth.xpr
StageResourcePackage = game:\hidden\Resource3D\Stage_S02.xpr
EnumerateLocalString = EnumLocalString_Test.tbl
EnumerateUnit = EnumUnit_Test.tbl
EnumerateWeapon = EnumWeapon_Test.tbl
EnumerateCharacter = EnumCharacter_Test.tbl
EnumerateNullFrame = Null_Test.tbl
EnumerateFormation = Formation_Test.tbl
EnumerateSquadron = EnumSquadron_Test.tbl
MessageSet = StageMessageSet_test.tbl
NamePlate = nameplate_test.tbl
CollisionMeshes = CollisionSet_test.bin
MotionEnumTable = game:\hidden\DefTables.pak+EnumGameModel_test.tbl
LodEnumTable = game:\hidden\DefTables.pak+EnumLODSet_test.tbl
MapMesh = test.col
MapPath = test.rgn
AsteroidDefinition = test_s8p1_asteroid.tbl
```
23 such records were found in `GP_MAIN_GAME_E.pak`, one per stage, plus one
`_Test`-suffixed Stage_S02 template (`0x7b2e67cd`):
| stage | squadron table | record hash |
|---|---|---|
| S01S09 | `UnitGroup_S01..S09.tbl` | `0x8f7c0139``0x97ad4939` |
| S10S16 | `UnitGroup_S10..S16.tbl` | `0x8f9efd2f``0x95c3f32f` |
| S24S29 | `UnitGroup_S24..S29.tbl` | `0x94e0c625``0x99ff9325` |
**Not settled:** S17S23 have no stage record in `GP_MAIN_GAME_E.pak`. They may
live in another pak, or be named differently. Not yet searched.
## ✅ `MapPath` / `MapMesh` name the MiscBin objects
Hashing the literal strings with the recovered `name_hash` resolves them:
| string | hash | found in |
|---|---|---|
| `test.rgn` | `0x3506e972` | `MiscBin.pak` |
| `test.col` | `0x2cf7eb47` | `MiscBin.pak` |
| `CollisionSet_test.bin` | `0x9ea1fff7` | `MiscBin.pak` |
| `EnumGameModel_test.tbl` | `0xefa15c41` | `DefTables.pak` |
This settles what the `REGN` blobs in `MiscBin.pak` are for: `.rgn` is the
stage's **MapPath**, not a generic grid — see [regn-map-grid.md](regn-map-grid.md).
`.col` (`MCOL`) is the **MapMesh**, i.e. stage collision.
The unqualified `UnitGroup_S02.tbl` does *not* resolve; the archive key is the
path-qualified form **`stage\UnitGroup_S02.tbl`** (`0x019fd129`), present in all
six `GP_MAIN_GAME_{D,E,F,I,J,S}.pak`.
## ✅ The squadron roster: `stage\UnitGroup_S02.tbl`
17898 bytes uncompressed. Its string table gives the complete field vocabulary
of one squadron record:
* `FormationID``Formation_2_Rhino1`, `Formation_TCAF_ArrowHead03_64`,
`Formation_ADAN_Turret07_30`, `Formation_1_only`. The trailing number tracks
squadron size (`_01`, `_02`, `_03`, `_12`, `_30`, `_32`, `_64`).
* `AIID``AI_TCAF_RhinoFlight`, `AI_TCAF_RhinoLeader`, `AI_TCAF_BirdFlight`,
`AI_TCAF_CraftSquadron`, `AI_TCAF_Fleet`, `AI_TCAF_Fleet_Escort`,
`AI_TCAF_Acropolis`, `AI_ADAN_CraftSquadron_{Rookie,Veteran}`,
`AI_ADAN_BomberSquadron_Rookie`, `AI_ADAN_Fleet`, `AI_ADAN_Frigates`,
`AI_Structure`, `AI_TraceRoute`.
* `SideID``TCAF`, `ADAN`, `Neutral`.
* `Count` → integer.
* `DisableInterval``Yes`/`No`.
* unit model, as `UN_<mesh>_<side>_<name>`, which matches the XBG7 mesh names we
already decode: `UN_f001_TCAF_DeltaSaber_T_Player`, `UN_f003_TCAF_ArrowHead`,
`UN_f105_TCAF_Cruiser`, `UN_f106_TCAF_Destroyer`, `UN_f201_TCAF_Tanker`,
`UN_e007_ADAN_Turret`, `UN_e010_ADAN_Attacker_S`, `UN_e105_ADAN_Cruiser`,
`UN_e106_ADAN_Destroyer`, `UN_e108_ADAN_ASFrigate`, `UN_e201_ADAN_ISCMissile`,
`UN_bf001_TCAF_SchlosBase`.
* `MessageSet_*` → radio chatter set, per named pilot (`MessageSet_Ellen`,
`_Raymond`, `_Gene`, `_Sandra`, `_Billy`, `_Antonius`, `_Carl`, `_Katana`) or
per faction group (`MessageSet_ADAN_plA/plB/plC/plBC/plAC`,
`MessageSet_TCAF_ShipA/B/C`, `MessageSet_TCAF_FleetAA`, `_Supply`).
* character id → `ELLEN`, `RAYMOND`, `GENE`, `SANDRA`, `BILLY`, `ANTONIUS`,
`CARL`, plus `Character_Player_Test`.
* nameplates for capital ships → `NP_Charon`, `NP_Caliban`, `NP_Amalthea`.
`DisableInterval` is the first direct evidence of the timing knob the mission
scheduler uses: squadrons are gated on an *interval*, and individual records can
opt out of it. That is consistent with the observed behaviour that a mission's
live enemy count rises and falls rather than being fixed.
## 🟡 Container layout
The file is a multi-section IDXD container, and `pak dump`'s current labels for
it are wrong — it prints the first section's key as "schema".
```
0x0000 "IDXD"
0x0004 u32 be = 0x70 = 112 record count
0x0008 112 x 16-byte entries: u32 key, u32 a, u32 b, u32 c
0x0708 next section: u32 tag (0x c5c9f110), 0,0, u32 count=0x6f, u32 size=0x46a,
then the SAME 112 keys again with a narrower payload
...
0x3c1d 112 squadron-ID strings: TCN001.. ADN101.. ADT102.. TCS151.. ADS151..
(prefixes ADN, ADS, ADT, TCN, TCS, TCT)
0x3f26 field-name / value string pool (the vocabulary above)
```
The 112×16 entry array was confirmed by boundary, not assumed: keys are strictly
increasing for exactly 112 entries and the run breaks at `0x708`, where the next
section header sits.
### ❌ Refuted: the record key is not the squadron-ID name hash
Tested directly — `name_hash("TCN001") = 0xd639f1a4`, but the first keys are
`0x659aff47, 0x659b0046, 0x669b0047, …`. **0 of 112** keys match any squadron
ID's hash. The keys are something else; their byte structure (`b0` ramping,
`b2` taking small signed values `ff,00,01,02,03`) suggests a packed tuple rather
than a hash, but that is untested.
## What is still open
* The per-record payload: which of the three words per entry selects the
formation, the AI, the side, the count.
* The meaning of the record key.
* **The interval schedule itself.** `DisableInterval` names the mechanism but
the interval *values* have not been located — they may be in the second
section, in `Formation_*.tbl`, or in `EnumSquadron_Test.tbl`.
* S17S23 stage records.