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.
135 lines
6.2 KiB
Markdown
135 lines
6.2 KiB
Markdown
# `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<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.
|