Files
Sylpheed/docs/re/structures/mission-script-ssb.md
Sylpheed RE agent c67b0585e8 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.
2026-08-25 12:12:59 +00:00

6.2 KiB
Raw Blame History

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 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.pakname_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 — 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 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.