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.
This commit is contained in:
Sylpheed RE agent
2026-08-27 10:14:00 +00:00
parent cbf54c9d21
commit 685e1d41b4
2 changed files with 95 additions and 0 deletions

View File

@@ -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_<id>_<page>_<line>`), 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

View File

@@ -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_<id>_<page>_<line>` — 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`.