Files
Sylpheed/docs/re/mission-phase-timers.md
Sylpheed RE agent a09c9e6517 re: timer_set's second argument is a SECOND countdown, not a warning threshold
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.
2026-08-27 09:03:18 +00:00

136 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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 **155165 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.