mission-phase-timers.md left 180 open as "a limit and a warning
threshold is the obvious reading, but not established". Reading
sub_822639B8 -- ScriptPhase::Update 0x82263528, same dt as the
stopwatch bank -- settles it the other way:
if running: [+304] += dt
if armed: [+308] -= dt while [+308] > 0
else [+312] -= dt, clamped at 0
[+312] is never compared with [+308]; it is decremented, and only in
the A<=0 arm. Two sequential countdowns. Disc-wide the second
argument is 180 in all 29 timer_set sites while the first varies
(600 x19, 1200 x8, 900, 1800).
Built-ins 123-127 are vtable slots 90-94 on five scalars at
[phase+304..320]. 125 and 126 have ZERO call sites in all 28 scripts:
the script arms, starts and stops this clock but never reads it.
Corrects mission-phase-timers.md, which merged this clock with
stopwatch 0 -- timer_resume starts [+304], set_flag(0) one instruction
later starts the stopwatch the timeline's kind=0 reads.
Docs only; all seven ISL artefacts regenerate byte-identical.
136 lines
6.3 KiB
Markdown
136 lines
6.3 KiB
Markdown
# What starts a phase's coroutines: a timer table, scanned every frame
|
||
|
||
Status: ✅ mechanism and table format, verified against the disassembly and all
|
||
28 scripts. This answers the question every phase experiment has been circling.
|
||
|
||
## ✅ The scanner
|
||
|
||
`sub_822748D0`, called from `ScriptPhase::Update` each frame:
|
||
|
||
```
|
||
822748e0 lwz r11, 240(r30) ; the table at [phase+240]
|
||
822748e4 lwz r10, 4(r11) ; record count
|
||
822748f4 addi r31, r11, 20 ; first record (+8 header, +12 into it)
|
||
822748fc lwz r11, 0(r31) ; TIMER INDEX (rec+20)
|
||
82274900 lfs f0, -8(r31) ; THRESHOLD, f32 (rec+12)
|
||
82274904 lwz r10, 104(r30) ; PREVIOUS timers [phase+104]
|
||
82274910 fcmpu f13, f0 / bgt ; skip if prev > thr
|
||
82274918 lwz r10, 88(r30) ; CURRENT timers [phase+88]
|
||
82274920 fcmpu f0, f13 / bge ; skip if thr >= cur
|
||
8227492c lwz r5, -16(r31) ; COROUTINE OFFSET (rec+4)
|
||
82274930 lwz r4, 232(r30) ; phase code base
|
||
82274934 bl 0x822737C8 ; START THE COROUTINE
|
||
8227493c addi r31, r31, 24 ; stride 24
|
||
```
|
||
|
||
So each record says: **when timer *i* crosses *t* seconds, start the coroutine at
|
||
`codebase + off`** — a **rising-edge** test, `prev ≤ t < cur`, so it fires once.
|
||
|
||
`[phase+88]` and `[phase+104]` are the current and previous copies of the
|
||
32-entry float register file; `sub_822710D0(phase, dt)` copies cur→prev and adds
|
||
`dt` to the running ones each frame. **They are timers, in seconds.**
|
||
|
||
## ✅ The table, and where it comes from
|
||
|
||
The mission-level bytecode's `begin_phase` (op `0x83`) carries five operands; the
|
||
fourth is this table's offset and the fifth is the phase's end-event routine.
|
||
`sub_82270DF8` stores them as `[phase+240] = base + w4` and `[phase+236] = w5`.
|
||
|
||
On disc the table is tagged constants, same `<len><op>` shape as the bytecode
|
||
(tags `0x819` = int, `0x81A` = float — both past the ISL dispatcher's
|
||
`cmplwi 0x18` bound, so they are data and never execute):
|
||
|
||
```
|
||
tbl+0 : 0819 <count>
|
||
rec+0 : 0819 <coroutine offset>
|
||
rec+8 : 081A <threshold seconds, f32>
|
||
rec+16 : 0819 <timer index>
|
||
```
|
||
|
||
Verified here on Stage 02's three phases — counts **25 / 13 / 18**, and the tag
|
||
triple `(0x819, 0x81A, 0x819)` correct in **25/25, 13/13, 18/18** records:
|
||
|
||
```
|
||
phase 1 base 0xE4 tbl 0x14848 end 0x1482C
|
||
rec0: off 0x2B20 thr 0.0 timer 5
|
||
rec1: off 0x2B88 thr 0.5 timer 5
|
||
rec2: off 0x2BFC thr 1.0 timer 5
|
||
```
|
||
|
||
Only two timers are ever used corpus-wide: **0** (thresholds 1–1170 s — the
|
||
mission clock) and **5** (0/0.5/1/4/5 s — a phase-intro clock).
|
||
|
||
## 🔑 Why this matters: it explains the poke results
|
||
|
||
Two experiments set a squadron's state to "destroyed" and watched nothing happen
|
||
([script-runtime-probe](script-runtime-probe.md)). The leading explanation was
|
||
that the condition coroutine "is not polling" — **this is why.** A phase's
|
||
coroutines are **started on a schedule**, by timer crossings. They are not
|
||
running continuously waiting to notice a state change, so writing state between
|
||
firings changes data nobody is looking at.
|
||
|
||
It also reframes the arrival timetable: the routes' `t=170` entry
|
||
([mission-wave-arrivals](mission-wave-arrivals.md)) and these thresholds are the
|
||
same kind of thing — **the mission is substantially a timeline**, with unit
|
||
predicates deciding *what* happens at each scheduled point rather than *when*.
|
||
|
||
## ✅ Stage 02 phase 1, as a timeline — and what arms the clock
|
||
|
||
Disassembling all 25 triggers in threshold order turns the phase into a script
|
||
you can read:
|
||
|
||
```
|
||
timer5 @ 0.0s fade in
|
||
timer5 @ 0.5s play_bgm(0x3ED)
|
||
timer5 @ 1.0s damage_unit(TCN131, 0)
|
||
timer5 @ 4.0s timer_set(1200, 180) ; timer_resume ; set_flag(0) <-- arms timer 0
|
||
timer0 @ 1.0s radio 0x52, 0x53
|
||
timer0 @ 30.0s deploy + move_order(TCN105 group) ; radio 0x55
|
||
timer0 @ 60.0s radio 0x56
|
||
timer0 @ 90.0s deploy + move_order(idx 0x08) ; radio 0x57
|
||
timer0 @ 120.0s deploy + move_order(idx 0x43) + objective_marker ; radio 0x58, 0x59
|
||
timer0 @ 170.0s deploy + move_order(idx 0x01 = ADN110) ; radio 0x5A, 0x5B
|
||
timer0 @ 210.0s deploy + move_order(idx 0x33) + objective_marker
|
||
timer0 @ 240.0s deploy + move_order(idx 0x19)
|
||
timer0 @ 270/300/330 s radio only
|
||
timer0 @ 1020/1080/1140/1170 s radio only
|
||
```
|
||
|
||
### ✅ What arms the clock — the open question, answered
|
||
|
||
**`timer_set(1200, 180)` then `timer_resume`, at 4.0 s on the phase-intro
|
||
clock.** So timer 0 is started by the phase's own intro coroutine, with a
|
||
**1200-second limit**; timer 5 is already running when the phase begins. That
|
||
closes "which built-in arms or resets each timer" for the common case.
|
||
|
||
### ✅ It independently confirms the arrival measurement
|
||
|
||
The `timer0 @ 170.0s` trigger deploys **symbol index 0x01 = `ADN110`** — the
|
||
first of the three squadrons the phase-1 condition polls. The live run measured
|
||
those three flipping to *active* at roughly **155–165 s of mission time**
|
||
([mission-wave-arrivals](mission-wave-arrivals.md)), against a route table that
|
||
also says `170`. Three independent sources — the route table, this trigger
|
||
table, and the running game — agree.
|
||
|
||
## 🟡 Not settled
|
||
|
||
* Whether a coroutine started this way can **re-arm** its own trigger.
|
||
* ~~`timer_set`'s second argument (`180`) — a limit and a warning threshold is the
|
||
obvious reading, but it is **not** established.~~
|
||
✅ **SETTLED (2026-08-27), and the obvious reading is REFUTED —
|
||
[structures/isl-mission-timer](structures/isl-mission-timer.md).** `180` is a
|
||
**second countdown that starts only after the first reaches zero**, not a
|
||
threshold on the first: `sub_822639B8` never compares `[phase+312]` with
|
||
`[phase+308]`, it *decrements* it, and only in the `A <= 0` arm. It is 180 in
|
||
all 29 `timer_set` sites on the disc while the first argument varies
|
||
(600 ×19, 1200 ×8, 900, 1800).
|
||
|
||
🔴 **And this page conflates two clocks.** "timer 0 is started by
|
||
`timer_set(1200, 180)` then `timer_resume`" merges the **mission timer**
|
||
(`[phase+304…320]`, armed by 127 and started by 123) with **stopwatch 0**
|
||
(`[phase+88][0]`, started by `set_flag(0)` — [structures/isl-timers](structures/isl-timers.md)).
|
||
The three calls sit one instruction apart in the same intro coroutine, so the
|
||
two clocks read almost the same value and the merge was invisible. The
|
||
timeline's `kind = 0` reads the **stopwatch**; the 1200-second limit belongs to
|
||
the **mission timer** and is not stopwatch 0's limit.
|