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/mission-phase-timers.md
Sylpheed RE agent 0cf75f998f 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.
2026-08-25 20:37:37 +00:00

3.5 KiB
Raw Blame History

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