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.
178 lines
7.9 KiB
Markdown
178 lines
7.9 KiB
Markdown
# ✅ 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 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](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.
|