Corrects this corpus's own notation: there is no lower bound, values below 16 pass. sub_8225EC78(X, kind, ...) -- the function carrying "script load cancel" -- stores its second argument into X+12 and applies the same two tests to it immediately, bailing out of the load. So the gate is the object's invariant, re-checked at every later touch, not a selector. 42 sites image-wide have the shape, 34 of them in one code region plus 3 in sub_8225EC78. Object chain: X = [GamePart+104] is the current script instance (installed at 0x821A78EC, previous one torn down via sub_8225EB60); X+4 -> Y; Y+4 -> the ScriptPhase; Y+72 and [ScriptPhase+10152] get the answer. Refutes my own lead in the same pass: "X+12 is a GamePart id and 16 is the unregistered one" fails its control -- 1, 2 and 18 are also absent from the RegisterToFactory list, so 16 is one of four gaps. Docs only; artefacts byte-identical.
242 lines
11 KiB
Markdown
242 lines
11 KiB
Markdown
# 🟡 The three built-ins that appear *inside* clear conditions — read, not named
|
||
|
||
[`isl-phase-guards`](isl-phase-guards.md) produced the per-phase clear
|
||
conditions, but three of the built-ins in them were unread, so the conditions
|
||
were only half-readable. All three are read now. **None of them is named** — the
|
||
corpus has withdrawn two names taken from usage shape, and what I read is not
|
||
enough to name these.
|
||
|
||
Implementations via the vtable at `0x820A84BC` (control: built-in 69
|
||
`unit_state` → slot 184 → `0x8226ADF0`, as recorded).
|
||
|
||
| built-in | slot | implementation |
|
||
|---|---|---|
|
||
| **104** | 288 | `0x8226BFE0` |
|
||
| **141** | 428 | `0x8226C9B8` |
|
||
| **7** | 40 | `0x82266C68` |
|
||
|
||
## ✅ `builtin104` is a pure getter — and it is the whole tutorial clear condition
|
||
|
||
```
|
||
8226BFE0 lwz r11, 10160(r3) ; r3 = the ScriptPhase
|
||
8226BFE4 stw r11, 164(r3) ; -> special[0]
|
||
8226BFE8 blr
|
||
```
|
||
|
||
Three instructions. It returns **`[phase+10160]`** and nothing else. So all six
|
||
tutorial stages (S18–S23) end on the value of a **single engine-written word** —
|
||
which is exactly why their exits have one dominating condition each and why
|
||
`isl-builtins.md` saw built-in 104 only ever inside a poll loop.
|
||
|
||
**That word has exactly one writer** in the whole image:
|
||
|
||
```
|
||
821AAD74 lwz r11, 104(r30)
|
||
821AAD84 lwz r11, 12(r11)
|
||
821AAD88 cmpi cr6, 0, r11, 16 ; skip if == 16
|
||
821AAD90 cmpi cr6, 0, r11, 32 ; skip if > 32
|
||
821AAD98 lwz r11, 4(r10)
|
||
821AAD9C stw r29, 10160(r11) ; <- the only write
|
||
```
|
||
|
||
inside `sub_821AA1B0`, gated on a type/kind field being in `(16, 32]`. `r29`
|
||
there comes from `or r29, r3, r3` — the return of a preceding call.
|
||
|
||
## ✅ (2026-08-27) NAMED — built-in 104 is `request_next`, and the game says so itself
|
||
|
||
`r29` is the return of a **getter on an object fetched from a registry by id**:
|
||
|
||
```
|
||
821AAD1C lwz r4, 2424(r30) ; the registry
|
||
821AAD20 addis r5, r0, 0x20FF
|
||
821AAD28 ori r5, r5, 0xFF02 ; <- the id
|
||
821AAD38 bcctrl ; registry->slot1(&out, 0x20FFFF02)
|
||
821AAD40 lwz r11, 0(r3) ; lwz r11, 8(r11)
|
||
821AAD4C bcctrl ; r29 = obj->slot2() -- GET
|
||
821AAD9C stw r29, 10160(r11) ; -> [phase+10160]
|
||
821AADA0..DD4 ; then the SAME id again, obj->slot1(0) -- CLEAR
|
||
```
|
||
|
||
So the frame loop **reads the value, publishes it to the script, and clears it**.
|
||
|
||
**The id namespace has exactly three members** — `0x20FFFF00`, `0x20FFFF01`,
|
||
`0x20FFFF02` — each built at exactly 4 sites image-wide. Two of those sites are
|
||
in `sub_821D5178`, which fetches `0x20FFFF01` and `0x20FFFF02`, calls the same
|
||
`slot2()` getter on each, and passes both to a log call whose format string is
|
||
at `0x820A4968`:
|
||
|
||
> `silph::GamePart_ReadyRoom::Impl::OnCommand - Wait() command is requested.`
|
||
> `Check flow control valiables. WAIT_MODE : %d, REQUEST_NEXT : %d`
|
||
|
||
Argument order settles which is which: `r4` (first `%d`, **WAIT_MODE**) is
|
||
`0x20FFFF01`'s value, `r5` (second, **REQUEST_NEXT**) is `0x20FFFF02`'s.
|
||
|
||
> ⇒ **`[phase+10160]` is `REQUEST_NEXT`, and built-in 104 returns it.**
|
||
|
||
That is the game's own name for the variable, not a shape-based guess.
|
||
Corroboration: `GamePart_ReadyRoom::Impl::PrepareScript` sets `WAIT_MODE = 1`
|
||
and `REQUEST_NEXT = 0` before an ISL script runs, and `sub_821AA1B0` clears both
|
||
each frame after reading. `[phase+10160]` has **exactly one writer and one
|
||
reader** in the whole image (the two above); the three other hits on offset
|
||
10160 are `lfs` on unrelated objects.
|
||
|
||
So the six tutorial stages' exit condition `request_next() != 1` is the script
|
||
waiting on the surrounding game part's *proceed* flag — which is why S18–S23
|
||
each have exactly one dominating condition.
|
||
|
||
🟡 Still unread: what the `(16, 32]` gate on `[r30+104]`'s `+12` selects, and
|
||
`0x20FFFF00`, the third id (used by the `GRAPH_PATH` / `EX_FONT` / `SYSTEM`
|
||
code in `sub_821D6350` and `sub_821D6A40`).
|
||
|
||
## ✅ (2026-08-27) `[phase+10152]` and `[phase+10156]` are an ANSWER and a STATE
|
||
|
||
The backlog carried these as "9 and 7 writers, unread". They are read now, and
|
||
they are not `request_next`'s siblings-by-adjacency: they belong to a **trio of
|
||
built-ins** sitting in four consecutive vtable stubs.
|
||
|
||
| built-in | vtable slot | handler | body |
|
||
|---|---|---|---|
|
||
| **102** | 70 | `sub_8226BF48` | the state machine below |
|
||
| **103** | 71 | `sub_8226BFA8` | returns `state != 0 && answer == 1` |
|
||
| 104 | 72 | `sub_8226BFE0` | `request_next` (above) |
|
||
| **130** | 97 | `sub_8226BFD0` | `[phase+10152] = 0` |
|
||
|
||
Built-in 102, read straight off `0x8226BF48`:
|
||
|
||
```
|
||
state = [phase+10156]
|
||
state == 1 -> return 2 ; still pending
|
||
state == 2 -> special[0] = (answer == 1) ; state = 0 ; return 0
|
||
otherwise -> state = 1 ; answer = 0 ; return 2 ; issue the request
|
||
```
|
||
|
||
**`2` and `0` are the dispatcher's thread codes**, so this is a *blocking*
|
||
built-in: it yields the coroutine until an answer arrives, then hands it back.
|
||
Built-in 103 is the **non-blocking** form of the same test, and 130 clears the
|
||
answer slot.
|
||
|
||
⇒ **`[phase+10156]` is a request STATE — 0 idle, 1 pending, 2 answered — and
|
||
`[phase+10152]` is the ANSWER**, compared against `1`.
|
||
|
||
The answer is published by `sub_821A9DC8`, which writes
|
||
`[phase+10152] = [r6+4]` and `[phase+10156] = 2` **under the same `(16, 32]`
|
||
gate on `[r30+104]+12`** that `request_next`'s writer uses — so one
|
||
engine→script publish path serves both mechanisms.
|
||
|
||
⚠️ **A name I did not verify.** `isl.py` calls built-in 102 `prompt_yes_no`,
|
||
and the mechanism above is *consistent* with a modal yes/no prompt — but
|
||
grepping the corpus, that name appears only in a list of built-ins unused by
|
||
Stage 02, with **no derivation recorded anywhere**. What is actually read here is
|
||
"a blocking request whose answer is tested against 1". 103 and 130 are therefore
|
||
left unnamed rather than named off an unverified premise.
|
||
|
||
🟡 Also unread: what question is being asked. `sub_821A9DC8` has **no strings**
|
||
and exactly one reference in the image — a tail `b` from `0x821AC064` — so the
|
||
string recipe finds nothing there.
|
||
|
||
## 🔴✅ (2026-08-27) The gate is `!= 16 && <= 32` — and it is a VALIDITY CHECK, not a selector
|
||
|
||
🔴 **First, a correction to this page's own notation.** Written as `(16, 32]` it
|
||
implies a lower bound. There is none:
|
||
|
||
```
|
||
8225EC90 cmpi cr6, 0, r4, 16
|
||
8225EC94 stw r4, 12(r30) ; <- the field IS the argument
|
||
8225EC98 bc 12, eq, <bail> ; == 16 -> bail
|
||
8225EC9C cmpi cr6, 0, r4, 32
|
||
8225ECA0 bc 12, gt, <bail> ; > 32 -> bail
|
||
```
|
||
|
||
Everything **below** 16 passes. The condition is `kind != 16 && kind <= 32`.
|
||
|
||
✅ **`X+12` is a constructor parameter, not engine state.** `sub_8225EC78(X,
|
||
kind, …)` — the function carrying the string `"script load cancel\n"` — stores
|
||
its second argument into `+12` and applies the identical pair of tests to it
|
||
immediately, bailing out of the load. Every later occurrence re-tests the field
|
||
it just stored. So the gate is that object's **invariant**, re-checked wherever
|
||
it is touched, not a switch selecting between behaviours.
|
||
|
||
✅ **Where it is checked**: 42 sites image-wide with this exact `cmpi 16` →
|
||
`cmpi 32` shape, **34 of them in one code region** — `sub_821A9DC8`,
|
||
`sub_821AA1B0`, `sub_821AB570`, `sub_821AB650` — plus 3 in `sub_8225EC78`. One
|
||
object, guarded everywhere.
|
||
|
||
✅ **And the object chain is now read.** `X` is installed at `0x821A78EC`
|
||
(`stw r25, 104(r30)`), the previous one torn down first through `sub_8225EB60`;
|
||
so `X = [GamePart+104]` is **the game part's current script instance**. From
|
||
there `X+4` → `Y`, and `Y+4` → the **ScriptPhase**, `Y+72` = the answer, with
|
||
`[ScriptPhase+10152]` receiving the same value. `X+12`'s value arrives as
|
||
`[r21+8]`, `r21` being `sub_821A6CF0`'s own second argument.
|
||
|
||
🔴 **A tempting reading, killed by its control.** GamePart ids (from the
|
||
`RegisterToFactory<N,…>` strings) run 0…27, and **16 is not among them** — which
|
||
makes "`X+12` is a GamePart id and 16 is the unregistered one" very inviting.
|
||
It fails: **1, 2 and 18 are also absent** from that list, so 16 is one of *four*
|
||
gaps, not a unique one. Nothing distinguishes it. The reading is not adopted.
|
||
|
||
🟡 So what the kind **means** is still open — the gate is now read, but the
|
||
value's domain is not.
|
||
|
||
🟡 Its neighbours belong to the same cluster: `builtin103` reads
|
||
`[phase+10156]` and `[phase+10152]` (9 and 7 writers), and a sibling vtable stub
|
||
clears `[phase+10152]`. The shape is an engine→script status trio, but that is a
|
||
description, not a name.
|
||
|
||
## ✅ `builtin7`'s mechanism — and an independent confirmation
|
||
|
||
```
|
||
82266C84 lwz r11, 324(r25) ; the unit array
|
||
82266C88 lwz r10, 4(r26) ; local[4]
|
||
82266C94 lwzx r11, r10, r11 ; -> the record
|
||
82266C98 lwz r10, 4(r11) ; the handle … (alive?)
|
||
82266CA4 lwz r11, 16(r11) ; rec+16 = the unit STATE
|
||
82266CA8..CBC ; bail if state == 1, 3 or 4
|
||
82266CC0 lwz r10, 12(r26) ; local[12]
|
||
82266CC8 lwz r11, 244(r25) ; [phase+244] = SYMBOL TABLE 1
|
||
82266CD8 lwzx r10, r10, r11 ; -> resolve local[12] as a symtab-1 index
|
||
82266CD0 lfd f31, 25600(r9) ; a double constant
|
||
82266CD4 lwz r9, 16(r26) ; local[16]
|
||
```
|
||
|
||
🔑 **`isl.py`'s `SYM1_SLOTS` already lists slot 12 for built-in 7**, derived
|
||
purely from operand ranges. Reading the implementation shows the *mechanism* —
|
||
`local[12]` is indexed into `[phase+244]`, which
|
||
[`isl-bytecode.md`](isl-bytecode.md) documents as symbol table 1 (routes,
|
||
messages, objectives). **Two independent methods, same conclusion.**
|
||
|
||
That makes the Stage 02 phase-2 condition
|
||
|
||
```
|
||
builtin7(TCT206, 1, Route_TCT206_p2S, 4294967295, 500) == 1
|
||
```
|
||
|
||
structurally coherent — a unit, a **route symbol**, and two numbers — but *what*
|
||
it asks about the route is not read, so it stays `builtin7`.
|
||
|
||
## 🟡 `builtin141` — only the entry read
|
||
|
||
```
|
||
8226C9D4 lwz r11, 324(r29) ; unit array
|
||
8226C9D8 lwz r10, 4(r31) ; local[4]
|
||
8226C9E8 lwz r8, 4(r8) ; handle
|
||
8226C9F0 bc … 0x8226CA0C ; if alive, continue
|
||
8226C9F4 addi r11, r0, 0
|
||
8226C9F8 stw r11, 164(r29) ; dead -> special[0] = 0
|
||
```
|
||
|
||
The same unit-array opening as every unit predicate, returning 0 when the unit is
|
||
gone. Everything past `0x8226CA0C` is unread. Stage 16 calls it twice with
|
||
arguments differing in one position (`0` vs `-4000`), which *looks* like a
|
||
coordinate — and looking like one is precisely the evidence this corpus does not
|
||
accept.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* **No name for any of the three.** For 104 that needs the meaning of `r29` at
|
||
`0x821AAD9C` and of the `(16, 32]` gate; for 7 and 141, their bodies past the
|
||
entry checks.
|
||
* `builtin103`'s fields `[phase+10152]` / `[phase+10156]` have 9 and 7 writers,
|
||
none read.
|
||
* This does not change any artefact — the conditions already printed
|
||
`builtin104`, `builtin7`, `builtin141`, and still do.
|