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.
596 lines
28 KiB
Markdown
596 lines
28 KiB
Markdown
# 🟡 The three built-ins that appear *inside* clear conditions — read, not named
|
||
|
||
[`isl-phase-guards`](isl-phase-guards.md) 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]` 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 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'` 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 `Stage01`–`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.
|
||
|
||
### ✅ (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 keys** — `MISSIONS`, `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) `Stage30`–`Stage33` 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` | **01–16, 18–29 — 28 stages** |
|
||
| `stage\AIParams_S%02d.tbl` | 01–16, **24–29 — 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 S30–S33 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 **S18–S23** — 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 roster** — `No_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 files** — `stage01_settings.tbl` …
|
||
`stage29_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`, `Extra01`…`Extra04`, 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 *mechanism* —
|
||
`local[12]` is indexed into `[phase+244]`, which
|
||
[`isl-bytecode.md`](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.
|