Files
Sylpheed/docs/re/boot-config-and-gamepart-registry.md
Sylpheed RE agent cc8d4230b6 re: the phase transitions -- and withdrawing yesterday's refutation
because the test probed the wrong state

Nine stores to the phase field this+132, control-gated on the 136 query
returning at least the 18 known ones. Attributed to the handler each
lives in, they give the phase graph: entry -> 2, then 2 -> 0 (splash),
2 -> 3, 3 -> 4, and 4 -> 2 on event 0.

That last edge is the problem with what I did last iteration. The event-0
block sets TWO fields one instruction apart -- stw r28,136(r30) for state
0 and stw r11,132(r30) with r11 = 2 for phase 2. So B from the menu lands
in PHASE 2, the same phase as the boot title, and the test I ran
believing it probed phase 4 state 0 probed nothing of the kind.

So the refutation is withdrawn. Worse for me and better for the idea: A
working on the B-returned title is exactly what the hypothesis predicts,
since phase 2 is the phase that references BUTTON. The hypothesis is back
to untested, now consistent with two observations rather than one, and
what it still needs is the attract-returned title's phase -- which no
test so far has read.

The measurement from that iteration stands, because it does not depend on
the phase: the B-returned title accepts A, so only the ATTRACT-returned
title is inert. I have said so explicitly in the page rather than letting
the withdrawal take the good half down with it.

METHOD gets the lesson: before testing "state X behaves like this", check
your route actually reaches state X. When a transition writes more than
one field, read the whole block and not just the store you were looking
for.
2026-08-28 20:25:06 +00:00

26 KiB
Raw Blame History

What the game reads at boot — config.ini, and which GameParts actually exist

Status: CONFIRMED and decoded for the language selection. a bounded negative for the boot order. 🟡 for what the registration strings imply about the attract loop.

Contributes to MISSION Q6, and firms up Q4's ids.

config.ini — the disc's only config, and it selects the language

config.ini sits at the disc root, is 400 bytes, and is the only .ini, .cfg or .txt anywhere on the disc. Its own header comment (Shift-JIS) names it:

アプリケーション/ゲームパート初期設定テーブル"Application / GamePart initial settings table"

SYSTEM セクションには、ゲーム及び各ゲームパートで共通にアクセスする情報を記述する"the SYSTEM section describes information accessed in common by the game and each game part"

[SYSTEM]

[LANGUAGE]
        = eng   ; default
#0x01   = eng   ; XC_LANGUAGE_ENGLISH
#0x02   = jpn   ; XC_LANGUAGE_JAPANESE
#0x03   = deu   ; XC_LANGUAGE_GERMAN
#0x04   = fra   ; XC_LANGUAGE_FRENCH
#0x05   = esp   ; XC_LANGUAGE_SPANISH
#0x06   = ita   ; XC_LANGUAGE_ITALIAN

This is the mechanism behind the EN/JP screen pairs. The console's XC_LANGUAGE_* setting selects a three-letter code, and that code is what picks GP_TITLE's English or Japanese build (ui-title-build-map.md) and the <lang>.pak families. Default is eng, keyed by the empty line — an unlisted language falls back to English. decoded. The executable does read the file: the string config.ini is at .rdata 0x82062b44.

But the boot ORDER is not in it — and this is the whole search space

[SYSTEM] — the section the file's own comment says holds what the game and every game part share — is empty. So the file the game itself calls the GamePart initial settings table says nothing about which part runs first or what follows what.

Reach of the negative: this is the only config file on the disc (one find over the whole extract). The boot order is therefore not in disc-side configuration at all; it is in code, or in a table inside the executable that has not been located.

🟡 GP_ADVERTISE_DEMO is never registered — the attract loop is not its own part

The executable carries one diagnostic string per GamePart registration site (silph::GamePartTask::RegisterToFactory<N, class silph::GamePart_X>::RegisterToFactory is failed!) — the same evidence the corpus used to pin the id table (challenge-mission-gate.md §3). Extracting all of them gives 24 of the 29 ids bound to a C++ class; full list in data/gamepart-class-ids.txt.

The five ids with no registration site are 1, 2, 16, 18, 28 — in the id table's naming, GP_ADVERTISE_DEMO, GP_SELECT_STORAGE, GP_DEMO, GP_SELECTOR, GP_TEST.

