This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-builtins.md
Sylpheed RE agent 70cff9ca21 re: the 147 ISL built-ins characterised; Stage 02 gates on units, not counts
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.
2026-08-25 12:36:02 +00:00

121 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](isl-bytecode.md) (the instruction encoding) and
[mission-phase-advance](../mission-phase-advance.md) (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, 0x410x44, 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](isl-bytecode.md) 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](mission-script-ssb.md).
## ✅ 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]` |
| **132134** | player gauges | speed/boost ratios and a player byte |
| **73, 123127** | 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 calls `823011B0` (initial, packed
`hi<<16|lo`) and `82301118` (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.py` prints a bare
`builtinN` for anything unread rather than guessing.