From c67b0585e8160a52511d9ad08c97660e47fac0ef Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 12:12:59 +0000 Subject: [PATCH] re: the mission scripts found -- Stage\StageNN.ssb in GP_MAIN_GAME_.pak mission-phase-advance.md recorded the bytecode as not on the disc. It is; the earlier grep failed because every pak entry is Z1+zlib, so no name is plaintext. The loader resolves table KEYS, not a filename: GamePart name -> GP_SCRIPT -> script.tbl (name_hash 0x75FE4656), whose SCRIPTS record is a 40-field manifest mapping MISSION1..MISSION29 to StageNN.ssb. Verified: Stage02.ssb is 226,596 bytes, md5 aff69b5a..., byte-identical across all six language paks. 28 scripts exist; S17 is the missing one -- which agrees with the table sweep (S17 in none of the five families) AND with the loader guard sub_8225EC78 'if (n == 16 || n > 32) return', proving that mission number is 0-based. Three independent routes to the same conclusion. Header decoded from ScriptMission::Load rather than guessed: version, code offset 0x24, two symbol tables. Stage02 symtab1 = 326 symbols (197 message ids, 119 route names, 10 subobjectives), symtab2 = 122 (111 unit ids, 11 characters). Two loops close: the 111 unit ids are exactly the 111 UnitGroup_S02 squadrons, and the route names appear as SCRIPT SYMBOLS -- which is why nothing in the executable parses Route_*_p* names. The _pN convention is the script's. Refuted: the .embsec_ sections are code (32,368 instructions, 108 functions), not script; MiscBin.pak and DefTables.pak have zero hits. Open: the 25 ISL opcodes and 147 built-ins are not decoded, and the mission-level stream at +0x24 is only partly read. --- docs/re/structures/mission-script-ssb.md | 134 +++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/re/structures/mission-script-ssb.md diff --git a/docs/re/structures/mission-script-ssb.md b/docs/re/structures/mission-script-ssb.md new file mode 100644 index 0000000..a37b850 --- /dev/null +++ b/docs/re/structures/mission-script-ssb.md @@ -0,0 +1,134 @@ +# `Stage\StageNN.ssb` — the mission scripts, found + +Status: ✅ located, container header and both symbol tables decoded, verified +against the loader's own disassembly; 🟡 the mission-level instruction stream is +only partly read; ❔ the 25 ISL opcodes and 147 built-ins are not yet decoded. + +[mission-phase-advance](../mission-phase-advance.md) established that a phase +ends when its **script** says so, and recorded the bytecode as "not on the disc +under any obvious name". **It is on the disc** — the earlier search failed +because every pak entry is `Z1` + zlib, so no plaintext name is greppable. + +## ✅ How the loader names it — a table key, not a filename + +`sub_8225EE20` never opens a file. It resolves a chain of *table keys*: + +``` +0x8225F090 vtbl[1]() on the game-part object -> group index +0x8225F0AC index into 0x820A1630 (29 GamePart names) -> "GP_MAIN_GAME" [17] +0x8225F0D4 prefix "game:\" (0x820A1864) +0x8225F168 look up key GP_SCRIPT (0x820A8244) +0x8225F1B8 look up section SCRIPTS (0x820A823C); 0x824A9088 = fatal if absent +0x8225F1DC sub_824483D0 enumerates that section against the MISSION*/PRT name arrays +``` + +`dat/tables.pak` → `name_hash("eng\GP_MAIN_GAME.tbl")` carries +`GP_SCRIPT.TABLE = script.tbl`, and +`name_hash("Stage\script.tbl") = 0x75FE4656` is the manifest — an IDXD whose +`SCRIPTS` record has **40 fields**: + +``` +MISSION1 = Stage01.ssb … MISSION29 = Stage29.ssb +MISSION_{START,END,UPDATE,FAILED,RESTART}_PRT = pgmsg_*.prt +DIALOG_MESSAGE, DIALOG_LOCAL_STRING, FONT, FONT_SIZE, TEXT_POS, TEXT_LINES +``` + +Verified here: the record is present and reads exactly that. + +## ✅ 28 scripts, and the missing one is S17 — a three-way convergence + +`name_hash("Stage\\StageNN.ssb")` resolves for **28** stages: S01–S16, S18–S29. +**S17 is absent**, and that agrees with two entirely independent findings: + +* the table sweep in [stage-definition-table](stage-definition-table.md) — S17 + appears in none of the five per-stage table families; +* the loader's own guard, `sub_8225EC78`: `if (n == 16 || n > 32) return`. + Index 16 skipped ⇒ the mission number there is **0-based** ⇒ it is `MISSION17` + that is skipped. + +Three routes to the same conclusion, from data, from a hash lookup and from +control flow. `MISSION30..33` exist as literals in the executable but appear in +no manifest and on no disc. + +Stage 02, verified byte-for-byte: + +``` +dat/GP_MAIN_GAME_E.pak key 0x5C2D6E8B = name_hash("Stage\Stage02.ssb") +226,596 bytes decompressed md5 aff69b5ade4c3c061a132ee5b88ac5a1 +``` + +Byte-identical across all six language paks, so the scripts are not localised. + +## ✅ Container header (big-endian, 0x24 bytes) + +Field meanings taken from `ScriptMission::Load` (`sub_82260568`), not guessed: + +| off | Stage02 | meaning | +|---|---|---| +| `+0x00` | `00000002` | version, always 2 | +| `+0x04` | `00000000` | → `[ScriptMission+36]` (`0x822605A4`) | +| `+0x08` | `00000024` | code offset → `[+24]` code base, `[+28]` pc (`0x822605AC`) | +| `+0x0C` | `00034C80` | symbol table 1 → `[+44]` (`0x822605C0`) | +| `+0x10` | `00036D60` | symbol table 2 | +| `+0x14…0x23` | `FF×16` | four `-1` slots, patched at runtime | + +Both tables are `u32 count; u32 offsets[count]`, offsets relative to the word +after `count`, each record `u32 type; char name[]` NUL-padded to 4. A zero +offset is an unused slot. + +## ✅ The symbol tables tie the script to the data tables + +Stage 02, parsed here: + +| table | count | slots used | types | +|---|---|---|---| +| symtab1 | 359 | 326 | **6** ×197 message ids, **1** ×119 route names, **7** ×10 `SUBOBJ_*`/`eff_*` | +| symtab2 | 122 | 122 | **2** ×111 unit ids, **8** ×11 characters | + +Two things fall out, and both close loops elsewhere: + +* **Type 1 symbols are `Route_TCN001_p1F`-style names** — so the script holds the + route names *as symbols*. That reconciles the finding in + [mission-phase-advance](../mission-phase-advance.md) that *nothing in the + executable parses `Route_*_p*`: the parsing never happens because the + compiler already resolved them, and the script simply refers to the ones it + wants. The `_pN` convention is real, and it is the **script's**, not a + runtime parser's. +* **Type 2 is 111 unit ids** (`ADN101`, `TCN328`, `NP_Amalthea`) — exactly the + **111 squadron records** in `stage\UnitGroup_S02.tbl`. The script addresses + squadrons one-for-one. + +Phase-tagged message ids give the phase count directly: Stage 02 carries +`S02_P1_*`, `S02_P2_*` and `S02_P3_*` → **3 phases**, matching the stage record. +Stage 01 has two, Stage 29 has one. + +## 🔴 Refuted along the way + +* **The `.embsec_` sections are not the payload.** All seven (129,472 B total) + decode as **32,368 PPC instructions in 108 functions**, each starting with a + real prologue (`mflr r12; bl `). They are code — overlays/math — + not script. +* **`hidden/MiscBin.pak` does not hold it** — zero hits across its 40 entries, + and zero across `DefTables.pak`'s 1,465. +* Sweeping all 41 paks / 26,443 TOC entries, the manifest strings appear in + **exactly one entry per pak**, `0x75FE4656`, in the six `GP_MAIN_GAME_.pak`. + Nothing is DLC, generated, or in `$SystemUpdate`. + +## ❔ Still open: the instruction encoding + +* **ISL (the `ScriptPhase` VM) — dispatcher confirmed**, not decoded: + `0x822635D4` reads a big-endian u32 and takes the opcode from its **low byte**, + `cmplwi 0x18` ⇒ 25 opcodes, jump table `0x822635FC`. +* **The mission-level stream at `+0x24` is not pinned.** Its first 48 words form + three near-identical groups — one per Stage 02 phase — each ending in a pair of + offsets that land on regions where ≥80 % of low bytes are in `0..0x18`, i.e. + plausible ISL entry points, two per phase. But `1883`'s operand is *not* + uniformly a code pointer: two of them land on IEEE floats (`44 8E 80 00` = + 1140.0, `43 B4 00 00` = 360.0). Whether offsets are file- or code-base-relative + (±0x24) is unsettled. + +**The next step for the port** is decoding the 25 ISL opcodes and the 147 +built-ins (`0x8227226C`) against `Stage02.ssb`, looking for calls to built-ins +**6** (`0x82272540`) and **62** (`0x82272558`) — the two that set +`[ScriptPhase+196]` and so end a phase. That yields the actual per-phase clear +condition for every stage.