That GP_ADVERTISE_DEMO (1) is among them matters for Q6, and it agrees with what was measured: the attract loop is the title screen replaying ADV.wmv (movie-binding.md), not a transition into a separate advertise part. The id table names a part the shipped build never registers.

🟡 not , because this is an argument from a diagnostic string: no error message for id 1 implies no registration site for id 1. That is how the corpus already reads these strings, and it is sound, but it is not the code.

Two bonuses for Q4

  • 3 and 4 are the same classGamePart_SaveLoad is registered twice. The id table's separate GP_LOAD / GP_SAVE names are two ids on one part.
  • The ids behind the menu buttons now match the executable's own class names, not just a list of table names: GamePart_Options = 8, GamePart_Tutorial = 25, GamePart_Extras = 5, GamePart_MissionSelect = 7, GamePart_MovieTheater = 6, GamePart_Title = 0. Still a name match — screen title ↔ class name — but anchored one level closer to the code.

What is still missing for Q6

The transitions. Nothing found so far says "title, then movie, then title" — the manifest gives the boot-side assets in play order (movie-binding.md), config.ini gives the language, and the registry gives which parts exist. What decides to advance is in GamePart_Title's own code, and reading it is a static PPC job that has not been started.

The transitions are code, not data — the reach of that negative

Q6's remaining half asked what the game reads to decide the boot order. The answer is: nothing. It is not data-driven. Four independent places were checked, and the sequence is in none of them.

looked in result
disc configuration config.ini is the only config file on the disc, its [SYSTEM] section is empty (above)
the movie manifest carries the boot-side assets in play order, and no transitions — movie-binding.md
a persistent part-id field already refuted: the requested GamePart id exists only as a stack argument in flight, with no literal store anywhere — challenge-mission-gate.md §5
the id table's attract entry the string GP_ADVERTISE_DEMO at 0x820a1fe8 has zero xrefs of any kind; nothing in the code reads it

So a transition is a call with an id argument, chosen by code. There is no table to read and nothing to poke.

Where that code is, as far as it was traced. The RegisterToFactory<0, class silph::GamePart_Title> diagnostic string at 0x820a3d60 is referenced from exactly one place, sub_8280E148 — the registration site — which also takes the address of sub_821C7D98 (addi), the position a factory template puts its creator. 🟡 That identification is by position and convention, not proven; sub_821C7D98 itself has 0 .rdata references, consistent with a small new+ctor thunk rather than the state machine. The substantial function in the same class neighbourhood is sub_821C6458 (4 460 bytes, has EH, 15 .rdata refs), and reading it has not been attempted.

What this means for the port

The boot sequence is authored, not transcribed — and that is fine, because the sequence itself is measured end to end:

developer splash (a RATC screen, not a video)
  → ADV.wmv
  → title + PRESS Ⓐ  ──idle ~810 s──> fade to black → ADV.wmv in full → title
  → Ⓐ → main menu

with the fade-through-black timings in screen-transitions.md and Ⓑ from the main menu returning to the title.

🟡 sub_821C6458 read — the title's states are NAMED in the executable

The page above left this function as the named next step: "the substantial function in GamePart_Title's neighbourhood, and reading it has not been attempted". It has now been looked at — not disassembled line by line, but characterised, which is enough to sharpen Q6.

It is the title part's screen-state function. 4 460 bytes, has EH, and called from exactly one place — sub_821C7850, which sits in the same neighbourhood as the creator the registration site points at (sub_821C7D98). It makes 33 distinct calls.

What it names. Its .rdata string references are:

string at
TITLE_SCREEN 0x820a3d3c
TITLE_MENU 0x820a3d30
LOADING 0x820a214c
BASE_INFO (×2) 0x820a20ec

Those are the states measured off the running game, in the game's own words: the title carrying PRESS Ⓐ BUTTON, the five-button menu, and a loading state. And BASE_INFO is this corpus's own marker for a screen-config lookup rather than a table read (REFUTED.md, "BASE_INFO discriminates screen-config from table-read, 9/9 vs 10/10").

The sharper negative

