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/structures/isl-mission-timer.md
Sylpheed RE agent d79f59d24e 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

5.0 KiB
Raw Blame History

The MISSION timer is a second, separate clock — and timer_set's second argument is a second COUNTDOWN

mission-phase-timers.md left this open: "timer_set's second argument (180) — a limit and a warning threshold is the obvious reading, but it is not established." It is established now, and the obvious reading is wrong.

This clock has nothing to do with the 32 stopwatches in isl-timers; it is five separate scalar fields on the same ScriptPhase, driven by its own updater.

The five fields

Zeroed together by sub_8225FEF8 (0x822600840x82260098):

field type meaning
[phase+304] f32 elapsed, counts up
[phase+308] f32 countdown A, counts down
[phase+312] f32 countdown B, counts down only after A reaches 0
[phase+316] int running — gates the elapsed counter
[phase+320] int armed — gates the two countdowns

The five built-ins

All are stubs into the ScriptPhase vtable, slots 9094 (0x8226C6900x8226C6F0):

built-in slot what it does calls disc-wide
127 timer_set(a, b) 94 armed = 1; A = (f32)local[0]; B = (f32)local[1] 29
123 timer_resume 90 running = 1 40
124 timer_stop 91 running = 0; armed = 0 170
125 timer_reset 92 elapsed = A = B = 0.0; armed = 0 0
126 timer_elapsed 93 returns elapsed as a double in [phase+176] 0

timer_set reads its two arguments as doubles at 0(r4) and 8(r4), r4 being the converted locals array — so local[0] and local[1].

The update — sub_822639B8(phase, dt)

Called from ScriptPhase::Update at 0x82263528, with fmr f1, f30 — the same dt the stopwatch bank gets at 0x82263480. So this clock counts the same seconds (isl-timers), and 600 / 900 / 1200 / 1800 and 180 are 10 / 15 / 20 / 30 minutes and 3 minutes.

822639C8  if [+316] != 0:  [+304] += dt          ; elapsed counts UP
822639E4  if [+320] != 0:
822639F4      if [+308] > 0.0:  [+308] -= dt     ; A counts down ...
82263A00      else:             [+312] -= dt     ; ... then B does
82263A10                        if [+312] < 0.0: [+312] = 0.0

🔴 That refutes "a limit and a warning threshold"

A threshold would be compared against the countdown. [+312] is never compared with [+308] anywhere. It is decremented, and the bc 12, gt at 0x822639FC jumps away to the A -= dt arm — so B -= dt runs only in the A <= 0 branch. The two are sequential countdowns: a per-mission period, then a fixed second period.

Every mission agrees, and the second value never varies

29 timer_set sites across the disc:

first argument sites
600 19
1200 8
900 1 (Stage 26)
1800 1 (Stage 24)
second argument 180 in all 29

A per-mission limit that varies, followed by a fixed 3-minute stage. Note this constancy is consistent with the sequential reading and does not by itself establish it — the disassembly above is what does.

What it feeds — and what it does not

Each frame the updater allocates a 32-byte message (type tag 0xAB03E5BA, vtable 0x820A8D68) and posts it to the bus at [0x828F35DC], carrying:

+24 = [phase+304]   elapsed
+28 = [phase+312]   countdown B
+16 = running       ([+316] != 0)
+20 = expired       ([+320] == 1 && [+308] <= 0.0)

expired is exactly "countdown A has run out" — the point at which B starts.

🔑 The script never reads this clock. timer_elapsed and timer_reset have zero call sites in all 28 scripts; the script only arms (127), starts (123) and stops (124) it. Its only consumer read here is the posted message.

🔴 A conflation in mission-phase-timers.md, corrected

That page says timer 0 "is started by timer_set(1200, 180) then timer_resume, with a 1200-second limit". Those are two different clocks armed back to back:

timer_set(1200, 180)   ->  this object: A = 1200, B = 180, armed
timer_resume           ->  this object: elapsed starts counting
set_flag(0)            ->  STOPWATCH 0 starts (isl-timers.md)

The timeline's kind = 0 reads stopwatch 0, not [phase+304]. The two start in the same coroutine one instruction apart, which is why they read almost the same value and the conflation was invisible. The 1200-second limit belongs to the mission timer; it is not stopwatch 0's limit.

🟡 Not settled

  • Who subscribes to 0xAB03E5BA. The literal appears at exactly one site — the producer — but 0xAB03 is the high half of many message tags in this image and dispatch is not by literal compare, so a single occurrence is not evidence that nothing consumes it. Whether running out of time ends the mission is therefore unread.
  • What B counting to zero does. Nothing observed acts on it.
  • Whether timer_stop's 170 sites are all phase teardown, or some are a real mid-mission stop.