This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-condition-builtins.md
Sylpheed RE agent 2a1236074b re: decode the config records -- render/input constants, and a correction
parse() + named() on each named record (data/config-records.txt).

The find, directly portable: Rendering carries the tone-mapping and
bloom numbers (Brightness 1.4, Contrast 1.0, ExposureKeyValue 0.18,
BrightPassThreshold 0.6, LuminanceMin/Max 0.15/3.6, BloomScale 0.5,
AfterimageScale 0.05, StarScale 1.20, GlareType 8), ControlTweak the
input constants (CameraSpeed 0.50, TargetSpeed 0.50, TargetMovingRange
-10..10, mov_trigger_play 20, eye/mov_stick_play 6000), and Camera
NoseCameraFOV 0.92 / ChangeTime 0.20.

Corrects my own last entry: WEAPONS is a 59-entry NAME ROSTER, not a
stat sheet, and UNITS is not units at all -- seven craft-slot/pilot
pairs, the wingman roster.  IGNORE is a 13-name blocklist over the
weapon enumeration.

AUTO_SETTINGS lists exactly 28 stageNN_settings.tbl files -- a third
independent confirmation of the 28-stage count.

The objective-shaped records are UI: SUB_OBJECTIVE is a HUD layout (two
variants ship), SQUADRON_ORDER_OBJECT is 29 .prt sprite names, MISSIONS
holds the challenge categories, STAGES has zero fields.

Docs + one new artefact; ISL artefacts byte-identical.
2026-08-27 12:02:13 +00:00

28 KiB
Raw Permalink 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.