So the title part does ask for configuration keyed by TITLE_SCREEN and TITLE_MENU — and config.ini, the disc's only config file, contains no such sections. Its only sections are an empty [SYSTEM] and [LANGUAGE]. The lookups find nothing, and the values are whatever the code defaults to.

That is a better answer than "the order is not in config": the game asks the question, the shipped disc does not answer it, and the defaults live in code. For the port it means the state names are transcribable even though their contents are not.

🟡 Not . This is a characterisation from string references and call counts, not a read of the control flow. Two self-references (0x821c6498, 0x821c6b7c) inside the function look like jump tables — a switch, which is what a state machine compiles to — but that was not confirmed, and nothing here shows which state leads to which.

🔴 Refuted on the way, because it looked like a find

The words at 0x820a3b48… resolve as neat {func, func, ptr} triples and read convincingly as a state/handler table. They are not. The bytes immediately before them are the tail of …SaveLoad>::RegisterToFactory is failed!, and the 0x8210c1xx targets are zero-filled descriptors — static-initialiser records trailing the registration strings, not a dispatch table. A plausible-looking array of function pointers next to relevant strings is not evidence of anything until its neighbours are read.

🟡 The title's transition is a screen lookup BY NAME — sub_821CC860

Reading the code around each of the three state names shows the same four instructions at all three sites:

        lwz   r29, 20(r30)          ; an object off the part
        lwz   r3,  4(...)           ; ...
        bl    0x822F2328
        lis   r11, 0x820A
        li    r6,  0
        lwz   r4,  24(r30)
        addi  r5,  r11, 15676       ; "TITLE_SCREEN"   (15664 = "TITLE_MENU",
        lwz   r3,  88(r30)          ;                   8524  = "LOADING")
        bl    0x821CC860            ; <- lookup(this+88, this+24, NAME, 0)
        mr    r4, r3
        bl    0x82187B78            ; <- install the result

So a title-side transition is sub_821CC860(…, "<NAME>", 0) followed by sub_82187B78(result) — a string-keyed screen lookup, then an install. Not a numeric id, not a table index. That also fits GP_ADVERTISE_DEMO having zero xrefs: at this level the screen graph is keyed by name, not by GamePart id.

The candidate name vocabulary

sub_821CC860 is called from 28 distinct functions. Collecting the upper-case identifier strings those callers reference gives 35 names, and they split into two obvious families:

  • screen/state namesTITLE_SCREEN, TITLE_MENU, LOADING, DIFFICULTY, EXTRA_MENU, TUTORIAL_MENU, STANDARD_MENU, DEBRIEFING, CHALLENGE, MISSIONS, LIVE_BOARD, LOCAL_BOARD, EXTRA_MENU;
  • config keysTEXT_FONT, TEXT_HEIGHT, LINE_SPACE, GAMMA_RGB, GAMMA_WB, TEXT_SPEED_PER_LETTER, EXIT_VALUE, INPUT_DIR, MENU_ENABLE_SKIP/_DISABLE_SKIP, PAUSE_SE, JINGLE, …

plus BASE_INFO in 19 of the 28 callers, which is this corpus's existing marker for a screen-config function.

Three of the screen names are independently corroborated by measurement: DIFFICULTY is exactly the screen NEW GAME opens, EXTRA_MENU matches the EXTRAS submenu and TUTORIAL_MENU the lesson list — all three measured off the running game in menu-navigation-semantics.md before this function was ever looked at.

⚠️ Why this is 🟡 and not . The 35 strings are what those callers reference, not proven arguments to sub_821CC860 — the list plainly mixes screen names with config keys, so it is a candidate vocabulary, not a decoded one. Confirming it means checking, per call site, which string actually lands in r5. That was not done.

What is still unread

Which state leads to which. The three lookups sit in different branches of one function and at least one (TITLE_MENU) is guarded by a cmplwi/bne, but the branch structure was not traced, so the order still comes from measurement, not from the code.

The argument is now decoded — and it refutes my own corroboration

The section above listed a candidate vocabulary from "upper-case strings the callers reference", flagged 🟡 because those are not proven arguments, and named the check: confirm per call site which string actually lands in r5. That check has been run, and it fired.

Method. Forward register simulation over the 100 instructions before each of the 48 call sites of sub_821CC860, tracking lis / addi / subi / mr. Full table in data/name-lookup-callsites.txt.

