From 685e1d41b484d9bfa10408d6f4dd44e5931917f8 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Thu, 27 Aug 2026 10:14:00 +0000 Subject: [PATCH] re: a script load builds TWO phase objects, main at Y+4 and demo at Y+8 sub_8225FEF8's only caller is sub_82260568, which looks like a derived ctor calling its base -- i.e. every phase is a Demo, which would make the built-in dispatch inert. Reading the constructor refutes it: it allocates 10216 bytes and runs SilphScriptPhase's ctor on object A, then initialises a second object B and stamps the Demo vptr on it. They land at Y+4 and Y+8, which is why the load path resolves the ScriptPhase through Y+4. The Demo class's 109 overrides all point at three no-op stubs; only slot 15, slot 111 (Update = sub_82275800), slot 112 and the destructor survive. DEMO is the corpus's established cutscene text family, so the cutscene-runner reading is recorded as a reading, not a result. Also notes a new handle for the stalled X+12 question: the kind is passed straight into sub_82260568 as its third argument. Docs only; artefacts byte-identical. --- docs/re/BACKLOG.md | 33 ++++++++++++ docs/re/structures/isl-builtin-dispatch.md | 62 ++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 2a9b98de..cc01dce2 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -534,6 +534,39 @@ unknown, what evidence exists, and what the first step would be. Move an item in 🟑 Still open: `X+12`'s domain β€” the one remaining static handle is another hop, to whatever the GamePartTask manager passes into `GamePart_MainGame` slot 4. +* βœ…βš οΈ **(2026-08-27) A SCRIPT LOAD BUILDS **TWO** PHASE OBJECTS β€” and the tidy + "every phase is a Demo" reading is killed by reading the constructor. + [structures/isl-builtin-dispatch](structures/isl-builtin-dispatch.md).** + Each vtable constant is materialised at exactly two sites (ctor + dtor), and + `vptr_writes` agrees: `SilphScriptPhase` ← `sub_8225FEF8` / `sub_82261B60`; + `SilphScriptPhaseDemo` ← `sub_82260568` / `sub_82260FF8`. + ⚠️ **The wrong turn.** `sub_8225FEF8`'s *only* caller is `sub_82260568` β€” which + looks exactly like a derived ctor calling its base, i.e. **every phase is a + Demo**. That would make the whole built-in dispatch inert, since the Demo + vtable no-ops nearly everything. Reading the constructor refutes it: + `sub_82260568` **allocates and builds two SEPARATE objects** β€” `sub_82150EF8` + for **10216 bytes** then `sub_8225FEF8` on object A, and `sub_822700C0` + + `stw 0x820A8684, 0(r29)` on object B β€” stored at **`Y+4` (main)** and + **`Y+8` (demo)**. That is exactly why the load path resolves the ScriptPhase + through `Y+4`. πŸ“ 10216 bytes is consistent with the highest field read here, + `[phase+10160]`. πŸ”‘ **Method note: a constructor calling another class's + constructor is not proof of inheritance β€” check which object each vptr lands + on.** + βœ… **The Demo overrides are no-ops.** All 109 point at three shared stubs, and + every one does nothing: `0x8226C160` (`special[0] = 0`), `0x822748B8` (same, + return 0), `0x822748A8` (`[phase+176] = 0.0`). Only four real methods survive: + slot 15 `sub_82391BA8`, **slot 111 `Update` `sub_82275800`**, slot 112 + `sub_8237EF08`, and the destructor. + 🟑 **What "Demo" means β€” a reading from the corpus's own vocabulary.** `DEMO` is + one of the eight **cutscene** text families already decoded here + (`MSG_DEMO___`), so a second phase built from the same load with + every gameplay built-in stubbed and only `Update` alive reads as **the cutscene + script runner**. Nothing here shows it executing cutscene bytecode, so it stays + a reading. + πŸ”‘ **New handle on the stalled `X+12` domain**: the Demo class's single + construction site is in `sub_8225EC78` β€” `sub_82260568(Y, r26, [X+12])` at + `0x8225ED5C`. The kind goes **straight into the constructor as arg 3**. + ## βœ…βœ… 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 132c2343..7a660f8a 100644 --- a/docs/re/structures/isl-builtin-dispatch.md +++ b/docs/re/structures/isl-builtin-dispatch.md @@ -144,3 +144,65 @@ callers of the spawner. They are a **matched pair**: Slot 0 in this ABI is the scalar deleting destructor, so these are the two classes' teardown paths β€” not two separate mysteries. 🟑 *Why* a destructor reaches the spawner is not read here. + +## βœ… (2026-08-27) A script load builds TWO phase objects, not one + +Each vtable constant is materialised at exactly **two** sites β€” its constructor +and its destructor β€” and `vptr_writes` agrees: + +| class | ctor | dtor | +|---|---|---| +| `SilphScriptPhase` | `sub_8225FEF8` (`0x8225FF28`) | `sub_82261B60` | +| `SilphScriptPhaseDemo` | `sub_82260568` (`0x82260660`) | `sub_82260FF8` | + +⚠️ **A wrong turn worth recording.** `sub_8225FEF8`'s *only* caller is +`sub_82260568`, which looks exactly like "base ctor called from derived ctor" β€” +i.e. **every phase is a Demo**. That would make the built-in dispatch inert, +since the Demo vtable no-ops almost everything. Reading the constructor kills it: +`sub_82260568` **allocates and builds two separate objects.** + +``` +822605C8 bl 0x82150EF8 ; allocate 10216 bytes <- object A +822605EC bl 0x8225FEF8 ; SilphScriptPhase ctor on A + … +82260658 bl 0x822700C0 ; the sibling initialiser on B +82260660 addi r11, r11, -31100 ; 0x820A8684 +82260664 stw r11, 0(r29) ; Demo vptr onto B +82260690 stw r29, 8(r30) +``` + +Both get the same four arguments. They land at **`Y+4` (the main phase)** and +**`Y+8` (the demo phase)** β€” which is why the load path read earlier resolves the +ScriptPhase through `Y+4`. *A constructor calling another class's constructor is +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]`. + +## βœ… What the Demo vtable's overrides actually are: no-ops + +The 109 overridden slots point at three shared stubs, and all three do nothing: + +``` +8226C160 stw r11(=0), 164(r3) ; special[0] = 0 ; return +822748B8 stw r10(=0), 164(r11) ; r3 = 0 ; same, and return code 0 +822748A8 lfd f0, 25600(0x820B) ; stfd f0, 176 ; [phase+176] = 0.0 +``` + +It keeps only four real methods: slot 15 β†’ `sub_82391BA8`, **slot 111 `Update` β†’ +`sub_82275800`**, slot 112 β†’ `sub_8237EF08`, and its destructor. + +🟑 **What a "Demo" phase is β€” a reading, from the corpus's own vocabulary.** +`DEMO` is one of the **eight cutscene text families** already decoded here +(`MSG_DEMO___` β€” see +[isl-message-dialogue-link](isl-message-dialogue-link.md) and the movie↔subtitle +work), so "demo" means *cutscene* in this codebase. A second phase object, +built from the same script load, with every gameplay built-in stubbed to a no-op +and only `Update` alive, reads as **the cutscene script runner**. Stated as a +reading: nothing here shows it executing cutscene bytecode. + +πŸ”‘ **A new handle on the stalled `X+12` question.** The Demo class's single +construction site is inside `sub_8225EC78` (the script load): +`sub_82260568(Y, r26, [X+12])` at `0x8225ED5C` β€” the kind is passed **straight +into the constructor as its third argument**, alongside `[r30+12]` again at +`0x822605E8` and `0x82260654`.