# The ISL script bytecode — instruction encoding decoded Status: ✅ the encoding, the 25-opcode table and the call form, read off the interpreter and confirmed by disassembling `Stage02.ssb`; 🟡 most opcode *semantics* are named only by their handler; ❔ the 147 built-ins are not yet characterised. Follows [mission-script-ssb](mission-script-ssb.md) (where the scripts live) and [mission-phase-advance](../mission-phase-advance.md) (why they matter). Tool: `tools/re-capture/isl.py`. ## ✅ Encoding `ScriptPhase::Update` (`sub_82263408`) fetches one **big-endian u32** per instruction: ``` 0x822635D4 lwz r11,0(r31) ; the instruction word 0x822635D8 clrlwi r4,r11,24 ; OPCODE = the LOW byte 0x822635DC cmplwi 0x18 ; 25 opcodes 0x822635FC jump table, 25 absolute VAs ``` Every handler advances the pc with `lbz r11,2(r31); add r31,r11,r31`, so: | byte | 0 | 1 | 2 | 3 | |---|---|---|---|---| | meaning | operand kind A | operand kind B | **instruction length in bytes** | **opcode** | Operand words follow. The common form is 12 bytes (opcode word + two operands). Operand kinds go through resolvers with their own 4-entry table (`sub_82271D40` for integers, `cmplwi 0x3`), so there are **4 operand kinds**. ## ✅ The opcode table | op | handler | what the handler does | |---|---|---| | 0 | `82263660` | integer assign — resolve rvalue (`82271D40`, kind byte[0], word@+8), resolve lvalue (`82272030`, kind byte[1], word@+4), `stw` | | 1 | `8226369C` | float assign — same shape with `82271F10`/`82272120` and `stfd` | | 2,4,6,8 | `822636D0` | → `822713E8` (a compare/branch family; four opcodes share one handler) | | 3,5,7,9 | `822636E4` | → `822714D0` (the sibling family) | | 10 | `822636F8` | → `82271598` | | 11 | `8226370C` | → `822716E0` | | **12** | `82263720` | **JUMP** — `r31 = [phase+232] + word@+4` | | 13–18 | `82263738`… | → `82271830`, `822718C8`, `82271960`, `822719F8`, `82271AC8`, `82271B60` | | **19** | `822637B0` | **CALL BUILT-IN** → `sub_82272220` | | 20 | `82263874` | `li r29,1` then the suspend path — **yield / return** | | 21 | `822637C4` | `sub_82175C20(phase+44, phase+168)` | | 22 | `822637E4` | `sub_82274BA0(phase+64, phase+184)` | | 23,24 | `82263804`… | → `82271C30`, `82271CB8` | Handler return codes drive the outer loop at `0x82263828`: **0** continue, **1** suspend, **2**/**3** other exits. ### ✅ Jump operands are code-base-relative Op 12 adds its operand to `[phase+232]`, the code base — i.e. the `.ssb` header's code offset (`0x24` in every file). That settles, for this opcode, the question `mission-script-ssb.md` left open about whether offsets are file- or code-base-relative. ### ✅ The call form, and a statement counter `sub_82272220` reads the **built-in id from word@+4** (`cmplwi 0x92` → 147 built-ins, table `0x8227226C`) and stores **word@+8** into `[phase+200]`. That second word turns out to be a **monotonically increasing statement id** — `0x245, 0x248, 0x24A, 0x24B, 0x24C, 0x24D, 0x24E, 0x252…` along a routine. It is a source-position counter, presumably for the script's own error traces. ## ✅ It decodes — Stage 02 Disassembling from the code base runs cleanly, and routines terminate on `ret` (op 20) exactly where expected. Data in `data/isl-stage02.txt`. Scanning the whole code region for the call encoding: ``` 2846 call sites, 73 distinct built-ins used (of 147) most used: 11 (×372), 69 (×255), 1 (×216), 64 (×213), 30 (×179), 20 (×167) ``` **The phase-control built-ins, located:** | built-in | meaning | sites in Stage02 | |---|---|---| | **6** | end phase (`[ScriptPhase+196] = 1`) | **12** | | **62** | force-end, skipping the end event | **3** | | **39** | mark last phase (`[phase+300] = 2`) | **8** | | 40 | `[phase+300] = 1` | 4 | Twelve end-phase sites across three phases — so a phase has several exit paths, which is what a mission with win *and* lose branches should look like. Argument passing is visible in the disassembly: pairs of `set.i k=01,02 <0> ` / `set.i k=02,03 <0>` stage arguments into slots, then `call`. Floats are staged the same way — e.g. `40080000` = 3.0 immediately before several calls. ## ✅ The four operand kinds, and how arguments are passed Resolver table `0x82271D74`, four entries: | kind | code | meaning | |---|---|---| | 0 | `lis 0x828E` / `bl 82454A40` / `lwzx` | **global[i]** — indexed global array | | 1 | `mr r3,r31` | **immediate** — the operand word itself | | 2 | `[phase+164]` if `i==0` else `[phase+168]` | **special[i]** — two scratch registers | | 3 | `addi r3,r3,20` / `lwzx` | **local[i]** — `[phase+20 + i]` | Byte[0] is the rvalue's kind (operand word@+8) and byte[1] the lvalue's (word@+4). That turns the recurring pair into something readable: ``` set.i k=01,02 special[A] = V (immediate -> special) set.i k=02,03 <0> local[B] = special[0] ``` — i.e. **argument staging**. Values land in `local[]` at byte offsets 0, 4, 8, 0xC…, and the following `call` consumes them; a built-in's arguments are not in its own instruction. `isl.py` now tracks the staging and prints them. ⚠️ **Immediates in `set.f` are DOUBLES**, carried as two words — op 1 stores with `stfd`. Reading only the high word as a *float* gives `2.125` where the script means **3.0**, which is exactly the sort of plausible-but-wrong number that would have been believed. The 16-byte `set.f` form is `high, low`. With that, the run-up to the first `END PHASE` in Stage 02 reads: ``` 0050F4 builtin=64(0x42, 0x2, 0x1, 0x9, 0x1, -1) 005160 builtin=120 005188 builtin=59(3) 0051B0 builtin=85(3) 0051D8 builtin=4(3) 0051E4 builtin=6 <-- end phase 0051F0 builtin=11 ``` Three separate built-ins taking `3` immediately before the phase ends — a plausible "wait 3 seconds" family, **unconfirmed** until the built-in table is read. ## ❔ What this does not settle * **The 147 built-ins are uncharacterised.** Without them the disassembly is structure without meaning: we can see *that* a phase ends here, not *what was tested*. That is the remaining step to per-phase clear conditions. * Opcodes 2–11 and 13–18 are named only by handler address. The four-way sharing (2/4/6/8 and 3/5/7/9) suggests the handler re-reads the opcode to pick a comparison or a type, but that is not yet read. * The four-way opcode sharing (2/4/6/8 and 3/5/7/9) suggests the handler re-reads the opcode to pick a comparison or a type; not yet read. * The mission-level stream at `+0x24` of a `.ssb` — as opposed to this ISL stream — is still only partly read.