Gated on a control, and the first version failed it. A backward scan for addi r5, … recovered only 7/48 and missed both title sites I had read by eye — because there the name goes into r27 first and reaches r5 via a later mr. The rewritten forward simulation reproduces all three known sites (TITLE_SCREEN, TITLE_MENU, LOADING) and then recovers 46 of 48.

🔴 DIFFICULTY and EXTRA_MENU are not arguments

Last section I wrote that three names were "independently corroborated by measurement": DIFFICULTY, EXTRA_MENU, TUTORIAL_MENU. Only TUTORIAL_MENU survives. DIFFICULTY and EXTRA_MENU never appear in r5 at any of the 48 sites — they are strings that merely live in the same functions. The corroboration I claimed was an artifact of the loose filter, and the 🟡 I put on it is exactly what caught it.

The 28 names that ARE passed

BASE  BASE(x3)  BG(x3)  BLACK(x2)  BUTTON  DEBRIEFING  DETAIL  EXTRA  FADE(x3)
FILE(x5)  GAME  GAMMA_RGB  GAMMA_TITLE  INFO  KEY  LIVE_BOARD(x2)  LOADING(x2)
LOCAL_BOARD(x2)  MENU(x2)  MENU_DISABLE_SKIP  MESSAGE  NEW_ITEM(x2)  PAD  SOUND
TITLE(x2)  TITLE_MENU  TITLE_SCREEN(x2)  TUTORIAL_MENU  WINDOW

🔴 So sub_821CC860 is not a screen factory

The previous section called it one. The real argument list says otherwise: BG, BLACK, FADE, FILE, KEY, PAD, SOUND, WINDOW, GAMMA_RGB, MENU_DISABLE_SKIP are not screens. It is a generic name-keyed lookup — a named-entry getter used for config and resources throughout the executable, of which the title part happens to call it with its three state names.

That weakens nothing about the title finding itself — sub_821C6458 really does transition by calling this with TITLE_SCREEN / TITLE_MENU / LOADING — but it removes the inference that the 28 names are a screen vocabulary. They are a lookup-key vocabulary, mostly config.

The title's state machine — decoded

The last open piece of Q6 was which state leads to which. It is a plain switch and it is now read.

821c6474  lwz    r11, 136(r30)     ; state = this+0x88
821c6478  cmplwi cr6, r11, 0x9     ; ten states, 0..9
821c647c  bgt    cr6, <default>
821c6480  lis/addi r12, 0x821C6498 ; jump table
821c648c  lwzx   r0, r12, r0
821c6494  bctr

⚠️ The table at 0x821C6498 disassembles as ten lwz r16, N(r28) instructions. It is data. That is the "self-reference" this page flagged two sections ago as looking like a jump table — it is one, and the disassembler was decoding its words as code.

Three of the ten states install a named screen: 0 → TITLE_SCREEN, 2 → TITLE_MENU, 8 → LOADING.

Eighteen transitions, every one a literal li rX, N ; stw rX, 136(r30):

0 → 1, 2        2 → 4        4 → 0, 5, 8×4        6 → 7, 9, 2        8 → 2
1 → 2, 2        3 → 4        5 → 6                7 → 9              9 → (none)

Full table with addresses in data/title-state-machine.txt.

🟡 It lines up with what was measured — read as corroboration, not proof

The boot reaches the title (state 0) and Ⓐ opens the main menu (state 2); the graph has 0 → 2 directly and 0 → 1 → 2. Ⓑ at the main menu returns to the title, and 4 → 0 is the only edge back to state 0, reached from 2 → 4. Entering a submenu goes through LOADING and comes back, and 4 → 8 (four separate sites) then 8 → 2 is exactly that shape.

Those readings are mine, matching a graph to observed behaviour — the conditions on the edges are not decoded, so nothing here proves which input picks which branch.

What is still not decoded

  • the condition on each edge — which input or event selects it;
  • states 1, 3, 5, 6, 7, 9 install no named screen, so what they do is unknown;
  • state 3 is never a destination in this function, so something outside sets it.

The edge conditions, for the state that has them — an EVENT CODE

The previous section left "the condition on each edge" open. For the state that matters it is now read.

sub_821C6458's third argument is an event code. State 4 — reached from 2 → 4, i.e. immediately after the main menu is installed — is the input-waiting state, and it dispatches on that argument:

