Corrects my own label: sub_824480D0/sub_82448AA0/sub_824482D0 is not an XML reader. The 40 <?xml pak entries are XPR2 resource manifests (XBGMesh/Texture declarations). The real config is a loose config.ini at the disc root, header comment "application / game-part initial- settings table", and every key this code touches is an INI section or key name. The shipped file is 400 bytes: [SYSTEM] (empty) and [LANGUAGE]. No [STAGENN_UNIT_MAX] anywhere, and UNIT_MAX/VESSEL appear in 0 pak entries -- so the lookup always misses and every stage runs on the defaults PLANE=200, VESSEL=20. The absence is controlled: the same scan decompressed 26443 entries across 41 paks and found MSG_DEMO 192x, mapmesh_box_500km 162x and <?xml exactly 40x, the census number. Last iteration's null was worthless -- it used s[8:] instead of s[10:] for the Z1 header and searched the index rather than the .pNN data. Using the corpus's own reader was the whole difference. Docs only; artefacts byte-identical.
20 KiB
🟡 The three built-ins that appear inside clear conditions — read, not named
isl-phase-guards produced the per-phase clear
conditions, but three of the built-ins in them were unread, so the conditions
were only half-readable. All three are read now. None of them is named — the
corpus has withdrawn two names taken from usage shape, and what I read is not
enough to name these.
Implementations via the vtable at 0x820A84BC (control: built-in 69
unit_state → slot 184 → 0x8226ADF0, as recorded).
| built-in | slot | implementation |
|---|---|---|
| 104 | 288 | 0x8226BFE0 |
| 141 | 428 | 0x8226C9B8 |
| 7 | 40 | 0x82266C68 |
✅ builtin104 is a pure getter — and it is the whole tutorial clear condition
8226BFE0 lwz r11, 10160(r3) ; r3 = the ScriptPhase
8226BFE4 stw r11, 164(r3) ; -> special[0]
8226BFE8 blr
Three instructions. It returns [phase+10160] and nothing else. So all six
tutorial stages (S18–S23) end on the value of a single engine-written word —
which is exactly why their exits have one dominating condition each and why
isl-builtins.md saw built-in 104 only ever inside a poll loop.
That word has exactly one writer in the whole image:
821AAD74 lwz r11, 104(r30)
821AAD84 lwz r11, 12(r11)
821AAD88 cmpi cr6, 0, r11, 16 ; skip if == 16
821AAD90 cmpi cr6, 0, r11, 32 ; skip if > 32
821AAD98 lwz r11, 4(r10)
821AAD9C stw r29, 10160(r11) ; <- the only write
inside sub_821AA1B0, gated on a type/kind field being in (16, 32]. r29
there comes from or r29, r3, r3 — the return of a preceding call.
✅ (2026-08-27) NAMED — built-in 104 is request_next, and the game says so itself
r29 is the return of a getter on an object fetched from a registry by id:
821AAD1C lwz r4, 2424(r30) ; the registry
821AAD20 addis r5, r0, 0x20FF
821AAD28 ori r5, r5, 0xFF02 ; <- the id
821AAD38 bcctrl ; registry->slot1(&out, 0x20FFFF02)
821AAD40 lwz r11, 0(r3) ; lwz r11, 8(r11)
821AAD4C bcctrl ; r29 = obj->slot2() -- GET
821AAD9C stw r29, 10160(r11) ; -> [phase+10160]
821AADA0..DD4 ; then the SAME id again, obj->slot1(0) -- CLEAR
So the frame loop reads the value, publishes it to the script, and clears it.
The id namespace has exactly three members — 0x20FFFF00, 0x20FFFF01,
0x20FFFF02 — each built at exactly 4 sites image-wide. Two of those sites are
in sub_821D5178, which fetches 0x20FFFF01 and 0x20FFFF02, calls the same
slot2() getter on each, and passes both to a log call whose format string is
at 0x820A4968:
silph::GamePart_ReadyRoom::Impl::OnCommand - Wait() command is requested.Check flow control valiables. WAIT_MODE : %d, REQUEST_NEXT : %d
Argument order settles which is which: r4 (first %d, WAIT_MODE) is
0x20FFFF01's value, r5 (second, REQUEST_NEXT) is 0x20FFFF02's.
⇒
[phase+10160]isREQUEST_NEXT, and built-in 104 returns it.
That is the game's own name for the variable, not a shape-based guess.
Corroboration: GamePart_ReadyRoom::Impl::PrepareScript sets WAIT_MODE = 1
and REQUEST_NEXT = 0 before an ISL script runs, and sub_821AA1B0 clears both
each frame after reading. [phase+10160] has exactly one writer and one
reader in the whole image (the two above); the three other hits on offset
10160 are lfs on unrelated objects.
So the six tutorial stages' exit condition request_next() != 1 is the script
waiting on the surrounding game part's proceed flag — which is why S18–S23
each have exactly one dominating condition.
🟡 Still unread: what the (16, 32] gate on [r30+104]'s +12 selects, and
0x20FFFF00, the third id (used by the GRAPH_PATH / EX_FONT / SYSTEM
code in sub_821D6350 and sub_821D6A40).
✅ (2026-08-27) [phase+10152] and [phase+10156] are an ANSWER and a STATE
The backlog carried these as "9 and 7 writers, unread". They are read now, and
they are not request_next's siblings-by-adjacency: they belong to a trio of
built-ins sitting in four consecutive vtable stubs.
| built-in | vtable slot | handler | body |
|---|---|---|---|
| 102 | 70 | sub_8226BF48 |
the state machine below |
| 103 | 71 | sub_8226BFA8 |
returns state != 0 && answer == 1 |
| 104 | 72 | sub_8226BFE0 |
request_next (above) |
| 130 | 97 | sub_8226BFD0 |
[phase+10152] = 0 |
Built-in 102, read straight off 0x8226BF48:
state = [phase+10156]
state == 1 -> return 2 ; still pending
state == 2 -> special[0] = (answer == 1) ; state = 0 ; return 0
otherwise -> state = 1 ; answer = 0 ; return 2 ; issue the request
2 and 0 are the dispatcher's thread codes, so this is a blocking
built-in: it yields the coroutine until an answer arrives, then hands it back.
Built-in 103 is the non-blocking form of the same test, and 130 clears the
answer slot.
⇒ [phase+10156] is a request STATE — 0 idle, 1 pending, 2 answered — and
[phase+10152] is the ANSWER, compared against 1.
The answer is published by sub_821A9DC8, which writes
[phase+10152] = [r6+4] and [phase+10156] = 2 under the same (16, 32]
gate on [r30+104]+12 that request_next's writer uses — so one
engine→script publish path serves both mechanisms.
⚠️ A name I did not verify. isl.py calls built-in 102 prompt_yes_no,
and the mechanism above is consistent with a modal yes/no prompt — but
grepping the corpus, that name appears only in a list of built-ins unused by
Stage 02, with no derivation recorded anywhere. What is actually read here is
"a blocking request whose answer is tested against 1". 103 and 130 are therefore
left unnamed rather than named off an unverified premise.
🟡 Also unread: what question is being asked. sub_821A9DC8 has no strings
and exactly one reference in the image — a tail b from 0x821AC064 — so the
string recipe finds nothing there.
🔴✅ (2026-08-27) The gate is != 16 && <= 32 — and it is a VALIDITY CHECK, not a selector
🔴 First, a correction to this page's own notation. Written as (16, 32] it
implies a lower bound. There is none:
8225EC90 cmpi cr6, 0, r4, 16
8225EC94 stw r4, 12(r30) ; <- the field IS the argument
8225EC98 bc 12, eq, <bail> ; == 16 -> bail
8225EC9C cmpi cr6, 0, r4, 32
8225ECA0 bc 12, gt, <bail> ; > 32 -> bail
Everything below 16 passes. The condition is kind != 16 && kind <= 32.
✅ X+12 is a constructor parameter, not engine state. sub_8225EC78(X, kind, …) — the function carrying the string "script load cancel\n" — stores
its second argument into +12 and applies the identical pair of tests to it
immediately, bailing out of the load. Every later occurrence re-tests the field
it just stored. So the gate is that object's invariant, re-checked wherever
it is touched, not a switch selecting between behaviours.
✅ Where it is checked: 42 sites image-wide with this exact cmpi 16 →
cmpi 32 shape, 34 of them in one code region — sub_821A9DC8,
sub_821AA1B0, sub_821AB570, sub_821AB650 — plus 3 in sub_8225EC78. One
object, guarded everywhere.
✅ And the object chain is now read. X is installed at 0x821A78EC
(stw r25, 104(r30)), the previous one torn down first through sub_8225EB60;
so X = [GamePart+104] is the game part's current script instance. From
there X+4 → Y, and Y+4 → the ScriptPhase, Y+72 = the answer, with
[ScriptPhase+10152] receiving the same value. X+12's value arrives as
[r21+8], r21 being sub_821A6CF0's own second argument.
🔴 A tempting reading, killed by its control. GamePart ids (from the
RegisterToFactory<N,…> strings) run 0…27, and 16 is not among them — which
makes "X+12 is a GamePart id and 16 is the unregistered one" very inviting.
It fails: 1, 2 and 18 are also absent from that list, so 16 is one of four
gaps, not a unique one. Nothing distinguishes it. The reading is not adopted.
🟡 So what the kind means is still open — the gate is now read, but the value's domain is not.
✅ (2026-08-27) The code region is NAMED: GamePart_MainGame
sub_821A6CF0 has no bl callers, only a tail b from 0x821AC04C. That
address sits in a run of adjustor thunks, and the table pointing at them is a
vtable at 0x820A319C (RTTI locator at 0x820A3198 = 0x8210BB58):
| slot | entry | resolves to |
|---|---|---|
| 0 | sub_821ABFA8 |
destructor |
| 1 | 0x821AC040 |
addi r3, r0, 17; blr — returns the factory id |
| 4 | 0x821AC048 |
lwz r3, 8(r3); b sub_821A6CF0 |
| 5 | 0x821AC050 |
→ sub_821A82A0 |
| 6 | 0x821AC058 |
→ sub_821A8428 |
| 7 | 0x821AC060 |
→ sub_821A9DC8 |
| 8 | 0x821AC078 |
→ sub_821A9CF0 (guarded r4 == 9) |
| 9 | 0x821AC068 |
→ sub_821AA1B0 — the per-frame Update |
| 10 | 0x821AC070 |
→ sub_821AB570 |
Slot 1 returning 17 is decisive: RegisterToFactory<17, class silph::GamePart_MainGame>. Every thunk does lwz r3, 8(r3) first, so these are
GamePart_MainGame::Impl methods reached through the outer class. The code
region this page has been reading is now named rather than inferred.
🟡 A lead on the kind's domain — better-controlled than the last one, still not adopted
The disc carries 28 mission scripts, numbered Stage01–Stage16 and
Stage18–Stage29 (as extracted; the corpus's own "all 28 StageNN.ssb").
Exactly one gap: Stage17. The gate excludes exactly one value: 16. Under
0-based indexing 16 ↔ Stage17, and <= 32 comfortably bounds indices 0…28.
Compare the controls honestly:
| reading | values missing from the domain | gate excludes | match |
|---|---|---|---|
| GamePart id (refuted) | 4 — 1, 2, 16, 18 | 1 value | 1-of-4 — worthless |
| stage index (this) | 1 — Stage17 | 1 value | 1-of-1 |
That is a much tighter fit, and it is still coincidence-shaped. Not adopted.
🔴 (2026-08-27) The test I proposed for it DOES NOT EXIST
The plan was to find the Stage%02d construction site and see whether it is fed
index or index + 1. Two searches say there is no such site:
- No stage-shaped format string. The
'%s%02d'at0x820A9C8Chas exactly one xref, intosub_822929E0, a generic string helper. Listing every short%d-bearing string in the image — 43 of them — turns up noStage/_S%02dpattern at all. - No precomputed name hash. Using the corpus's own
name_hash, the values forStage_S00…30,Stage00…30,StageNN.ssbandUnitGroup_S00…30appear nowhere in the image as a 4-byte word.
So the executable never builds a stage script's name; the mapping lives on the
data side. The stage-index lead cannot be settled this way, and the next pass
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 <= 32is the array bound — the table has precisely 33 entries, 0…32, withPLANEat 33 where it stops.kind != 16isSTAGE17— the one stage number with no.ssbon the disc (files runStage01–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.
✅ (2026-08-27) What STAGENN_UNIT_MAX is: two per-stage unit caps, PLANE and VESSEL
The lookup in sub_82261F70 continues straight past the table read. r21 is the
phase there; r31 is the stack frame (addi r4, r31, 452, lwz r11, 376(r31)
— easy to misread as phase fields).
82262308 bl 0x824480D0 ; open the config node -> [r31+84]
8226230C lwz r11, 152(r21) ; the 0-based stage index
8226231C lwzx r4, r11, r10 ; "STAGE(N+1)_UNIT_MAX"
82262320 bl 0x82448AA0 ; find that section
82262328 bc 4, eq, 0x82262340 ; found -> read two keys
; NOT found — defaults:
82262334 stw r11(=20), 360(r21)
82262338 stw r10(=200), 356(r21)
; found:
82262348 addi r4, r11, -29932 ; 0x820A8B14 = "PLANE"
82262360 stw r11, 356(r21)
8226235C addi r4, r10, -29924 ; 0x820A8B1C = "VESSEL"
82262368 stw r3, 360(r21)
| phase field | config key | default |
|---|---|---|
[phase+356] |
PLANE |
200 |
[phase+360] |
VESSEL |
20 |
So the section is a per-stage pair of unit caps — planes (fighters) and vessels (capital ships) — with the section name built from the stage index and the whole thing falling back to 200/20 when a stage has no entry. All three names are the game's own.
The surrounding reads name the config itself: GP_SCRIPT (0x820A8244),
SCRIPTS, Resource2D, TABLE. The pak census counts 40 <?xm entries, so
this is an XML config reader (sub_824480D0 open, sub_82448AA0 find-section,
sub_824482D0 read-int, sub_82448C50 / sub_824490E0 others).
🟡 The values themselves are not confirmed from the data side. A crude
Z1+zlib scan over dat/*.pak found no UNIT_MAX — but that scan did not use
the corpus's own pak reader (unitgroup.py), so it is not evidence of
absence. 🟡 No reader of [phase+356] / [phase+360] was identified: the
offsets recur, and the one ISL-region hit that looked promising
(0x82273400 lwz r11, 360(r11)) is a vtable slot 90 call — r11 is the
vptr — which is built-in 123, consistent with the known mapping.
✅🔴 (2026-08-27) The values: the config is an INI, it ships nearly EMPTY, and every stage takes the defaults
🔴 First, my own label from the previous pass is wrong. I called
sub_824480D0 / sub_82448AA0 / sub_824482D0 an XML config reader because
the pak census counts 40 <?xml entries. Those 40 are XPR2 resource
manifests — <RDF Version="XPR2">, <XBGMesh Name=… DataFile=…/>,
<Texture Name=… Source=… Format="D3DFMT_DXT1_SRGB"/> — mesh and texture
declarations, nothing to do with game config.
The real config is a loose INI at the disc root, config.ini, whose header
comment reads 「アプリケーション/ゲームパート初期設定テーブル」 — "application /
game-part initial-settings table" — and notes that the SYSTEM section holds
settings the game and every game part share. That matches every key this code
touches: SYSTEM, GP_SCRIPT, SCRIPTS, Resource2D, TABLE, GRAPH_PATH,
EX_FONT, SCRIPT_ID, SCRIPT_PATH, BASE_INFO, PLANE, VESSEL,
STAGENN_UNIT_MAX — INI section and key names.
And the shipped file is 400 bytes. In full it has two sections:
[SYSTEM]
[LANGUAGE]
= eng ; default
#0x01 = eng ; XC_LANGUAGE_ENGLISH
#0x02 = jpn ; XC_LANGUAGE_JAPANESE
… deu / fra / esp / ita
[SYSTEM] is empty, and there is no [STAGENN_UNIT_MAX] anywhere.
| check | result |
|---|---|
.ini files on the extracted disc |
1 — config.ini, 400 bytes, printed in full |
[STAGENN_UNIT_MAX] sections in it |
0 |
UNIT_MAX in any pak entry |
0 |
VESSEL in any pak entry |
0 |
The pak scan is controlled: the same loop over all 41 paks decompressed
26 443 entries and found MSG_DEMO 192×, mapmesh_box_500km 162× and
<?xml exactly 40× — the census number. So the reader works and the absence is
real. (An earlier hand-rolled scan used s[8:] instead of s[10:] for the Z1
header and searched the index file rather than the .pNN data — that one's null
result was worthless.)
⇒ The lookup always misses, so every stage runs on the defaults:
PLANE = 200,VESSEL = 20.
🟡 A hash search was run too, in case the section were name-hashed like a pak
entry: name_hash("STAGE01…33_UNIT_MAX"), name_hash("PLANE"),
name_hash("VESSEL") — 0 hits as pak entry keys. Eight of the 35 hashes did
appear as 4-byte words inside entries, seven of them in the 1.1 GB sound.pak;
that is the chance rate for a 32-bit needle in a blob that size, not a finding.
🟡 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
description, not a name.
✅ builtin7's mechanism — and an independent confirmation
82266C84 lwz r11, 324(r25) ; the unit array
82266C88 lwz r10, 4(r26) ; local[4]
82266C94 lwzx r11, r10, r11 ; -> the record
82266C98 lwz r10, 4(r11) ; the handle … (alive?)
82266CA4 lwz r11, 16(r11) ; rec+16 = the unit STATE
82266CA8..CBC ; bail if state == 1, 3 or 4
82266CC0 lwz r10, 12(r26) ; local[12]
82266CC8 lwz r11, 244(r25) ; [phase+244] = SYMBOL TABLE 1
82266CD8 lwzx r10, r10, r11 ; -> resolve local[12] as a symtab-1 index
82266CD0 lfd f31, 25600(r9) ; a double constant
82266CD4 lwz r9, 16(r26) ; local[16]
🔑 isl.py's SYM1_SLOTS already lists slot 12 for built-in 7, derived
purely from operand ranges. Reading the implementation shows the mechanism —
local[12] is indexed into [phase+244], which
isl-bytecode.md documents as symbol table 1 (routes,
messages, objectives). Two independent methods, same conclusion.
That makes the Stage 02 phase-2 condition
builtin7(TCT206, 1, Route_TCT206_p2S, 4294967295, 500) == 1
structurally coherent — a unit, a route symbol, and two numbers — but what
it asks about the route is not read, so it stays builtin7.
🟡 builtin141 — only the entry read
8226C9D4 lwz r11, 324(r29) ; unit array
8226C9D8 lwz r10, 4(r31) ; local[4]
8226C9E8 lwz r8, 4(r8) ; handle
8226C9F0 bc … 0x8226CA0C ; if alive, continue
8226C9F4 addi r11, r0, 0
8226C9F8 stw r11, 164(r29) ; dead -> special[0] = 0
The same unit-array opening as every unit predicate, returning 0 when the unit is
gone. Everything past 0x8226CA0C is unread. Stage 16 calls it twice with
arguments differing in one position (0 vs -4000), which looks like a
coordinate — and looking like one is precisely the evidence this corpus does not
accept.
🟡 Not settled
- No name for any of the three. For 104 that needs the meaning of
r29at0x821AAD9Cand of the(16, 32]gate; for 7 and 141, their bodies past the entry checks. builtin103's fields[phase+10152]/[phase+10156]have 9 and 7 writers, none read.- This does not change any artefact — the conditions already printed
builtin104,builtin7,builtin141, and still do.