From 52c08b72469b70671209ef5a6106984e92700cd5 Mon Sep 17 00:00:00 2001 From: "Claude (auto)" Date: Thu, 27 Aug 2026 17:28:57 +0000 Subject: [PATCH] re: Interval and IntervalFluctuation are the cooldown; the chatter system closes sub_82210C38 retires a line and is the only chatter function reading the rule's +20/+24 as words: node[+16] = Fluctuation * rand01 + Interval frames, state 0x20. Interval is a floor, Fluctuation a uniform additive jitter. The queue node IS the cooldown timer -- the tick counts +16 down and only then frees the slot, so the already-queued bail in sub_82210670 and the cooldown are one mechanism: a speaker cannot repeat an event until its node expires. A lingering node still holds one of the 128 pool slots. Also: +20 on the node is a ducking level, raised while a higher-priority line plays and released on retirement. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/BACKLOG.md | 46 +++++++++++++++ docs/re/INDEX.md | 2 +- docs/re/structures/preset-message-rules.md | 69 ++++++++++++++++++++-- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 09e6bf0c..ee8eb954 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -4344,3 +4344,49 @@ cooldown per event, per rule or per speaker, and what `IntervalFluctuation` paths read so far — `sub_82210320` was checked as a candidate and is a different structure (a deque at `this+8744`, with a divide-by-60 frames→seconds), not the rule object. Also still open: which of the 49 speakers are wingmen. + +--- + +## ✅ 2026-08-27 — `Interval` + `IntervalFluctuation`: the chatter system closes + +The last two open fields. **Docs checked:** `preset-message-rules.md` (mine); +nothing else in the corpus mentions `sub_82210C38`. + +✅ **`sub_82210C38` is the retire-a-line routine** (called from the tick +`sub_8220FC50` and from `sub_8220FB90`) and is the **only** place in the chatter +code that reads the rule's `+20`/`+24` as **words** — which is what separates +them from the byte fields at the same offsets in the queue node. On retirement: + + node[+16] = (int)( (float)rule[+24] * rand01() + (float)rule[+20] ) + node[+24] = 0x20 + +✅ **`Interval` is a floor, `IntervalFluctuation` is uniform additive jitter** — +the cooldown is uniform over `[Interval, Interval + IntervalFluctuation)` frames. +Disc: `Interval ∈ {0,3,4,5,30,120}` s, jitter 0 or up to +2 s (813 records). + +✅ **The cooldown is per (speaker, event), and the queue node IS the timer.** +`node[+16]` — which held `EffectiveTime` while the line played — is reused. In +state `0x20` the tick decrements it per frame and only at zero unlinks the node, +clears `[node+23]` to free the slot and decrements `[this+8740]`. **This closes +the loop with the "already queued" bail in `sub_82210670`: they are the same +mechanism.** A retired line keeps its node alive for `Interval + jitter` frames, +and for exactly that long the same speaker cannot repeat the same event. The rule +object holds no timestamp; nothing per-rule or global is involved. + +⚠️ **Port note:** a lingering node still occupies one of the **128** pool slots — +a 120 s `Interval` parks a slot for two minutes. + +✅ Same routine: stops the presentation via `sub_822168E8` on `this+8776` +(`Pattern` 1) or `node+28` (`Pattern ≥ 3`); and when `[node+20] == 1` sweeps the +pending list calling `sub_82217980(…, 0)` on every node not in state `0x10`. +With `sub_82210AF0` (max `[node+20]` over *higher*-priority pending nodes) that +makes `+20` a **ducking level**, raised while a higher-priority line plays and +released on retirement. + +**The chatter system is now closed end to end** — data, loader, phase merge, +firing, probability, pick, queue, priority, one-shot flag and cooldown. + +❔ **Not established** (and none of it changes the data model a port needs): the +numeric attenuation of the ducking level, the meaning of node states other than +`0x10`/`0x20`, and whether `sub_822168E8`/`sub_82217980` do more than +stop/resume. ❔ Still open elsewhere: which of the 49 speakers are wingmen. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index aa2034d9..eece3254 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -98,7 +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/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128 (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue | +| [`structures/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuation*rand held in the lingering queue node (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue | | [`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/archive-naming.md`](structures/archive-naming.md) | Which archives the disc can name — 100% for menu paks, **0.0%** for the six 2D paks and `GP_READY_ROOM` | ✅ CONFIRMED — 6027 names × 16 prefixes, bimodal | | [`structures/hud-config.md`](structures/hud-config.md) | The in-game HUD configuration — 16 records, 419 asset paths, and a per-stage `ResourceTable` | ✅ CONFIRMED — 0 of 419 config paths resolve as pak entries | diff --git a/docs/re/structures/preset-message-rules.md b/docs/re/structures/preset-message-rules.md index d4c7c810..362cebc5 100644 --- a/docs/re/structures/preset-message-rules.md +++ b/docs/re/structures/preset-message-rules.md @@ -265,11 +265,12 @@ can never reach, because it skips the record first. Thirteen dead records out of ### 🟡 What this does *not* settle -`Probability`, the pick, the queue, `Priority` and the `Yes`/`No` flag are all -settled above. **What remains is only the two interval fields:** whether -`Interval` (`+20`) is a cooldown per event, per rule or per speaker, and what -`IntervalFluctuation` (`+24`) randomises against. Neither is read by the entry, -the pick, the enqueue or the tick paths read so far. +All seven fields and the `Yes`/`No` flag are settled above; the static side of +this system is complete. What is **not** established: the numeric effect of the +ducking level at `node[+20]` (its mechanism is clear, its attenuation is not), +what the state values other than `0x10`/`0x20` mean, and whether +`sub_822168E8`/`sub_82217980` do anything beyond stop/resume. None of these +change the data model a port needs. 🔴 One handle was tried and refuted: searching the whole image for functions that touch `+16`/`+17`/`+18`/`+20`/`+28`/`+32`/`+36` returns dozens of unrelated @@ -473,6 +474,64 @@ The tick also branches on `Pattern < 3` (using `this+8776` on the taken side), s `Window` (3) is on a different presentation path from `Sound` (1) — consistent with the two values the disc ships. +## ✅ `Interval` and `IntervalFluctuation` — the cooldown, and the system closes + +**Settled 2026-08-27** from `sub_82210C38`, the retire-a-line routine (called +from the tick `sub_8220FC50` and from `sub_8220FB90`). It is the only place in +the chatter code that reads the rule's `+20` and `+24` as **words**, which is +what distinguishes them from the byte fields at the same offsets in the queue +node. + +When a line finishes: + +``` +rule = node[+12] +if rule: + f1 = rand01() ; sub_8220D970 + f0 = (float)rule[+24] ; IntervalFluctuation, frames + f13 = (float)rule[+20] ; Interval, frames + node[+16] = (int)(f0 * f1 + f13) ; fmadds +else: + node[+16] = 0 +node[+24] = 0x20 ; state: cooling down +``` + +✅ **`Interval` is a floor and `IntervalFluctuation` is uniform additive jitter**: +the cooldown is `Interval + IntervalFluctuation · rand01` frames, i.e. uniform +over `[Interval, Interval + IntervalFluctuation)`. On the disc that is +`Interval ∈ {0,3,4,5,30,120}` seconds with a jitter of `0` or up to `+2` seconds +(813 records carry `IntervalFluctuation 2`). + +✅ **The cooldown is per (speaker, event), and the queue node *is* the timer.** +`node[+16]` — the field that held `EffectiveTime` while the line was playing — is +reused for the cooldown. In state `0x20` the tick decrements it each frame and, +only when it reaches zero, unlinks the node, clears `[node+23]` to free the pool +slot and decrements the count: + +``` +node[+16] -= elapsed +if node[+16] > 0: keep waiting +else: unlink; node[+23] = 0; [this+8740]-- +``` + +That closes the loop with the duplicate check in `sub_82210670`, which refuses to +fire when a node with the same `(speaker, rule)` is already in the list. **The +"already queued" bail and the cooldown are the same mechanism**: a retired line +keeps its node alive for `Interval + jitter` frames, and for exactly that long the +same speaker cannot repeat the same event. Nothing per-rule or global is +involved — the rule object itself holds no timestamp. + +⚠️ **Port note:** the lingering node still occupies one of the **128** pool slots. +A long `Interval` (120 s = 7 200 frames) parks a slot for two minutes. + +The same routine also stops the presentation — `sub_822168E8` on `this+8776` for +`Pattern` 1, or on `node+28` when `Pattern ≥ 3` — and, when `[node+20] == 1`, +sweeps the pending list calling `sub_82217980(…, 0)` on every node whose state is +not `0x10`. Together with `sub_82210AF0` (which returns the largest `[node+20]` +among *higher*-priority pending nodes) that makes `+20` a ducking level: raised +while a higher-priority line plays, released when it retires. The exact +attenuation is not established here. + ## 🟡 Not settled * The *policy* the seven fields drive — see "what this does not settle" above.