From 17d1d2c999e1e2c6b4f76f896766206c3de6640d Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Thu, 27 Aug 2026 11:05:50 +0000 Subject: [PATCH] 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. --- docs/re/BACKLOG.md | 34 ++++++++++++ docs/re/structures/isl-builtin-dispatch.md | 21 ++++++++ docs/re/structures/isl-condition-builtins.md | 55 ++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index cc01dce2..bd55ee42 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -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 diff --git a/docs/re/structures/isl-builtin-dispatch.md b/docs/re/structures/isl-builtin-dispatch.md index 7a660f8a..90e2c7fa 100644 --- a/docs/re/structures/isl-builtin-dispatch.md +++ b/docs/re/structures/isl-builtin-dispatch.md @@ -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: diff --git a/docs/re/structures/isl-condition-builtins.md b/docs/re/structures/isl-condition-builtins.md index 0934bdc5..69f410b7 100644 --- a/docs/re/structures/isl-condition-builtins.md +++ b/docs/re/structures/isl-condition-builtins.md @@ -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