Files
Sylpheed/docs/re/structures/isl-condition-builtins.md
Sylpheed RE agent abbd0cfb32 re: the (16,32] gate is != 16 && <= 32, a validity check on a load parameter
Corrects this corpus's own notation: there is no lower bound, values
below 16 pass.

sub_8225EC78(X, kind, ...) -- the function carrying "script load
cancel" -- stores its second argument into X+12 and applies the same
two tests to it immediately, bailing out of the load.  So the gate is
the object's invariant, re-checked at every later touch, not a
selector.  42 sites image-wide have the shape, 34 of them in one code
region plus 3 in sub_8225EC78.

Object chain: X = [GamePart+104] is the current script instance
(installed at 0x821A78EC, previous one torn down via sub_8225EB60);
X+4 -> Y; Y+4 -> the ScriptPhase; Y+72 and [ScriptPhase+10152] get
the answer.

Refutes my own lead in the same pass: "X+12 is a GamePart id and 16 is
the unregistered one" fails its control -- 1, 2 and 18 are also absent
from the RegisterToFactory list, so 16 is one of four gaps.

Docs only; artefacts byte-identical.
2026-08-27 09:48:36 +00:00

11 KiB
Raw Blame History

🟡 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 (S18S23) 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 members0x20FFFF00, 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] is REQUEST_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 S18S23 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 16cmpi 32 shape, 34 of them in one code regionsub_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+4Y, 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.

🟡 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 mechanismlocal[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 r29 at 0x821AAD9C and 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.