Three iterations circled this. Following the value rather than the
filename settles it in two hops:
1. SilphScriptPhase's ctor sub_8225FEF8 never touches r7 (zero
mentions). The BASE ctor sub_822700C0 keeps it:
or r26, r7, r7 -> stw r26, 152(r30)
2. sub_82261F70 indexes a stack table of string pointers with that
field. The table is contiguous at 0x820A8880, 20 bytes/entry:
index 0 = STAGE01_UNIT_MAX ... index 32 = STAGE33_UNIT_MAX,
then PLANE at 33.
So X+12 = [phase+152] = a 0-based stage index, N -> STAGE(N+1), and the
gate is explained rather than described: <= 32 is the array bound (33
entries) and != 16 is STAGE17, the one stage number with no .ssb.
This retracts my own "not adopted": the stage-index reading was 1-of-1
but coincidence-shaped two iterations ago; 0-based is now PROVED by
index 5 -> STAGE06.
Caught a false positive: 0x82272D88 lwz r11, 152(r11) in the built-in
switch is a virtual call to slot 38 -- r11 is the vptr, not the phase.
Also names a third class: sub_822700C0 stamps 0x820A8E44 =
SilphScriptPhaseBase, so the hierarchy is Base <- ScriptPhase and
Base <- Demo.
Docs only; artefacts byte-identical.
355 lines
16 KiB
Markdown
355 lines
16 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.
|
||
|
||
## ✅ (2026-08-27) The code region is NAMED: `GamePart_MainGame`
|
||
|
||
`sub_821A6CF0` has no `bl` callers, only a tail `b` from `0x821AC04C`. That
|
||
address sits in a run of adjustor thunks, and the table pointing at them is a
|
||
**vtable at `0x820A319C`** (RTTI locator at `0x820A3198` = `0x8210BB58`):
|
||
|
||
| slot | entry | resolves to |
|
||
|---|---|---|
|
||
| 0 | `sub_821ABFA8` | destructor |
|
||
| 1 | `0x821AC040` | **`addi r3, r0, 17; blr`** — returns the factory id |
|
||
| 4 | `0x821AC048` | `lwz r3, 8(r3); b sub_821A6CF0` |
|
||
| 5 | `0x821AC050` | `→ sub_821A82A0` |
|
||
| 6 | `0x821AC058` | `→ sub_821A8428` |
|
||
| 7 | `0x821AC060` | `→ sub_821A9DC8` |
|
||
| 8 | `0x821AC078` | `→ sub_821A9CF0` (guarded `r4 == 9`) |
|
||
| **9** | `0x821AC068` | **`→ sub_821AA1B0`** — the per-frame Update |
|
||
| 10 | `0x821AC070` | `→ sub_821AB570` |
|
||
|
||
Slot 1 returning **17** is decisive: `RegisterToFactory<17, class
|
||
silph::GamePart_MainGame>`. Every thunk does `lwz r3, 8(r3)` first, so these are
|
||
**`GamePart_MainGame::Impl` methods** reached through the outer class. The code
|
||
region this page has been reading is now named rather than inferred.
|
||
|
||
## 🟡 A lead on the kind's domain — better-controlled than the last one, still not adopted
|
||
|
||
The disc carries **28 mission scripts, numbered `Stage01`–`Stage16` and
|
||
`Stage18`–`Stage29`** (as extracted; the corpus's own "all 28 `StageNN.ssb`").
|
||
**Exactly one gap: Stage17.** The gate excludes **exactly one value: 16**. Under
|
||
0-based indexing `16 ↔ Stage17`, and `<= 32` comfortably bounds indices 0…28.
|
||
|
||
Compare the controls honestly:
|
||
|
||
| reading | values missing from the domain | gate excludes | match |
|
||
|---|---|---|---|
|
||
| GamePart id (refuted) | **4** — 1, 2, 16, 18 | 1 value | 1-of-4 — worthless |
|
||
| stage index (this) | **1** — Stage17 | 1 value | 1-of-1 |
|
||
|
||
That is a much tighter fit, and it is *still* coincidence-shaped. **Not adopted.**
|
||
|
||
### 🔴 (2026-08-27) The test I proposed for it DOES NOT EXIST
|
||
|
||
The plan was to find the `Stage%02d` construction site and see whether it is fed
|
||
`index` or `index + 1`. Two searches say there is no such site:
|
||
|
||
* **No stage-shaped format string.** The `'%s%02d'` at `0x820A9C8C` has exactly
|
||
**one** xref, into `sub_822929E0`, a generic string helper. Listing *every*
|
||
short `%d`-bearing string in the image — 43 of them — turns up no
|
||
`Stage`/`_S%02d` pattern at all.
|
||
* **No precomputed name hash.** Using the corpus's own `name_hash`, the values for
|
||
`Stage_S00…30`, `Stage00…30`, `StageNN.ssb` and `UnitGroup_S00…30` appear
|
||
**nowhere in the image as a 4-byte word**.
|
||
|
||
So the executable never builds a stage script's name; the mapping lives on the
|
||
data side. **The stage-index lead cannot be settled this way**, and the next pass
|
||
should not retry it. The one remaining static handle is another hop up: `X+12`
|
||
arrives as `[r21+8]` in `GamePart_MainGame` vtable slot 4, so it comes from
|
||
whatever the GamePartTask manager passes when it switches parts.
|
||
|
||
## ✅✅ (2026-08-27) SETTLED — `X+12` IS A 0-BASED STAGE INDEX, and the game's own strings prove it
|
||
|
||
Following the *value* instead of the filename does it in two hops.
|
||
|
||
**Hop 1 — where it is stored.** `sub_82260568` passes the kind as `r7` into the
|
||
phase initialisers. `SilphScriptPhase`'s constructor `sub_8225FEF8` **never
|
||
touches `r7`** (zero mentions in the whole function); the *base* constructor
|
||
`sub_822700C0` does:
|
||
|
||
```
|
||
822700F0 or r26, r7, r7
|
||
82270188 stw r26, 152(r30) ; -> [phase+152]
|
||
```
|
||
|
||
**Hop 2 — who reads it.** `sub_82261F70` builds a table of string pointers on its
|
||
stack at `r31+144…` and indexes it with that field:
|
||
|
||
```
|
||
8226230C lwz r11, 152(r21) ; the stored kind
|
||
82262310 addi r10, r31, 144 ; the table
|
||
82262318 rlwinm r11, r11, 2, 0, 29 ; kind * 4
|
||
8226231C lwzx r4, r11, r10 ; table[kind] -> a STRING
|
||
```
|
||
|
||
The table is contiguous at `0x820A8880`, 20 bytes per entry:
|
||
|
||
| index | string |
|
||
|---|---|
|
||
| 0 | `STAGE01_UNIT_MAX` |
|
||
| 1 | `STAGE02_UNIT_MAX` |
|
||
| … | … |
|
||
| **16** | **`STAGE17_UNIT_MAX`** |
|
||
| … | … |
|
||
| **32** | **`STAGE33_UNIT_MAX`** |
|
||
| 33 | `PLANE` — the table ends |
|
||
|
||
> ⇒ **`X+12` = `[phase+152]` = a 0-BASED STAGE INDEX**, `index N → STAGE(N+1)`.
|
||
|
||
The gate falls out of it exactly:
|
||
|
||
* **`kind <= 32`** is the **array bound** — the table has precisely 33 entries,
|
||
0…32, with `PLANE` at 33 where it stops.
|
||
* **`kind != 16`** is **`STAGE17`** — the one stage number with no `.ssb` on the
|
||
disc (files run `Stage01`–`Stage16`, `Stage18`–`Stage29`).
|
||
|
||
✅ **This retracts the "not adopted" above.** The stage-index reading was recorded
|
||
as 1-of-1 but coincidence-shaped and deliberately not believed; it is now read off
|
||
the game's own string table, and **0-based is proved by `index 5 → STAGE06`**,
|
||
not assumed.
|
||
|
||
⚠️ **A false positive caught on the way.** `0x82272D88 lwz r11, 152(r11)` inside
|
||
the built-in switch looks like a read of this field. It is not: `r11` had just
|
||
been loaded from `0(r31)`, so it is the **vptr** — that instruction is a virtual
|
||
call to **slot 38**. Offset 152 recurs; the base register decides.
|
||
|
||
🟡 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.
|