sub_8226DAF8 past the route-name lookup: it resolves the name through sub_823012D8,
requires the unit record's rec+100 != 0, pulls TWO three-double vectors out of that
record -- rec+32/+40/+48 and rec+64/+72/+80 -- and then makes a virtual call:
8226DC1C lwz r11, 0(r30) ; the ScriptPhase vptr
8226DC28 lwz r11, 60(r11) ; vtable slot 60
8226DC30 bcctrl ; test(phase, A, B, P = the route, f1 = node+16)
Slot 60 resolves to sub_82268068, which no built-in uses, so it is engine-internal.
Its arithmetic leaves no room for interpretation: per-axis fsub, fmul plus two fmadd,
fsqrt, giving |B - A|; reject if that is under 0.1 (the constant is literally 0.1, a
degenerate-segment guard); reject if |P - A| > f1 or |P - B| > f1; then compute
t = (P-A).(B-A) / |B-A|^2 and reject if t < 0.
That is a point-to-segment distance test, and it makes the node's double -- built-in
19's local[24] -- a RADIUS. So a kind-0 trigger fires when a route point lies within
that radius of the segment between two vectors from the unit's record.
Recorded as unproven: WHAT A and B are. A previous/current position pair would make
this the standard frame-rate-robust waypoint test, and it would be easy to write that
down -- which is exactly why it is not written down. rec+64/72/80 and rec+32/40/48
are two positions and nothing here says which.
All artefacts regenerate byte-identical; documentation only.
Still unread: sub_8226DC80, the kind-1 condition, entirely; and slot 60 past the
t < 0 rejection, where a second constant at 0x820B0000+25192 is loaded.
12 KiB
✅ The trigger node's layout — from a matched writer/reader pair
isl-trigger-queue located the appender (built-in 25). The node's contents are now read, because the reader was found too and the two agree field for field.
The reader: sub_8226E220
sub_8226D740 — the per-frame engine→script drain — calls it as
8226D758 addi r26, r29, 272 ; r29 = the phase, so r26 = the TRIGGER CONTAINER
8226D79C or r3, r26, r26
8226D7A0 bl 0x8226E220 ; (container, &out88, &out84, &out80,
; &out120, &out92, &out112)
and it pops one node:
8226E228 lwz r10, 20(r11) ; the PENDING COUNT -- return 0 if empty
8226E244 lwz r10, 16(r11) ; the list head
8226E250 stw r10, 24(r11) ; a cursor
8226E254 lwz r11, 8(r10) ; -> the node PAYLOAD
✅ The layout, confirmed from both ends
| payload | written by built-in 25 | read by sub_8226E220 |
source |
|---|---|---|---|
| +0 | stw r26, 0(r30) |
→ out88 | local[4] — the unit |
| +4 | stw r28, 4(r30) |
→ out84 | local[12] |
| +8 | stw r27, 8(r30) |
→ out80 | computed |
| +16 | stfd f31, 16(r30) |
→ out120 (a double) | local[16] |
| +24 | stw r25, 24(r30) |
→ out92 | the constant 1 |
| +28 | stw r28, 28(r30) |
→ out112 | local[12] again |
| +32 | stw r11, 32(r30) |
→ out116 | — |
| +36 | stw r23, 36(r30) |
not read here | — |
Container: +16 head, +20 pending count, +24 cursor. The +20 matches
the count isl-builtins.md watched live at phase+272+20, from a completely
different direction.
That the writer and the reader touch the same seven offsets, in the same order, is the check — neither was derived from the other.
payload+0 being local[4] also agrees with UNIT_ARG, which lists built-in 25
and was derived from operand ranges.
🔴 Two corrections, both mine
[node+112] does not exist
isl-coroutine-spawner recorded the drain's spawns
as taking their offset from [node+112]. r31 = r1 - 256 — it is the
stack frame, so +112 is an output slot, not a node field. Withdrawn.
And the trigger does NOT carry a routine offset
The follow-up hypothesis — that payload+28 reaches the spawner and so names the
routines nothing else starts — does not survive. payload+28 is local[12], and
across all 25 call sites disc-wide those values are the small integers
1 … 12:
| land on the instruction stream | 1 / 25 = 4.0 % |
| control, random 4-aligned offsets | 16.0 % |
| are unreached run-starts | 0 |
Below chance. They are indices of some kind, not code offsets.
⚠️ My first version of this test used n = 2 — Stage 02 only. It happened to give the same answer, but two samples could not have supported it either way.
✅ One correction to the corpus, in the other direction
isl-builtins.md withdrew the idea that sub_8226E458 touches the trigger
queue, reasoning: "At 0x8226D780 the argument is lwz r4, 324(r29) —
[ScriptPhase+324], the unit array, not the trigger container."
The call is sub_8226E458(r3 = r26, r4 = [phase+324]), and r26 was set to
phase + 272 twelve instructions earlier. The withdrawal read the second
argument. sub_8226D740 does operate on the trigger container; what
sub_8226E458 does to it is still unread, so only the argument is corrected
here, not the conclusion.
✅ RESOLVED: there are TWO appenders, and only one carries a routine
The puzzle above — payload+28 holding a 1–12 index while the drain hands it to
a spawner that wants a code offset — had a simple cause: built-in 25 is not the
only thing that appends a trigger node.
Searching for functions that write a double at +16 and a word at +28 of the same register finds exactly two:
| builder | how |
|---|---|
sub_8226A0D8 — built-in 25, vtable slot 28 |
fills the payload inline |
sub_8226E160 |
a free-list recycling variant, called from sub_82269ED0 |
sub_82269ED0 is vtable slot 68 = built-in 19, and it calls the builder as
8226A034 addi r3, r28, 272 ; the trigger container
8226A040 lwz r4, 4(r30) ; local[4] -> node+0 (the unit)
8226A03C lwz r5, 12(r30) ; local[12] -> node+4
8226A038 lfd f1, 24(r30) ; local[24] -> node+16 (double)
8226A030 lwz r8, 32(r30) ; local[32] -> node+28
✅ Built-in 19's local[32] IS a code offset
| over all 79 call sites disc-wide | |
|---|---|
| land on the instruction stream | 79 / 79 = 100.0 % |
| control, random 4-aligned offsets | 27.8 % |
| value range | 12 164 … 111 080, 73 distinct |
Large, distinct, and every one resolves — against a control that resolves barely
a quarter of the time. So built-in 19 registers a trigger whose handler is a
routine, and the drain's base + payload+28 spawn is exactly that handler.
Built-in 25's payload+28 really is a small index; the two builders simply put
different things in the same slot. Observed alongside it: built-in 25 writes
+24 = 1 and built-in 19 writes +24 = 0, which is the shape of a kind
tag — 🟡 but the drain was not shown branching on it, so that is an observation,
not a reading.
✅ +24 IS the kind tag — a three-way dispatch in the drain
Read straight off the drain, immediately after the pop:
8226D7AC lwz r11, 92(r31) ; payload+24
8226D7B8 cmpli cr6, 0, r11, 0x1
8226D7C4 bc 12, lt, 0x8226D89C ; +24 == 0 -> the SPAWN path
8226D7C8 bc 4, eq, 0x8226DA44 ; +24 > 1 -> bail
; +24 == 1 -> fall through, the OTHER path
+24 |
written by | path | condition test | what +28 is |
|---|---|---|---|---|
| 0 | built-in 19 | 0x8226D89C → bl 0x822737C8 |
sub_8226DAF8 |
a code offset (79/79 vs a 27.8 % control) |
| 1 | built-in 25 | falls through at 0x8226D7CC — no spawn |
sub_8226DC80 |
an index into [phase+244] |
The non-spawn path is explicit about it:
8226D834 lwz r10, 112(r31) ; payload+28
8226D838 lwz r11, 244(r29) ; [phase+244] = SYMBOL TABLE 1
8226D848 lwzx r10, r10, r11 ; resolve it as a symtab-1 index
✅ And built-in 25's +28 resolves to a ROUTE
Over all 25 call sites disc-wide, local[12] read through symbol table 1:
25 of 25 resolve, and every name is a route —
Route_ADS101_p1F, Route_ADT107_p1F, Route_TCN004_p1S, Route_ADT202A_p2S, …
So the 1–12 values that looked like a puzzle are simply small symbol indices.
This also confirms isl.py's SYM1_SLOTS, which lists slot 12 for built-in 25
and was derived from operand ranges alone — two independent routes, same
conclusion.
The two built-ins register two kinds of trigger: built-in 19 a trigger whose payload is a handler routine, built-in 25 one whose payload is a route.
✅ BOTH trigger kinds watch a ROUTE
The kind-0 condition tester sub_8226DAF8 resolves a symbol too — from
payload+4, not +28:
8226DB14 rlwinm r10, r5, 2, 0, 29 ; r5 = payload+4, x4
8226DB24 lwz r11, 244(r30) ; [phase+244] = SYMBOL TABLE 1
8226DB3C lwzx r10, r10, r11 ; resolve it
8226DB50 addi r4, r11, 4 ; -> the name string
8226DB54 bl 0x8217FA08
Both appenders write local[12] into the slot their path reads — built-in 19
into +4, built-in 25 into +4 and +28. So the same operand is the symbol
in both cases, and it resolves:
local[12] through symbol table 1 |
symbol type | |
|---|---|---|
| built-in 19 | 79 / 79 | all type 1 |
| built-in 25 | 25 / 25 | all type 1 |
isl-builtins.md records symtab-1 type 1 as the Route_* names (1362
entries), and the samples bear it out: Route_ADN106_p1F, Route_ADN1xxe1_p1F,
Route_ADS101_p1F, Route_TCN004_p1S.
So both built-ins register a trigger on a UNIT reaching a ROUTE — payload+0
is the unit, local[12] the route. They differ only in what happens when it
fires: built-in 19 additionally carries a handler routine (local[32] →
payload+28, kind 0 → spawn); built-in 25 carries none (kind 1 → no spawn).
This also confirms isl.py's SYM1_SLOTS, which lists slot 12 for both 19
and 25 and was derived from operand ranges alone — a third independent agreement
with that table.
🔑 It joins up with the closed REMAINING OB result, where the counter was
measured to rise at a squadron's route arrival time. Route triggers are the
mechanism that would do that. 🟡 Stated as a connection, not a demonstration —
nothing here traces a trigger to that counter.
✅ What the kind-0 condition TESTS — a point-to-segment proximity check
sub_8226DAF8 past the route-name lookup:
8226DB84 bl 0x823012D8 ; look the route name up -> a route object; bail if absent
8226DBB8 lwz r11, 324(r30) ; the unit array
8226DBBC rlwinm r10, r26, 2,0,29; r26 = payload+0, the unit index
8226DBC8 lwz r10, 100(r11) ; require rec+100 != 0, else return 0
8226DBD4 lfd f0, 32(r11) … ; THREE doubles: rec+32, +40, +48 -> out vector B
8226DBF8 lfd f0, 64(r11) … ; THREE doubles: rec+64, +72, +80 -> out vector A
8226DBFC fmr f1, f31 ; the node's double (payload+16)
8226DC1C lwz r11, 0(r30) ; the ScriptPhase vptr
8226DC28 lwz r11, 60(r11) ; VTABLE SLOT 60
8226DC30 bcctrl ; test(phase, A, B, P=the route, f1)
Slot 60 → sub_82268068, which no built-in uses — it is engine-internal — and
its arithmetic is unambiguous:
f29 = |B - A| ; fsub per axis, fmul + 2x fmadd, fsqrt
if |B - A| < 0.1 -> 0 ; degenerate segment (the constant is 0.1)
f28 = |P - A| ; if > f1 -> 0
|P - B| ; if > f1 -> 0
t = (P-A)·(B-A) / |B-A|² ; the projection parameter
if t < 0.0 -> 0
Per-axis fsub, a sum of three squares, fsqrt, and a dot product divided by
the squared length: this is a point-to-segment distance test, and the node's
double is therefore a RADIUS.
So a kind-0 trigger fires when a route point lies within local[24] of the
segment between two vectors taken from the unit's record.
🟡 What A and B are is not established. rec+64/72/80 and rec+32/40/48 are
two positions; a previous/current pair would make this the standard
frame-rate-robust waypoint test, and that is exactly the kind of tidy reading
this corpus makes itself prove. Not proven here.
🟡 Not settled
What the drain actually spawns.✅ Resolved above: built-in 19's nodes carry a real routine offset at+28.Whether✅ It does — see above.+24selects between the two node kindsWhat the kind-0 condition tests✅ Read above.sub_8226DC80(kind 1) is still unread entirely.- Which unit-record fields A and B are —
rec+64/72/80vsrec+32/40/48. - The rest of slot 60 past the
t < 0rejection (a second constant at0x820B0000+25192is loaded next). payload+8is a computed value in both appenders (r29-1in built-in 19) and is passed to both testers; a waypoint index would fit, which is exactly why it is not being called one.- What the kind-1 path DOES with the route past
sub_8217FA08/sub_8218CC88/sub_8225FEA0is unread. - The drain's second spawn site (
0x8226DA10) is in a later block and was not traced. - This still does not explain the unreached code. All 79 of built-in 19's handler offsets are already reachable — 0 are unreached run-starts.
sub_8226E458's effect on the container.- What starts the ~15 % of unreached code — still open, and this was not it.