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.
This commit is contained in:
Sylpheed RE agent
2026-08-27 11:05:50 +00:00
parent 4feab6c579
commit 17d1d2c999
3 changed files with 110 additions and 0 deletions

View File

@@ -567,6 +567,40 @@ unknown, what evidence exists, and what the first step would be. Move an item in
construction site is in `sub_8225EC78``sub_82260568(Y, r26, [X+12])` at
`0x8225ED5C`. The kind goes **straight into the constructor as arg 3**.
* ✅✅ **(2026-08-27) SETTLED — `X+12` IS A 0-BASED STAGE INDEX. The gate is a
bounds check on a 33-entry table, and `!= 16` is Stage17.
[structures/isl-condition-builtins](structures/isl-condition-builtins.md).**
Three iterations circled this. Following the *value* rather than the filename
does 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` builds a
string-pointer table on its stack and indexes it with exactly that field
(`lwz r11, 152(r21)`; `rlwinm ×4`; `lwzx r4, r11, r10` where `r10 = r31+144`).
The table is contiguous at **`0x820A8880`**, 20 bytes/entry:
**index 0 = `STAGE01_UNIT_MAX` … index 16 = `STAGE17_UNIT_MAX` … index 32 =
`STAGE33_UNIT_MAX`**, then `PLANE` at 33.
**`X+12` = `[phase+152]` = a 0-based stage index, `N → STAGE(N+1)`** — and the
gate is now explained rather than described: **`<= 32` is the array bound**
(33 entries, 0…32) and **`!= 16` is STAGE17**, the one stage number with no
`.ssb` on the disc.
**Retracts my own "not adopted".** Two iterations ago the stage-index reading
scored 1-of-1 but was coincidence-shaped and deliberately not believed. It is
now read off the game's own strings, and **0-based is PROVED by index 5 →
`STAGE06`**, not assumed.
⚠️ **False positive caught**: `0x82272D88 lwz r11, 152(r11)` in the built-in
switch is not this field — `r11` had just been loaded from `0(r31)`, so it is
the **vptr**, and the instruction is a virtual call to **slot 38**. Offset 152
recurs; the base register decides.
**And a THIRD class** — [structures/isl-builtin-dispatch](structures/isl-builtin-dispatch.md).
`sub_822700C0` stamps its own vptr `0x820A8E44` before `sub_8225FEF8` overwrites
it: **`.?AVSilphScriptPhaseBase@silph@@`**, also 113 slots. So the hierarchy is
`SilphScriptPhaseBase``SilphScriptPhase` and ← `SilphScriptPhaseDemo`, and
the Demo object is built by calling the **base** ctor then stamping the Demo
vptr — which is why `sub_822700C0` shows up on both construction paths.
🟡 Not settled: what `STAGENN_UNIT_MAX` resolves to (the lookup goes through
`sub_824480D0` / `sub_82448AA0`), and whether `Stage30``Stage33` exist on the
disc at all — the table has 33 slots but only 28 scripts were extracted.
## ✅✅ SOLVED — the mission freeze was a modal sign-in dialog (2026-08-26)
`XamShowSigninUI` opens a modal dialog and `xeXamDispatchDialog` blocks the

View File

@@ -179,6 +179,27 @@ not proof of inheritance; check which object each vptr lands on.*
📏 The allocation is **10216 bytes**, consistent with the highest field this
corpus has read, `[phase+10160]`.
### ✅ There are THREE classes, not two — the base is `SilphScriptPhaseBase`
`sub_8225FEF8` opens by calling `sub_822700C0`, which stamps **its own** vptr
`0x820A8E44`; `sub_8225FEF8` then overwrites it with `0x820A84BC`. That *is* the
derived-constructor pattern, on one object. `0x820A8E44`'s COL gives
**`.?AVSilphScriptPhaseBase@silph@@`**, and it is **113 slots** like the other two.
```
silph::SilphScriptPhaseBase 0x820A8E44 ctor sub_822700C0
├── silph::SilphScriptPhase 0x820A84BC ctor sub_8225FEF8
└── silph::SilphScriptPhaseDemo 0x820A8684 ctor inlined in sub_82260568
```
The Demo object is built by calling the **base** ctor and stamping the Demo vptr,
which is why `sub_822700C0` appears on both paths — and why the two objects are
separate allocations rather than one.
🔑 The base ctor is also what stores the load's kind argument
(`or r26, r7, r7``stw r26, 152(r30)`). That field is now identified as a
**0-based stage index** — see [isl-condition-builtins](isl-condition-builtins.md).
## ✅ What the Demo vtable's overrides actually are: no-ops
The 109 overridden slots point at three shared stubs, and all three do nothing:

View File

@@ -235,6 +235,61 @@ 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