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

@@ -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