From 0cf75f998f23f5d68402573f49f936f96a30fa3a Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 20:37:37 +0000 Subject: [PATCH] re: phase coroutines are started by a per-frame timer table sub_822748D0, called from ScriptPhase::Update every frame, walks the table at [phase+240]: stride 24, each record carrying a coroutine offset (rec+4), an f32 threshold (rec+12) and a timer index (rec+20). It compares the previous and current copies of the 32-entry float register file ([phase+104] and [phase+88]) and starts the coroutine at codebase+off on a RISING EDGE, prev <= t < cur, so each fires once. Verified instruction by instruction. The table comes from the mission-level begin_phase opcode 0x83's fourth operand (the fifth is the phase end-event); sub_82270DF8 stores them at [phase+240] and [phase+236]. On disc it is tagged constants -- 0x819 int, 0x81A float, both past the ISL dispatcher's bound so they never execute. Verified on Stage 02: counts 25/13/18 with the tag triple correct in every record. Only timers 0 (1-1170s, mission clock) and 5 (0-5s, phase intro) are used corpus-wide. This explains the poke results. Two experiments set a squadron to 'destroyed' and nothing happened; the leading explanation was that the condition coroutine is not polling. It is not -- coroutines are started on a schedule by timer crossings, so state written between firings is read by nobody. It also reframes the arrival timetable: the routes' t=170 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. Open: which built-in arms or resets each timer. --- docs/re/mission-phase-timers.md | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/re/mission-phase-timers.md diff --git a/docs/re/mission-phase-timers.md b/docs/re/mission-phase-timers.md new file mode 100644 index 00000000..f6a82b08 --- /dev/null +++ b/docs/re/mission-phase-timers.md @@ -0,0 +1,81 @@ +# 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 `` 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 +rec+0 : 0819 +rec+8 : 081A +rec+16 : 0819 +``` + +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*. + +## 🟡 Not settled + +* **Which built-in arms or resets each timer.** Indices 0 and 5 are named from + the `+= dt` accumulator and the threshold magnitudes, not from a writer. +* Whether a coroutine started this way can *re-arm* its own trigger.