From a09c9e6517fa8002edb2497527ab28d4279a48b0 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Thu, 27 Aug 2026 09:03:18 +0000 Subject: [PATCH] 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. --- docs/re/BACKLOG.md | 43 +++++++++ docs/re/INDEX.md | 1 + docs/re/mission-phase-timers.md | 20 +++- docs/re/structures/isl-builtins.md | 1 + docs/re/structures/isl-mission-timer.md | 122 ++++++++++++++++++++++++ docs/re/structures/isl-timers.md | 12 ++- 6 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 docs/re/structures/isl-mission-timer.md diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 1daeb901..93c4f97b 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -293,6 +293,49 @@ unknown, what evidence exists, and what the first step would be. Move an item in no writer searched for โ€” a game-speed setter would be one; and 31 `read_freg` sites index by a computed value rather than an immediate. +* โœ…๐Ÿ”ด **(2026-08-27) `timer_set`'s SECOND ARGUMENT IS A SECOND COUNTDOWN โ€” the + "warning threshold" reading is REFUTED. [structures/isl-mission-timer](structures/isl-mission-timer.md).** + `mission-phase-timers.md` left it open as "a limit and a warning threshold is + the obvious reading, but it is **not** established". Reading the updater + settles it the other way. The **mission timer** is five scalars on the + ScriptPhase โ€” `[+304]` elapsed (up), `[+308]` countdown A, `[+312]` countdown + B, `[+316]` running, `[+320]` armed โ€” zeroed together by `sub_8225FEF8`, and + driven by **`sub_822639B8`, called from `ScriptPhase::Update` at `0x82263528` + with `fmr f1, f30` โ€” the SAME `dt` the stopwatch bank gets**, so it counts the + same seconds. Its body: `if running: [+304] += dt`, and `if armed:` **`[+308] + -= dt` while `[+308] > 0`, ELSE `[+312] -= dt` clamped at 0.** ๐Ÿ”‘ A threshold + would be *compared*; `[+312]` is never compared with `[+308]` anywhere โ€” it is + decremented, and the `bc 12, gt` at `0x822639FC` jumps **away** to the A arm, + so B only runs once A is spent. **Two sequential countdowns.** Disc-wide, 29 + `timer_set` sites: first argument 600 ร—19 / 1200 ร—8 / 900 / 1800, **second + argument 180 in all 29** โ€” a per-mission limit then a fixed 3-minute stage. + (Stated as consistent-with, not as the proof; the disassembly is the proof.) + โœ… **The five built-ins are vtable slots 90โ€“94** (`0x8226C690`): 127 + `timer_set(a,b)` arms and loads A/B (as **doubles** from `local[0]`/`local[1]`), + 123 `timer_resume` starts the elapsed counter, 124 `timer_stop` stops it and + disarms, 125 `timer_reset` zeroes all three floats, 126 `timer_elapsed` returns + `[+304]`. ๐Ÿ”‘ **125 and 126 have ZERO call sites in all 28 scripts** โ€” the script + arms, starts and stops this clock but **never reads it**. Its only consumer read + here is a 32-byte message (`0xAB03E5BA` โ†’ the bus at `[0x828F35DC]`) carrying + elapsed, B, running, and `expired = armed && A <= 0`. + ๐Ÿ”ด **CORRECTION to `mission-phase-timers.md`:** "timer 0 is started by + `timer_set(1200,180)` then `timer_resume`, with a 1200-second limit" **merges + two clocks**. `timer_set`/`timer_resume` arm and start THIS object; + `set_flag(0)`, one instruction later, starts **stopwatch 0**, which is what the + timeline's `kind = 0` reads. They start together and so read almost the same + value โ€” which is why the merge was invisible. The 1200 s is the mission timer's, + not stopwatch 0's. + โœ… This also unblocks the naming collision noted in + [isl-timers](structures/isl-timers.md): 123โ€“127 act on five *scalars*, 8/9/93 + on a 32-entry *bank*. Not renamed here โ€” that is its own change and its own + artefact diff; everything regenerates byte-identical as it stands. + ๐ŸŸก 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 here and dispatch is not by literal compare, so one occurrence is **not** + evidence of no consumer. So whether running out of time ends the mission is + unread, as is what B reaching zero does, and whether all 170 `timer_stop` sites + are phase teardown. + ## โœ…โœ… SOLVED โ€” the mission freeze was a modal sign-in dialog (2026-08-26) `XamShowSigninUI` opens a modal dialog and `xeXamDispatchDialog` blocks the diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 47f9a4e8..610864f5 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -98,6 +98,7 @@ files, which is how the same ground got covered twice. | [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | โœ… CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; โš ๏ธ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn | | [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` โ€” message id -> cue -> sound id -> `.slb` bank | โœ… CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files | | [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue โ€” speaker, portrait, on-screen seconds, audio cue per page | โœ… CONFIRMED, field count = 9ยทPageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways | +| [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer โ€” two sequential countdowns, not a limit and a warning | โœ… CONFIRMED โ€” read from `sub_822639B8`; the threshold reading is | | [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | โœ… CONFIRMED โ€” the advance is read from `sub_822710D0`, the unit from | | [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue โ€” built-in 64 -> message id -> caption text | โœ… CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue | | [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` โ€” the mission's own objective counter, in RAM | โœ… CONFIRMED for one Stage 02 run: a big-endian u32 whose value | diff --git a/docs/re/mission-phase-timers.md b/docs/re/mission-phase-timers.md index 26d58cc3..62d39246 100644 --- a/docs/re/mission-phase-timers.md +++ b/docs/re/mission-phase-timers.md @@ -115,5 +115,21 @@ 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. +* ~~`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. diff --git a/docs/re/structures/isl-builtins.md b/docs/re/structures/isl-builtins.md index 94f34cef..26d39f56 100644 --- a/docs/re/structures/isl-builtins.md +++ b/docs/re/structures/isl-builtins.md @@ -84,6 +84,7 @@ two tables already parsed in [mission-script-ssb](mission-script-ssb.md). | **132โ€“134** | player gauges | speed/boost ratios and a player byte | | **73, 123โ€“127** | timer family | start / resume / stop / reset / read elapsed / read limit | | **8 / 9 / 93** | `set_flag` / `read_freg` / `clear_flag` | start / read / stop one of 32 **stopwatches** โ€” [isl-timers](isl-timers.md); the names predate the reading | +| **123 / 124 / 125 / 126 / 127** | `timer_resume` / `timer_stop` / `timer_reset` / `timer_elapsed` / `timer_set` | the **mission timer** โ€” five scalars at `[phase+304โ€ฆ320]`, vtable slots 90โ€“94 โ€” [isl-mission-timer](isl-mission-timer.md). 125 and 126 have **zero** call sites on the disc | | **100 / 115** | `reset_phase_threads` / `named_event` | โŒ 100 is **not** `push_trigger` โ€” see below | **The state machine is therefore:** a trigger fires a coroutine โ†’ the coroutine diff --git a/docs/re/structures/isl-mission-timer.md b/docs/re/structures/isl-mission-timer.md new file mode 100644 index 00000000..fc635004 --- /dev/null +++ b/docs/re/structures/isl-mission-timer.md @@ -0,0 +1,122 @@ +# โœ… 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.** + +## ๐ŸŸก 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. diff --git a/docs/re/structures/isl-timers.md b/docs/re/structures/isl-timers.md index b3fe1ec4..1309a0d0 100644 --- a/docs/re/structures/isl-timers.md +++ b/docs/re/structures/isl-timers.md @@ -109,11 +109,13 @@ in a coherent block of stages) โ€” checked before believing. ## ๐ŸŸก What this leaves -* The **naming**. `set_flag` / `read_freg` / `clear_flag` describe none of this; - `timer_start` / `timer_elapsed` / `timer_stop` would. They are not renamed here - because built-ins 127/123/124 already hold `timer_set` / `timer_resume` / - `timer_stop` in the corpus for a *different* timer, and untangling the two is - its own change. Every artefact regenerates byte-identical as a result. +* The **naming**. `set_flag` / `read_freg` / `clear_flag` describe none of this. + โœ… The other timer is now read โ€” [isl-mission-timer](isl-mission-timer.md) โ€” + so the collision is understood: 123/124/125/126/127 act on five *scalar* + fields (`[phase+304โ€ฆ320]`), this family on a 32-entry *bank*. `stopwatch_start` + / `stopwatch_elapsed` / `stopwatch_stop` would separate them. Still not renamed + here, so every artefact regenerates byte-identical; the rename is its own + change and its own artefact diff. * The **one exception**: `read_freg(15)` in Stage 06 phase 1 is not started by a `set_flag(15)` in that phase. Cross-phase carry-over or a start in the unreached code โ€” not chased.