Table at 0x8227226C is 147 big-endian absolute VAs (verified structurally: it ends exactly where the first handler begins, all targets inside sub_82272220). Arguments are not in the instruction -- every handler does c_str() on [phase+20], a packed blob, which is what the local[] staging fills. Return 2 = yield; five built-ins block by skipping the pc advance. Recovered the ScriptPhase state layout: 32-entry float and flag register files, int/double result registers, the timer block, and the runtime unit array at +324 indexed by symbol-table-2 index -- a direct hook from bytecode call sites to the two .ssb symbol tables. Spot-checked two claims against the disassembly rather than trusting them: id 4 loads a DOUBLE into the thread countdown and returns 2 (wait_s), and id 24 reads current/initial squadron member counts (squad_survival_pct). Both exact. Counting Stage02.ssb: unit_state 255, hp_pct_test 167, dist_lt 92, unit_alive 71, unit_relation 52 -- and squad_survival_pct, group_ratio_pct and the two global counters are NOT called at all. So Stage 02's phases are gated on named units (destroyed / HP / proximity), never on an aggregate count, even though the kill-counter primitives exist in the VM. That answers the standing 'next wave after N kills or after an event?' question for this stage: specific units, not a number. isl.py now names the built-ins, so the run-up to the first END_PHASE reads wait_cmds_drained / fade_sound(3) / builtin85(3) / wait_s(3) / END_PHASE. Not settled: 3 handlers unresolved (55, 75, 105); the 1024-slot interpreter command table is only partly recovered.
5.7 KiB
The 147 ISL built-ins
Status: ✅ table encoding, calling convention and the ScriptPhase state layout;
✅ ~135 of 147 handlers characterised from the disassembly; 🟡 three resolved
only partially; ❔ the interpreter-command table is only partly recovered.
Companion to isl-bytecode (the instruction encoding) and mission-phase-advance (why phases hinge on these).
✅ Table and calling convention
0x8227226C … 0x822724B7 is 147 big-endian absolute VAs — no base-relative
offsets. Verified structurally: the table starts immediately after the bctr at
0x82272268, 0x8227226C + 147·4 = 0x822724B8 is exactly where the first
handler begins, and every target lies inside sub_82272220.
Six ids are unused defaults (0, 0x41–0x44, 0x7A) and about ten more are deliberate stubs returning a constant.
Arguments do not live in the instruction. Every handler starts
addi r3,r31,20 ; bl 0x82454A40 — std::string::c_str() — so [phase+20] is a
packed operand blob, which is what the local[] staging in
isl-bytecode fills.
Return codes: 0 continue, 2 yield (re-execute next frame), 3 coroutine control. Five built-ins skip the pc advance on 2 and so genuinely block: 97, 120, 137, 142, 143.
✅ ScriptPhase state layout
| offset | meaning |
|---|---|
+88 |
32-entry float register file |
+120 |
32-entry flag register file |
+160 |
frame-wait counter |
+164 / +176 |
int / double result register |
+196 |
phase-finished flag |
+232 / +236 |
code base / end-event offset |
+244 |
symbol table 1 base (route + message names) |
+272 |
trigger queue |
+300 |
1 = not last phase, 2 = last |
+304…+320 |
mission timer (elapsed, t0, limit, running, enabled) |
+324 |
runtime unit array, indexed by symbol table 2 index |
Per-unit record: +4 live object (NULL = absent), +16 state (2 = active;
1/3/4 = gone/dead/invalid), +32/40/48 position, +128/132 HP / max HP,
+140 flag bitmask.
That is the hook into the data: blob fields indexing [phase+244] are
symtab-1 indices and fields indexing [phase+324] are symtab-2 indices — the
two tables already parsed in mission-script-ssb.
✅ The conditions a phase can test
| id | name | what it tests |
|---|---|---|
| 6 / 62 | END_PHASE / FORCE_END_PHASE |
sets [+196], with / without the end event |
| 39 / 40 | MARK_LAST_PHASE / mark_not_last |
[+300] = 2 / 1 |
| 69 / 70 | unit_state / unit_alive |
a named unit's lifecycle state; state == 2 |
| 20 / 95 | hp_pct_test / unit_hp_pct |
unit HP as a percentage of max |
| 18 | dist_lt |
3-D distance between two named units below a threshold |
| 24 / 72 | squad_survival_pct / group_ratio_pct |
current ÷ initial squadron members × 100 |
| 56 / 94 | unit_relation / is_engaged |
relation between units; is anything engaging this one |
| 33 / 34 | global_counter0/1 |
two global counters read straight into [+164] |
| 132–134 | player gauges | speed/boost ratios and a player byte |
| 73, 123–127 | timer family | start / resume / stop / reset / read elapsed / read limit |
| 8 / 9 / 93 | set_flag / read_freg / clear_flag |
latch a result into the 32-entry files |
| 100 / 115 | push_trigger / named_event |
the engine→script edge |
The state machine is therefore: a trigger fires a coroutine → the coroutine
tests one of the predicates → it latches the answer with set_flag → some later
thread reaches END_PHASE.
Two spot-checks I ran against the disassembly rather than taking on trust:
- id 4 (
wait_s) —c_str(),li r3,2(yield),lfd f0,0(r11),stfd f0,8(r30): a double seconds value into the thread countdown. Exactly as described. - id 24 (
squad_survival_pct) — indexes[phase+324]by[arg+4], rejects a NULL object and state 1, then calls823011B0(initial, packedhi<<16|lo) and82301118(current). Exactly as described.
✅ What Stage 02 actually uses — and it settles a standing question
Counting call sites in Stage02.ssb (data/isl-stage02.txt):
| built-in | sites |
|---|---|
unit_state |
255 |
hp_pct_test |
167 |
dist_lt |
92 |
unit_alive |
71 |
unit_relation |
52 |
set_flag / clear_flag / push_trigger |
12 each |
END_PHASE / MARK_LAST_PHASE / FORCE_END_PHASE |
12 / 8 / 3 |
Not used at all in Stage 02: squad_survival_pct, group_ratio_pct,
global_counter0/1, is_engaged, player_gauge*, prompt_yes_no,
deploy_and_wait.
🔑 So Stage 02's phases are gated on named-unit tests — destroyed / HP / proximity — and not on any aggregate count. The kill-counter primitives exist in the VM (33, 34) and this mission never calls them.
That is a direct answer to the standing "does the next wave start after N kills or after an event?" question, at least for Stage 02: specific units, not a number. "Certain objectives shot down" is right; "a certain number shot down" is not.
⚠️ Scoped to Stage 02. Other stages may well use squad_survival_pct — the
counting is per-file and cheap to repeat.
🟡 Not settled
- Three handlers resisted: id 55 (
vt35, 411 instructions, returns a float), id 75 (vt52, message/HUD-ish), id 105 (vt73, meaning of unit field+600). - The 1024-slot interpreter-command table is only partly recovered — 57
slots, by simulating the constant/stack dataflow of
sub_822FE040. - Names here are from handler behaviour, not from symbols;
isl.pyprints a barebuiltinNfor anything unread rather than guessing.