diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 070014e..94ff47d 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -827,6 +827,35 @@ unknown, what evidence exists, and what the first step would be. Move an item in field; the stat keys in records not opened here (`MegaTons`, `BulletLimit`, `CrewCount`, `HP_CLASS`); why the roster lists 59 of 131. +* ✅✅ **(2026-08-27) THE DISC'S STAGE NUMBERS ARE NOT 1..28 — S01-S16, S17 + ABSENT, S18-S23 the tutorials, S24-S29 — and the player's craft per stage + falls out with it.** + [structures/stage-numbering-and-player-craft](structures/stage-numbering-and-player-craft.md), + artefact `data/stage-numbering.txt`, regenerator + `tools/re-capture/stage_numbering.py`. + The corpus knew "28 stages" and "six tutorials with no `AIParams`"; it did not + know the NUMBERS. Hashing `stage\UnitGroup_S%02d.tbl` for N=0..39: + **1-16, 18-29 ship, 17 does not** — 28 files, and the six with no + `AIParams_SNN.tbl` are exactly **18-23**. So 22 story stages + 6 tutorials. + 🔑 `eng\GP_HANGAR_ARSENAL_3D.tbl`'s **`ResourceID`** record keys a player-craft + mesh by stage: `Unit_St1_6` -> `rou_f001` (DeltaSaber **T**), `Unit_St7_16` -> + `rou_f002` (**W**), and **six further fields tagged with the raw numbers + 24,25,26,27,28,29** -> `rou_f002` x4, **`rou_f004` (DeltaSaber A) at 28**, + `rou_f002`. The bare tags ARE the last six story-stage numbers. + 🧪 Control in the same record: `tag_hash(name) == tag` for **11/11** named + fields, so those six genuinely carry no name. + 🧪 **Cross-check from a different file** — grouping the Arsenal pak's 168 + stage-scoped entries by which `_Player` craft their loadout mounts gives + **f001 = 6, f002 = 15, f004 = 1, tutorials 5+1 = 6**, and every number closes + against `ResourceID`: 6 = `Unit_St1_6`; 15 = `Unit_St7_16` (10) + tags + 24,25,26,27,29 (5); 1 = tag 28; 6+15+1+6 = **28**. + 🔑 **The player flies the DeltaSaber A in exactly one mission, S28**, and the + DeltaSaber T only for the first six. + 🟡 Not settled: the Arsenal entry FILENAMES, so which of the 168 is S24 vs S25 + is constrained but not pinned (`name_hash` probing over 8 templates x 40 + indices x 6 languages resolved 0; controls `\weapon.tbl`, + `\strings.tbl`, `\GP_HANGAR_ARSENAL_3D.tbl` all resolve). + * ✅ **(2026-08-27) WHO SELECTS A LOADOUT: THE PAK ENTRY DOES — the Arsenal pak is STAGE-SCOPED, 168 entries = 28 stages x 6 languages.** Also a **correction to the entry below**. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index a15c263..1bf1094 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -99,6 +99,7 @@ files, which is how the same ground got covered twice. | [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files | | [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways | | [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer — two sequential countdowns, not a limit and a warning | ✅ CONFIRMED — read from `sub_822639B8`; the threshold reading is | +| [`structures/stage-numbering-and-player-craft.md`](structures/stage-numbering-and-player-craft.md) | The disc's stage numbering (S01–S16, S18–S29) and the player's craft per stage | ✅✅ CONFIRMED — two independent files close on 6 + 15 + 1 + 6 = 28 | | [`structures/hangar-loadout-system.md`](structures/hangar-loadout-system.md) | The Hangar loadout system — loadout → per-slot allow-list → arsenal item | ✅ CONFIRMED — 60/60 slot refs, 70/88 item refs (18 = the `No_Equipment` sentinel) | | [`structures/arsenal-item-weapon-chain.md`](structures/arsenal-item-weapon-chain.md) | The 59-of-131 arsenal question — an item names a hardpoint slot, not a weapon | ✅✅ CONFIRMED — 0/59 `PlayerWeapon` are a `Weapon.ID`, 59/59 are a `Turret_NNN` slot | | [`structures/unit-substructure-records.md`](structures/unit-substructure-records.md) | The destructible-subsystem model — a unit's `Turret`/`Bridge`/`Hatch`/`ShieldGenerator`/`Thruster` sub-records | ✅ CONFIRMED — 835 turrets, 835/835 `WeaponID` resolve, `StructureCount` control 667/684 | diff --git a/docs/re/data/stage-numbering.txt b/docs/re/data/stage-numbering.txt new file mode 100644 index 0000000..dddd10a --- /dev/null +++ b/docs/re/data/stage-numbering.txt @@ -0,0 +1,28 @@ +# The disc's stage numbering, and the player's craft per stage +# Regenerate: python3 tools/re-capture/stage_numbering.py +# See docs/re/structures/stage-numbering-and-player-craft.md + +## which stage numbers actually ship + stage\UnitGroup_SNN.tbl N = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] + count 28 ; gap inside the range: [17] + stage\AIParams_SNN.tbl N = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 24, 25, 26, 27, 28, 29] (count 22) + stages with NO AIParams (the tutorials): [18, 19, 20, 21, 22, 23] + +## ResourceID -- the player craft, keyed by stage + tag 24 name rou_f002 + tag 25 name rou_f002 + tag 26 name rou_f002 + tag 27 name rou_f002 + tag 28 name rou_f004 + tag 29 name rou_f002 + tag 2354614773 name Unit_St1_6 rou_f001 + tag 3281124436 name Unit_St7_16 rou_f002 + CONTROL tag_hash(name) == tag for the named fields: 11/11 + +## CROSS-CHECK the same thing from the Arsenal entries (168 = 28 stages x 6 languages) + 90 entries = 15 stages UN_f002_TCAF_DeltaSaber_W_Player + 36 entries = 6 stages UN_f001_TCAF_DeltaSaber_T_Player + 30 entries = 5 stages UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 + 6 entries = 1 stages UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 + 6 entries = 1 stages UN_f004_TCAF_DeltaSaber_A_Player + total 28 stages diff --git a/docs/re/structures/stage-numbering-and-player-craft.md b/docs/re/structures/stage-numbering-and-player-craft.md new file mode 100644 index 0000000..d030e89 --- /dev/null +++ b/docs/re/structures/stage-numbering-and-player-craft.md @@ -0,0 +1,74 @@ +# ✅✅ The disc's stage NUMBERING, and which craft the player flies in each + +The corpus has known "the disc has 28 stages" and "the six tutorials have no +`AIParams`" for a while. **It did not know the stage NUMBERS.** They are not +1…28: + +``` +S01 … S16 story (16) +S17 ABSENT +S18 … S23 the tutorials (6) -- exactly the six with no AIParams_SNN.tbl +S24 … S29 story (6) + == 28 shipped, 22 of them story +``` + +Measured by hashing `stage\UnitGroup_S%02d.tbl` and `stage\AIParams_S%02d.tbl` +for N = 0…39 against `GP_MAIN_GAME_E.pak`. Artefact `../data/stage-numbering.txt`, +regenerator `tools/re-capture/stage_numbering.py`. + +## The player's craft, per stage + +`eng\GP_HANGAR_ARSENAL_3D.tbl` holds a `ResourceID` record whose `Unit_St*` +fields key a player-craft mesh by stage — and whose **remaining entries are +tagged with the raw stage NUMBER instead of a hashed name**: + +| tag | field name | mesh | +|---|---|---| +| `tag_hash("Unit_St1_6")` | `Unit_St1_6` | `rou_f001` — DeltaSaber **T** | +| `tag_hash("Unit_St7_16")` | `Unit_St7_16` | `rou_f002` — DeltaSaber **W** | +| **24** | — | `rou_f002` | +| **25** | — | `rou_f002` | +| **26** | — | `rou_f002` | +| **27** | — | `rou_f002` | +| **28** | — | **`rou_f004` — DeltaSaber A** | +| **29** | — | `rou_f002` | + +🔑 The six bare tags are `24 … 29`, and the disc's last six story stages are +**S24 … S29**. That is not a coincidence available to a guess: the numbering was +recovered independently, from the shipped filenames. **Control in the same +record: `tag_hash(name) == tag` for 11/11 of the named fields**, so the reader is +right and these six genuinely carry no name. + +## 🧪 The cross-check — the Arsenal entries say the same thing + +[The Arsenal pak is stage-scoped](hangar-loadout-system.md): 168 entries = +28 stages × 6 languages. Grouping them by which `_Player` craft their loadout +mounts: + +| stages | player craft | +|---:|---| +| **6** | `UN_f001_TCAF_DeltaSaber_T_Player` | +| **15** | `UN_f002_TCAF_DeltaSaber_W_Player` | +| **1** | `UN_f004_TCAF_DeltaSaber_A_Player` | +| 5 + 1 | `…_T_Player_Ttrl1` / `_Ttrl2` — **6 tutorials** | + +Every number closes against `ResourceID`, from a different file: + +* **f001 = 6** ⇔ `Unit_St1_6`. +* **f002 = 15** ⇔ `Unit_St7_16` (10 stages) **+** tags 24, 25, 26, 27, 29 (5) = 15. +* **f004 = 1** ⇔ tag 28 alone. +* **tutorials = 5 + 1 = 6** ⇔ the six stages with no `AIParams`. +* 6 + 15 + 1 + 6 = **28**. + +🔑 So **the player flies the DeltaSaber A in exactly one mission — S28** — and +the DeltaSaber T only for the first six. + +## 🟡 Not settled + +* The Arsenal entry FILENAMES are still unrecovered, so *which* of the 168 + entries is S24 vs S25 is not directly readable — the craft grouping constrains + it but does not pin it. `name_hash` probing over 8 templates × 40 indices × 6 + languages resolved 0 (controls `\weapon.tbl`, `\strings.tbl`, + `\GP_HANGAR_ARSENAL_3D.tbl` all resolve, so the method works). +* Why the numbering skips S17 and parks the tutorials at S18–S23 is a production + fact this cannot reach. diff --git a/tools/re-capture/stage_numbering.py b/tools/re-capture/stage_numbering.py new file mode 100644 index 0000000..f5ee209 --- /dev/null +++ b/tools/re-capture/stage_numbering.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""The disc's stage NUMBERING, and which craft the player flies in each. + +`eng\\GP_HANGAR_ARSENAL_3D.tbl`'s `ResourceID` record maps stage ranges to a +player-craft mesh; the Arsenal pak's 168 stage-scoped entries say the same thing +from the other side. Regenerates docs/re/data/stage-numbering.txt. +""" +import sys, os, glob, collections + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import unitgroup as U +from unit_substructures import pak_entries + +def main(): + mg = glob.glob('/work/sylph_extract/**/GP_MAIN_GAME_E.pak', recursive=True)[0] + ars = glob.glob('/work/sylph_extract/**/GP_HANGAR_ARSENAL.pak', recursive=True)[0] + H = {h for h, b in pak_entries(mg)} + E = {h: b for h, b in pak_entries(ars)} + + print("# The disc's stage numbering, and the player's craft per stage") + print("# Regenerate: python3 tools/re-capture/stage_numbering.py") + print("# See docs/re/structures/stage-numbering-and-player-craft.md") + + ug = [i for i in range(0, 40) if U.name_hash('stage\\UnitGroup_S%02d.tbl' % i) in H] + ai = [i for i in range(0, 40) if U.name_hash('stage\\AIParams_S%02d.tbl' % i) in H] + print("\n## which stage numbers actually ship") + print(" stage\\UnitGroup_SNN.tbl N = %s" % ug) + print(" count %d ; gap inside the range: %s" + % (len(ug), [i for i in range(min(ug), max(ug) + 1) if i not in ug])) + print(" stage\\AIParams_SNN.tbl N = %s (count %d)" % (ai, len(ai))) + print(" stages with NO AIParams (the tutorials): %s" % [i for i in ug if i not in ai]) + + recs = U.parse(E[U.name_hash('eng\\GP_HANGAR_ARSENAL_3D.tbl')]) + rid = [r for r in recs if r['squadron'] == 'ResourceID'][0] + print("\n## ResourceID -- the player craft, keyed by stage") + for t, n, v in rid['fields']: + if n and not n.startswith('Unit_St'): continue + print(" tag %-10s name %-14s %s" % (t, n if n else '', v)) + print(" CONTROL tag_hash(name) == tag for the named fields: %d/%d" + % (sum(1 for t, n, v in rid['fields'] if n and U.tag_hash(n) == t), + sum(1 for t, n, v in rid['fields'] if n))) + + idxd = [(h, U.parse(b)) for h, b in pak_entries(ars) if b[:4] == b'IDXD'] + withu = [(h, r) for h, r in idxd if any(x['squadron'] == 'UNITS' for x in r)] + pc = collections.Counter() + for h, rs in withu: + ids = {U.named(r).get('UnitID', '') for r in rs if 'PlayerUnit' in U.named(r)} + pc[tuple(sorted(ids))] += 1 + print("\n## CROSS-CHECK the same thing from the Arsenal entries (%d = 28 stages x 6 languages)" + % len(withu)) + for t, c in pc.most_common(): + print(" %3d entries = %2d stages %s" % (c, c // 6, ", ".join(t))) + print(" total %d stages" % sum(c // 6 for c in pc.values())) + +if __name__ == "__main__": + main()