Files
Sylpheed/docs/re/structures/isl-mission-timer.md
Sylpheed RE agent b86c8c82ba isl: rename built-ins 8/9/93 to stopwatch_start/_elapsed/_stop
They are start / read / stop of one of 32 per-phase stopwatches, not
flag operations.  123-127 keep timer_* -- that is the mission timer,
five scalars at [phase+304..320], a different clock.

Artefact check: 84 lines changed across 5 files and all 84 pair
exactly with their old-name partners once column padding is
normalised (0 removed lines without an old name, 0 added lines
without a new one).  data/isl-timers.txt reproduces the same
675/675, 11.2 % control, 82/1 and identical histograms, which is
what shows the rename is cosmetic.

Also withdraws a label from the previous commit: sub_8230C398 is NOT
the message pump.  It runs every frame but drains nothing -- a state
machine on [0x828E1F8C] that only allocates, builds strings, looks up
and PUSHES.  And bus+8216 is weak evidence: sub_82254A08 is a generic
map find with ~120 sites, and the key looked up is a pointer, not a
tag.  The open handle is now the ring buffer at bus+4, not bus+8216.
2026-08-27 09:21:04 +00:00

178 lines
7.9 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.
# ✅ 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](isl-timers.md); 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 9094
(`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](isl-timers.md)), 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 through `sub_8228E208`. It never looks at the message.
* **The message class has a ONE-METHOD vtable.** `0x820A8D18``0x820A8DA0` is a
run of 2-word records `{ sub_82301C20, 0x8210Exxx }`. The timer message's vptr
is `0x820A8D68`, so `vtable[0] = sub_82301C20` — a scalar deleting destructor
(it stores `0x820A7014` over the vptr and conditionally frees) — and
`vtable[-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.db`
starts at `0x82150000`). **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`.
🔴 **Correction to my own label from the previous pass.** I called
`sub_8230C398` "the per-frame message pump". It is *called* every frame (from
`sub_821A6470` at `0x821A654C` and from the mission loop `sub_821AA1B0` at
`0x821AA7DC`, both with the frame `dt`), but **it drains nothing**: its whole
body is a state machine on the counter at `[0x828E1F8C]`, and across 466
instructions it calls only an allocator (`sub_8230C160`), string building
(`sub_8217FA08`), a map lookup (`sub_823001E8`) and a *push* (`sub_8225FEA0`).
It is a boot/loading sequencer that posts messages, not the consumer of them.
Two further cautions the search turned up:
* `sub_82254A08` is a **generic map find** with ~120 call sites image-wide, and
`+8216` is a common offset — the pair alone identifies nothing.
* The key `sub_823001E8` looks up is a **pointer** (`lwz r4, 13780(r11)` stored
and passed by address), **not a message tag**. So `bus+8216` may not be the
subscriber registry at all.
**So the drain of the queue at `bus+4` is still unlocated**, and with it the
answer to whether running out of time ends the mission.
## 🟡 Not settled
* **Who subscribes to `0xAB03E5BA`** — see above. Located to the registry at
`bus+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.