re: the mission scripts found -- Stage\StageNN.ssb in GP_MAIN_GAME_<L>.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<N>* 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.
This commit is contained in:
Sylpheed RE agent
2026-08-25 12:12:59 +00:00
parent 351ba37c19
commit 66a8a5de2d

View File

@@ -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: S01S16, S18S29.
**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<N>*`: 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 <save-regs>`). 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_<L>.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.