821c6b5c  cmplwi cr6, r27, 0x19     ; 26 events, 0..25
821c6b60  bgt    cr6, <default>
821c6b68  addi   r12, r12, 27516    ; second jump table, at 0x821c6b7c
821c6b78  bctr

Six of the 26 are handled; the other twenty fall through and change nothing.

event → state
0 0 TITLE_SCREEN — back to the title
3 8 LOADING
5 8 LOADING
8 8 LOADING
10 5
25 8 LOADING

So last section's edges 4 → 0, 5, 8×4 are each now attributed to a specific event, and the shape of the state graph is complete for the input state.

🟡 A correspondence worth noticing, and not more than that

The main menu has five items, Ⓑ returns to the title, and this table has one event to state 0, four to LOADING, one elsewhere. It is tempting to read that as Ⓑ = event 0, four items load an external archive, EXTRAS stays inside GP_TITLE — which would match everything measured.

That is a count-match, not a decode. The event numbers are not named, nothing here shows event 3 is a particular menu row, and state 5 installs no named screen, so the EXTRAS half of the story has no support at all. Recorded as an observation so the next person sees the shape; it is not a button→event map.

What is still not decoded

  • what the event numbers mean — button id, menu-item index, or message id;
  • conditions on edges out of the other states (0 → 1 vs 0 → 2, 6 → 7/9/2);
  • states 1, 3, 5, 6, 7, 9, which install no named screen.

🔴 Correcting my own framing: sub_821C6458 is ONE PHASE, not the whole part

Chasing where the event code comes from turned up the level above, and it revises what the previous three sections called "the title part's state machine".

sub_821C6458 has exactly one caller, sub_821C7850, and that caller is itself a dispatcher — on a second, outer state field:

821c786c  lwz    r11, 16(r30)      ; only runs at all when this+16 == 3
821c7874  bne    cr6, <exit>
821c787c  lwz    r11, 132(r30)     ; phase = this+0x84
821c7880  cmplwi cr6, r11, 0x4     ; FIVE phases, 0..4
821c789c  bctr                     ; table at 0x821C78A0
phase handler
0 sub_821C5690 the splash — independently identified as the splash mechanics in the iterate3E notes, before any of this
1 inline at 0x821c790c
2 sub_821C5818
3 sub_821C5EC0
4 sub_821C6458 the title/menu machine — TITLE_SCREEN / TITLE_MENU / LOADING

So GamePart_Title has two nested state fields: a phase at this+132 (five values) selecting which sub-machine runs, and the state at this+136 (ten values) inside phase 4. Everything the previous sections decoded — the ten states, the eighteen edges, the event dispatch — is phase 4 only. Phases 03 are untouched, and one of them is the developer splash.

That phase 0 lands on the function the corpus had already fingered as the splash, from a completely different direction, is the useful check here.

And the event code is forwarded, not created

All five phase handlers are called as (this, r29, r28) with the same arguments, and r28 is sub_821C7850's own third argument, passed through untouched. So the event vocabulary is defined at least one level above this function. Finding what 3, 5, 8, 10, 25 mean means going up again, and that was not done.

Going up one more level — three of four callers pass a constant event

sub_821C7850 has four direct callers. Recovering the r5 argument at each:

caller event passed
sub_821C7CB8 0 (→ back to the title)
sub_821C7BA0 0
sub_821C47A0 5 (→ LOADING)
sub_821C5580 not constantlwz r5, 4(r27), read out of a structure

So the vocabulary is still not enumerable from here: the interesting caller reads its event from a field. What 3, 8, 10 and 25 mean remains open.

sub_821C5580 is also where the outer gate is set:

821c5640  li   r11, 3
821c5644  stw  r11, 16(r28)      ; this+16 = 3 — the exact value sub_821C7850 tests
821c5650  lwz  r5, 4(r27)        ; event from a struct field
821c5658  bl   0x821C7850

That answers the "what does this+16 == 3 gate on" question from the previous section: this function arms it.

🎁 And it names the title's music

The same function, a few instructions earlier:

821c560c  li   r5, 1103
821c5610  li   r4, 4
821c561c  lwz  r3, 0(r29)
821c5620  bl   0x8217ACF8        ; a sound-play call

