re: three ISL built-in names were wrong, including the most-used one
All re-read twice — the handler, and the thing it calls — because each had been named from its shape rather than its effect. * id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ; stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from the active list and returns it to the free list. It destroys the thread. 2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was. * id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the thread parked at the target pc, or itself if the target is its own pc. It waits for nothing. * id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger container and then frees every thread whose pc differs from the caller's — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage 02 sites all sit in the phase terminator, next to timer_stop, clear_flag(-1) and MARK_LAST_PHASE. One name recovered from the game's own text: opcode 992 prints "RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message (2683 sites). Return codes documented properly: 1 = restart the coroutine from its entry (previously not recorded at all), 3 = terminate. And the blocking set was wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block; its handler ends `b 0x822724F8`, so it always returns 0. Unit-operand resolution settled from DATA over all 28 stages rather than by reading 147 handlers: a slot qualifies only if every value is a valid symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most of the table — that last clause is what discriminates, since every small integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the table, and the resolver now declines rather than inventing a name. New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and 128 take type 8 at slot 12 in 100% of their sites. A downstream inference is withdrawn with it: the note reading the live trigger counter attributed it to "the script arming watches as it goes" via built-in 100. The measurement stands; the attribution does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
@@ -22,9 +22,28 @@ Arguments do **not** live in the instruction. Every handler starts
|
||||
**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.
|
||||
### ✅ Return codes — read off `ScriptPhase::Update` (`0x82263830`)
|
||||
|
||||
The dispatcher switches on `r3` exactly four ways:
|
||||
|
||||
| r3 | address | effect |
|
||||
|---|---|---|
|
||||
| **0** | `0x82263850` | continue to the next thread in the same frame |
|
||||
| **1** | `0x82263878` | `[thread+4] = [thread+0]` — **restart this coroutine from its entry** |
|
||||
| **2** | `0x82263888` | `[thread+4] = saved pc` — resume next frame |
|
||||
| **3** | `0x82263894` | `sub_8226EA20` erases the thread from the active list `[+216]`, then `sub_8226EAB8` returns it to the free list `[+204]` — **terminate this coroutine** |
|
||||
|
||||
Two tails do the pc bookkeeping: `0x822724F8` is `li r3,0` then advance;
|
||||
`0x822724FC` advances only, preserving `r3`. Advance is `pc += [insn+2]`, the
|
||||
length byte — the same field [isl-bytecode](isl-bytecode.md) decodes. So **every
|
||||
handler that ends `b 0x822724F0` returns 0**, and its only output is
|
||||
`[phase+164]`/`[phase+176]`.
|
||||
|
||||
❌ **CORRECTED — the blocking set was wrong in two places.** The blocking form is
|
||||
`bctrl ; cmpwi r3,2 ; bne 0x822724FC`, and it appears at **102, 120, 137, 142,
|
||||
143**. This file previously listed **97**, which does not block: its handler
|
||||
`0x8227313C` ends `b 0x822724F8`, so it *always* returns 0. And **102** was
|
||||
missing. All six handlers re-read to confirm.
|
||||
|
||||
## ✅ `ScriptPhase` state layout
|
||||
|
||||
@@ -65,7 +84,7 @@ two tables already parsed in [mission-script-ssb](mission-script-ssb.md).
|
||||
| **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 |
|
||||
| **100 / 115** | `reset_phase_threads` / `named_event` | ❌ 100 is **not** `push_trigger` — see below |
|
||||
|
||||
**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
|
||||
@@ -80,9 +99,63 @@ Two spot-checks I ran against the disassembly rather than taking on trust:
|
||||
a NULL object and state 1, then calls `823011B0` (initial, packed
|
||||
`hi<<16|lo`) and `82301118` (current). Exactly as described.
|
||||
|
||||
## ❌ Three built-in names WITHDRAWN
|
||||
|
||||
Each re-read twice — the handler, and the thing it calls — because all three had
|
||||
been named from their shape rather than their effect.
|
||||
|
||||
| id | was | **is** | evidence |
|
||||
|---|---|---|---|
|
||||
| **11** | `yield` | **`end_coroutine`** | `0x82272624` is `li r11,1 ; li r3,3 ; stw r11,164(r31)`. Return 3 **destroys the thread**. It is the single most-used built-in in the game — 2945 sites, 372 in Stage 02 alone — so this was the most load-bearing wrong name in the file. |
|
||||
| **5** | `await_label` | **`kill_coroutine(label)`** | `sub_82273B08` computes `target = [phase+232] + blob[0]`; if that equals the **caller's own** pc it returns 3 (kill self), otherwise it finds the thread parked at `target` in `[phase+220]` and moves it to the free list. It does not wait for anything. |
|
||||
| **100** | `push_trigger` | **`reset_phase_threads`** | the handler calls vtable slot 2 (clears the trigger container at `[phase+272]`) and then `sub_82273BE8`, which walks `[phase+220]` and frees **every thread whose pc differs from the caller's**. It drops queued triggers and terminates every *other* coroutine — the opposite of pushing one. |
|
||||
|
||||
One name is newly **recovered**, from the game's own text: interpreter opcode 992
|
||||
prints `★RequestScriptMessage %s` (`0x820A5700`), so **id 64 is
|
||||
`request_script_message`** — 2683 sites, and the second most-used built-in.
|
||||
|
||||
## ✅ Which operands are unit indices — settled from the data
|
||||
|
||||
`tools/re-capture/isl.py` resolved a symbol-table-2 name only for 11 built-ins,
|
||||
at slot 4. The real set is much larger, and it was established by **measurement
|
||||
over all 28 stages** rather than by reading 147 handlers:
|
||||
|
||||
> a slot qualifies only if every observed value is a valid symtab-2 index, it
|
||||
> takes ≥15 distinct values, **and its maximum reaches most of the table**.
|
||||
|
||||
That last clause is what makes the test work. Symbol table 2 tops out at 122
|
||||
entries, so a slot carrying something else overruns it; plain range-checking
|
||||
cannot separate an index from a bool, because every small integer is in range.
|
||||
|
||||
* **unit index at slot 4** — 2, 3, 7, 12, 15, 16, 18, 19, 20, 24, 25, 26, 28,
|
||||
29, 30, 47, 48, 56, 57, 58, 63, 69, 70, 79, 91, 92, 95, 105, 108, 128, 143
|
||||
* **a second at slot 12** — 2, 18, 47, 48, 56, 79, 95, 128
|
||||
* **a third at slot 20** — 128
|
||||
|
||||
Every one of these is 100.0 % in range across its call sites (the largest, id 20,
|
||||
over 2930 of them).
|
||||
|
||||
The test also **refuted** a tempting entry: `set_flag`'s slot 0 passes the range
|
||||
and spread checks but its maximum *exceeds* the table — flag indices run 0..31
|
||||
against symbol tables as small as 40 — so it is excluded, and the disassembler
|
||||
now declines to resolve it rather than printing an invented name.
|
||||
|
||||
❔ **New, unexplained:** symbol table 2 holds **two types**, 2 (1160 entries
|
||||
disc-wide) and 8 (249), and they are not interchangeable. Built-ins **95** and
|
||||
**128** take a type-2 unit at slot 4 and, at slot 12, an operand that is type 8
|
||||
in **100 %** of its 90 and 152 call sites. What separates the two classes is not
|
||||
established.
|
||||
|
||||
## ✅ What Stage 02 actually uses — and it settles a standing question
|
||||
|
||||
Counting call sites in `Stage02.ssb` (`data/isl-stage02.txt`):
|
||||
Counting call sites in `Stage02.ssb` (`data/isl-stage02.txt`, regenerated by
|
||||
`tools/re-capture/isl_report.py calls`):
|
||||
|
||||
⚠️ The sibling artefact `data/isl-stage02-conditions.txt` **predates the name
|
||||
corrections above** — it still prints `yield`, `await_label` and `push_trigger`,
|
||||
and its operand rendering predates the staging fix. It has no committed
|
||||
generator; reproducing it needs the coroutine entry points, which
|
||||
`start_coroutine`'s operand carries and the tool does not yet follow.
|
||||
|
||||
| built-in | sites |
|
||||
|---|---|
|
||||
@@ -91,7 +164,7 @@ Counting call sites in `Stage02.ssb` (`data/isl-stage02.txt`):
|
||||
| `dist_lt` | **92** |
|
||||
| `unit_alive` | **71** |
|
||||
| `unit_relation` | **52** |
|
||||
| `set_flag` / `clear_flag` / `push_trigger` | 12 each |
|
||||
| `set_flag` / `clear_flag` / `reset_phase_threads` | 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`,
|
||||
@@ -285,9 +358,16 @@ Read from a running Stage 02 mission (`ScriptPhase 0xBE14DD80`, container at
|
||||
```
|
||||
|
||||
**`+20` moves, 0 → 1 → 2**, while the phase ordinal stays 1. So it is a real
|
||||
counter of **currently registered triggers** — the script arming watches as it
|
||||
goes (Stage 02 has 12 `push_trigger` sites) — and it is readable live with no
|
||||
debugger. That is the first direct view of *what the script is waiting for*.
|
||||
counter of **currently registered triggers**, readable live with no debugger —
|
||||
the first direct view of *what the script is waiting for*.
|
||||
|
||||
⚠️ The *measurement* stands; its attribution did not. This paragraph used to add
|
||||
"the script arming watches as it goes (Stage 02 has 12 `push_trigger` sites)",
|
||||
pointing at built-in 100. Built-in 100 is `reset_phase_threads` — it **clears**
|
||||
the trigger container, it does not arm one. The 12 sites are real, but they are
|
||||
12 places where Stage 02 *tears the trigger set down*, which is close to the
|
||||
opposite reading. ❔ What actually arms a trigger is now open again; built-ins
|
||||
19 and 25 both queue into `[phase+272]` and are the first place to look.
|
||||
|
||||
🟡 **`+12` is not a list head after all**, or not only that: it reads
|
||||
`0x000A0009`, which is not a pointer. The `addi r31, r30, 12` in the push made
|
||||
|
||||
Reference in New Issue
Block a user