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-condition-builtins.md
Sylpheed RE agent 17d1d2c999 re: X+12 is a 0-based STAGE INDEX -- proved by the game's own string table
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.
2026-08-27 11:05:50 +00:00

355 lines
16 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 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.