1103 is a BGM cue idBGM_103. That closes an open residual from Q8/Q10 ("which bank is the menu's music is not on the disc"), and it checks out three ways; see structures/bgm-two-stems.md.

The other phases, characterised — and phase 0 confirms the splash twice over

Same treatment for the phase handlers that were still unread. Strings each one references, and whether it carries a jump table of its own:

phase handler size strings referenced own switch
0 sub_821C5690 380 LOGO none
2 sub_821C5818 1 576 BASE_INFO, BUTTON, TITLE_SCREEN none
3 sub_821C5EC0 1 220 (none) one, bctr at 0x821c5ef8
4 sub_821C6458 4 460 BASE_INFO, LOADING, TITLE_MENU, TITLE_SCREEN two (known)

Phase 0 references LOGO — a second, independent confirmation that it is the developer splash. The iterate3E notes reached sub_821C5690 from the guest side and named the splash's LOGO items; this reaches the same function from the registration site and finds the same string.

Phase 2 draws the title with the PRESS Ⓐ BUTTON plate — it references TITLE_SCREEN and BUTTON, and ui-title-build-map.md established from the archive that the plate is its own build (2/3), composited over the title art.

🟡 A hypothesis for a puzzle this corpus has had open for months

The title is installed from two different places: phase 2, and phase 4's state 0. Same screen, different code.

canary-scripted-input-traps.md has long recorded, and never explained, that the title which ends the boot accepts Ⓐ while the title the attract loop returns to accepts nothing — with the giveaway that a draw capture in each state is identical, 13 quads at the same rects. Two code paths installing the same screen is exactly the shape that would produce that.

Candidate: the boot title is phase 2; the attract-returned title is phase 4, state 0. Only phase 2 wires up the Ⓐ handling.

⚠️ Tested, and dead. Ⓑ from the main menu is the decoded edge 4 → 0, so the Ⓑ-returned title is phase 4 state 0 — and Ⓐ works there, opening the main menu, with Ⓐ on the boot title as the control in the same run. Phase 4 state 0 handles Ⓐ, so the two-code-paths explanation is refuted. See canary-scripted-input-traps.md, which the test did narrow usefully: only the attract-returned title is inert, not every title after the first.

⚠️ One query in this section failed its own control

Counting stw rX, 136(r30) per phase returned 0 for phase 4, which is known to have 18 — the operand text is r11, 136(r30) and the pattern did not allow the space. The per-phase store counts were discarded. The bctr half does pass its control (phase 4 shows both known tables) and is reported above.

The phase transitions — and they invalidate yesterday's test

Nine stores to the phase field this+132 across the GamePart_Title neighbourhood, attributed to the handler each lives in (control: the same query shape finds 30 stores to this+136, ≥ the 18 known in phase 4):

store sets phase lives in
0x821c4fbc 2 outside the handlers (entry/setup)
0x821c4fe0, 0x821c4fec 4 outside the handlers
0x821c5010 (not an immediate) outside the handlers
0x821c592c 0 phase 2
0x821c5b00 3 phase 2
0x821c6370 4 phase 3
0x821c6e04 2 phase 4, in the event-0 block
0x821c7950 2 the dispatcher
entry → 2        2 → 0 (splash)   2 → 3        3 → 4        4 → 2 (event 0)

🔴 Withdrawing the refutation from the previous iteration

The event-0 block sets both fields, one instruction apart:

821c6df8  li   r11, 2
821c6e00  stw  r28, 136(r30)     ; state = 0
821c6e04  stw  r11, 132(r30)     ; phase = 2

So Ⓑ from the menu lands in phase 2, not phase 4 state 0. The previous section tested "Ⓐ on the Ⓑ-returned title" believing it was probing phase 4 state 0 — it was probing phase 2, the same phase as the boot title.

The test therefore says nothing about phase 4 state 0, and the refutation it produced is withdrawn. Worse for me and better for the idea: Ⓐ working there is exactly what the hypothesis predicts, since phase 2 is the phase that references BUTTON.

Status of the hypothesis: untested, and now consistent with two observations (Ⓐ works on the boot title and on the Ⓑ-returned title, both phase 2). What it still needs is the attract-returned title's phase — the one thing no test so far has read.