The move I flagged last iteration -- "0xAB03E5BA is built once, so
nothing consumes it" -- is refuted by its own control: 25 of the 40
distinct 0xAB03xxxx tags in the image are built exactly once (62.5 %),
and no 4-aligned word anywhere in the image begins 0xAB03, so tags are
never stored statically.
What does stand:
* sub_82175C20(bus+4, &msg) is a ring-buffer PUSH, not a dispatch.
* The message class has a one-method vtable {scalar deleting dtor}
with an MSVC RTTI locator at vtable[-1] -- no handler of its own.
* The bus at [0x828F35DC] has a map at +8216, looked up via
sub_82254A08 from sub_823001E8, whose only caller is the per-frame
pump sub_8230C398.
So the item is NOT settled: the insert path into bus+8216 is unread,
and whether running out of time ends the mission is still unknown.
Docs only; ISL artefacts regenerate byte-identical.
7.1 KiB
✅ 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 (0x82260084–0x82260098):
| 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 90–94
(0x8226C690–0x8226C6F0):
| 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.
🔴 The message bus: what the "one occurrence" of the tag is worth — nothing
The obvious next move was to search for a second site building 0xAB03E5BA and
conclude from its absence that nothing consumes the timer message. That
reasoning is dead, and the control kills it:
instructions building 0xAB03E5BA (addis 0xAB03 + ori 0xE5BA) |
1 — the producer |
0xAB03E5BA as a 4-byte word anywhere in the image |
0 |
any 4-aligned word beginning 0xAB03 anywhere in the image |
0 |
distinct 0xAB03xxxx tags built by an addis/ori pair |
40 |
| of those, built exactly once | 25 (62.5 %) |
Single-site construction is the majority case, so it says nothing about this tag in particular. And because no tag is ever stored as a static word, there is no table of subscribed tags to find either.
✅ Two structural reasons it cannot be a compare or a virtual call
- The post is a QUEUE PUSH, not a dispatch.
sub_82175C20(bus+4, &msg)is a ring-buffer append — capacity at+8, head at+12, count at+16, element array at+4, growing throughsub_8228E208. It never looks at the message. - The message class has a ONE-METHOD vtable.
0x820A8D18–0x820A8DA0is a run of 2-word records{ sub_82301C20, 0x8210Exxx }. The timer message's vptr is0x820A8D68, sovtable[0] = sub_82301C20— a scalar deleting destructor (it stores0x820A7014over the vptr and conditionally frees) — andvtable[-1] = 0x8210E288, an MSVC RTTI locator (0,0,0, &typeDescriptor 0x8289D094, &classHierarchy 0x8210E29C,numBaseClasses = 3). Not code: that address is data, and the disassembly does not even reach it (sylpheed.dbstarts at0x82150000). The message carries no handler of its own.
🟡 Where the subscriber actually lives — located, not read
The bus object at [0x828F35DC] carries a map at +8216, looked up through
sub_82254A08 from sub_823001E8 — whose only caller is sub_8230C398, the
per-frame message pump (itself called from the frame loop sub_821A6544 with the
frame dt). So dispatch is a runtime registry keyed by the tag, and the
question "does running out of time end the mission" reduces to: what inserts into
bus+8216, and with which key. That insert path is not read here.
🟡 Not settled
- Who subscribes to
0xAB03E5BA— see above. Located to the registry atbus+8216; the registration path is unread, so whether running out of time ends the mission is still unknown. - 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.