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 0b06e29d58 re: Stage 02 phase 1 read as a timeline; what arms the mission clock
Disassembling all 25 timer triggers in threshold order turns the phase into
readable script: fade, BGM, then squadron deployments at 30/60/90/120/170/210/
240s each as deploy + move_order + objective_marker, with radio messages
interleaved and a late block at 1020-1170s.

Answers the open question from the previous entry: timer 0 is armed by
timer_set(1200, 180) followed by timer_resume, fired at 4.0s on the phase-intro
clock (timer 5), which is already running when the phase begins. So the mission
clock has a 1200-second limit.

And 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 going active at
~155-165s of mission time, and the route table also says 170. Three independent
sources agree -- the route table, the trigger table, and the running game.

Open: whether a coroutine can re-arm its own trigger, and timer_set's second
argument (180), for which 'warning threshold' is a guess rather than a finding.
2026-08-25 20:43:07 +00:00

5.2 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 11170 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.

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 155165 s of mission time (mission-wave-arrivals), 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.