(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 Stage01Stage16 and Stage18Stage29 (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' at 0x820A9C8C has exactly one xref, into sub_822929E0, a generic string helper. Listing every short %d-bearing string in the image — 43 of them — turns up no Stage/_S%02d pattern at all.
  • No precomputed name hash. Using the corpus's own name_hash, the values for Stage_S00…30, Stage00…30, StageNN.ssb and UnitGroup_S00…30 appear 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 <= 32 is the array bound — the table has precisely 33 entries, 0…32, with PLANE at 33 where it stops.
  • kind != 16 is STAGE17 — the one stage number with no .ssb on the disc (files run Stage01Stage16, Stage18Stage29).

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 callr11 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 1config.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.

(2026-08-27) The absence re-tested properly — and the reader is NOT INI-only

🔴 Correction to the label above. sub_82448AA0 is not "the INI config reader". Resolving the r4 string at every call site of it and of sub_824482D0 (artefact data/config-keys.txt) gives 65 section names and 54 int keysMISSIONS, FONTS, StageResource, EPILOGUE_MOVIES, WEAPONS, UNITS, SOUNDS, ACHIEVEMENTS_REQUIREMENTS, LINE_PITCH, MSG_FONT_SIZE, SUBTITLE_Y … far more than the 400-byte config.ini holds. And the decisive check: BASE_INFO, SYSTEM, MISSIONS and FONTS are all present as IDXD record keys on the disc. So this pair is a generic named-section accessor over the IDXD .tbl containers, with config.ini as one small extra input.

⚠️ My first re-test was invalid, and only its control showed it. IDXD record keys are not name_hash — they are tag_hash (case-sensitive, modulus 0x00FFFFDF; unitgroup.py documents it). Searching 190 782 records with name_hash returned 0 for STAGENN_UNIT_MAX and 0 for every control name, which is what exposed it.

Redone with tag_hash:

IDXD entries parsed 7 750 / 7 750 (matches the corpus census)
records scanned 190 782, 3 496 distinct keys
control names found as record keys BASE_INFO, SYSTEM, MISSIONS, FONTS
STAGE01…33_UNIT_MAX records 0

With the literal search (0 in 26 443 decompressed entries) and config.ini printed in full, the section exists nowhere on the disc — not as an INI section, not as an IDXD record. The earlier worry that the GP_HANGAR_ARSENAL word-hit might be real is settled: it is not a record key.

PLANE = 200 / VESSEL = 20 for every stage — now on a controlled test rather than a scan whose reader had never been checked.

(2026-08-27) Stage30Stage33 do not exist — the 33-slot table is headroom

Probing the pak entry names (name_hash, the right hash for entry names) for each stage-table family across all 41 paks:

family stages present
stage\UnitGroup_S%02d.tbl 0116, 1829 — 28 stages
stage\AIParams_S%02d.tbl 0116, 2429 — 22 stages
stage\Stage_S%02d.tbl 0 — expected; stagetbl.py documents that the per-stage record is not name-addressed

The control is built in: 28 hits out of 33 probes for UnitGroup, so the loop resolves entry names correctly. S17 and S30S33 are absent, so the 33-entry STAGENN_UNIT_MAX table is oversized headroom over a 28-stage game.

🔑 An independent corroboration falls out of the second row. AIParams is missing for exactly S18S23 — precisely the six stages this page identifies as the tutorials by a completely different route (their only dominating condition is request_next() != 1). Tutorials ship no AI parameters.

🟡 Who consumes [phase+356] / [phase+360]: not findable by offset

Image-wide there are 243 non-stack lwz reads from +356/+360, spread across the UI, sound, render and script subsystems — the offsets are far too common to attribute. Inside the ISL region the only hits are the two writers in sub_82261F70 and two vtable calls (0x82273400 slot 90, 0x82290ED8). Dropping this sub-item: it needs type information the offset index does not carry.

(2026-08-27) The two-function model is confirmed by a 64/65 vs 0/54 dissociation

Taking every literal key from data/config-keys.txt and testing it against the 3 496 distinct IDXD record keys on the disc (tag_hash):

class argument to present as an IDXD record key
section names sub_82448AA0 64 / 65
field keys sub_824482D0 0 / 54

The two classes are each other's control, and the split is total. It confirms the model exactly:

  • sub_82448AA0(node, NAME) finds a RECORD, keyed by tag_hash(NAME).
  • sub_824482D0(node, KEY) reads a FIELD, whose name is a literal string in the record's pool — so of course none of them is a record key.

🔑 The single absent section is LANGUAGE — which is precisely the one real section in the disc-root config.ini. The same API serves both stores, and the data shows the seam.

So the earlier worry that "64 sections have unknown hardcoded fallbacks" was overblown: all but one are real data on the disc. STAGENN_UNIT_MAX is the genuine exception, and it is absent because its name is built at runtime from the stage index rather than being one of these literals.

📎 The artefact now records which pak each record lives in — tables.pak for the global tables (MISSIONS, STAGES, SOUNDS, SUB_OBJECTIVE, SQUADRON_ORDER_OBJECT, EPILOGUE_MOVIES, ACHIEVEMENTS_REQUIREMENTS, FONTS, BASE_INFO …), GP_HANGAR_ARSENAL.pak for WEAPONS, UNITS, Camera, ControlTweak, Rendering, AUTO_SETTINGS, IGNORE, and the per-language GP_MAIN_GAME_* paks for the enum tables.

(2026-08-27) The records DECODED — and my "canonical definitions" guess was half wrong

parse() + named() on each record (artefact data/config-records.txt):

Real, directly portable constants — these are the find:

Rendering      Brightness 1.4   Contrast 1.0   Saturation 1.00   Hue 0.00
               ExposureKeyValue 0.18   BrightPassThreshold 0.6
               BrightPassOffset 1.5    LuminanceMin 0.15  LuminanceMax 3.6
               BloomScale 0.5   AfterimageScale 0.05   StarScale 1.20
               GlareType 8      ColorLayer R/G/B/A 0.00
ControlTweak   CameraSpeed 0.50   TargetSpeed 0.50
               TargetMovingRange -10.0 … 10.0
               mov_trigger_play 20   eye_stick_play 6000   mov_stick_play 6000
Camera         NoseCameraFOV 0.92   ChangeTime 0.20

🔴 But WEAPONS and UNITS are not what I called them. Last iteration I said GP_HANGAR_ARSENAL.pak holds "the canonical weapon/unit definitions". Half right at best:

  • WEAPONS is a 59-entry name rosterNo_Equipment, Machiene_Cannon_MG1, Broad_Sword_SG1, Frail_GP37, Stiletto_BG1, Pilum_BP… — an enumeration of ids, not a stat sheet.
  • UNITS is not units at all: seven fields pairing a craft slot with a pilot — Bird1-Sandra, Bird2-Billy, Bird3-Antonius, Bird4-Carl, Rhino1-Raymond, Rhino2-Katana, Rhino3-Ellen. The wingman roster.
  • IGNORE is a 13-name blocklist over that roster — Long_Spear_HBP, Twin_Saber_LG2H, Eagle_120AM, Maelstrom_Bomb, Cluster_Mine_B10, and the placeholders Wep_82, Wep_85. Weapons that exist in the enumeration but are withheld.

🔑 AUTO_SETTINGS lists exactly 28 filesstage01_settings.tblstage29_settings.tbl — a third independent confirmation of the 28-stage count, after the .ssb census and the UnitGroup_SNN.tbl probe.

🟡 And the UI records are UI, not gameplay. SUB_OBJECTIVE is a HUD layout — MAX_OBJECTIVE_OFFSET 353,242, TEXT_OFFSET 85,271, NUM_QUALIFY_OFFSET 309,242, DOT_OFFSET 0,20 — with a second variant (275,222 / 85,251 / 231,222 / 0,0) in one pak, so there are two layouts. SQUADRON_ORDER_OBJECT is 29 .prt sprite names for the squadron-order menu. MISSIONS holds six positional members — TimeAttack, ScoreAttack, Extra01Extra04, the challenge categories, not the campaign. STAGES has zero fields.

🟡 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.