diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 45aaec33..b79c685e 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -4245,3 +4245,56 @@ spending a query on it. The remaining handle is the map's consumer reached via ❔ **Still open:** the firing policy proper (who rolls `Probability`, compares `Priority`, ticks `Interval`, what `IntervalFluctuation` randomises); which of the 49 speakers are wingmen. + +--- + +## ✅ 2026-08-27 — who fires a bark, and the first half of the firing policy + +Continues the chatter thread; reached through the **call graph**, after the +offset-intersection handle was refuted last iteration. + +**Docs checked:** `preset-message-rules.md` (mine), `squadron-orders.md` — it +owns `sub_82320B48` as the selector for the twelve-string `0x820AEEB0` enum and +warns it is a comms axis. **What it does not say** is that the function is one of +**eight bark firing sites sharing a single entry point**, and that it also raises +`COUNTER`, `COUNTER_TO_PLAYER` and `FOUND_PLAYER`. + +✅ **36 of the 64 event names are executable strings** (`0x820AEEC8`…`0x820B2160`), +so the string-xref join names the firing sites directly: `sub_823800A8` (17 +events — damage/destruction), `sub_82320B48` (15 — squadron orders + 3), +`sub_8237B020` (9 — player state), `sub_823B1438` (ammo), `sub_82381B10` +(evasion), and three singles. + +✅ **`sub_8220FA98` is the shared entry** — the only callee common to all eight. +Lock around `this+4`; enable byte `[this+8843]`; speaker lookup in the map at +`[this+8820]`; index `[this+8816] − 1` into a vector inside that entry (phase is +a *reading*, not measured); event-name lookup → the rule object; then the roll. + +✅ **`Probability` is a per-occurrence percentage — now measured, not inferred.** +`f0 = rand01 · 100.0` and `f0 > [rule+16]` returns without firing. The constant +at `0x820856F8` is exactly **100.0**, read from the image. `sub_8220D970` folds +two `rand()` calls into a 16-bit uniform. + +✅ **`sub_82210670` picks the line.** (1) Walks the pending list at +`[this+8736]` for a node matching this (speaker, rule) pair and **bails if the +same bark is already queued**. (2) Recomputes `MessageCount` from the vector and +takes a message as eligible only if its bit is **clear** in `[rule+36]` — the +word the loader zeroes — so **`+36` is a runtime exclusion mask** ("already +spoken" is the obvious reading; the write site was not found). (3) Uniform pick: +`eligible · rand01`, subtract `1.0` (`0x8208583C`) per eligible bit until the +counter empties or the accumulator reaches `0.0` (`0x8209FD28`). (4) Reads +`[rule+18]` `Pattern`, `[rule+17]` `Priority`, `[rule+28]` `EffectiveTime` and +hands them with the chosen message to `sub_822109B0`. + +✅ **Measured negative:** the pick path does **not** read `+20` `Interval`, +`+24` `IntervalFluctuation` or `+32` (the disc `Yes` mask). They are consumed +after the hand-off. Within the chatter CU (`0x8220C000`…`0x82216000`), `+17` and +`+18` are read by exactly **two** functions — the comparator and this one — which +is what makes the attribution safe where the binary-wide search was not. **The +bounded offset search is sound once the call graph says which functions matter; +the unbounded one is what was refuted.** + +❔ **Still open:** everything after `sub_822109B0` — how `Priority` orders the +queue, whether `Interval` is a per-event or per-speaker cooldown, what +`IntervalFluctuation` randomises, where `+36`'s bits get set, and what reads +`+32`. Also still open: which of the 49 speakers are wingmen. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index fd2e142b..cbc2bcb9 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) (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 (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 ce0a2387..09bbe618 100644 --- a/docs/re/structures/preset-message-rules.md +++ b/docs/re/structures/preset-message-rules.md @@ -265,14 +265,18 @@ can never reach, because it skips the record first. Thirteen dead records out of ### 🟡 What this does *not* settle -The loader and the merge tell us units, storage and duplicate handling — not -the *firing* policy. `Priority`'s comparison rule, whether `Interval` is a -cooldown per event or per speaker, and what `IntervalFluctuation` randomises -against all live in the consumer of the map, reachable via -`sub_82215A58` ← `sub_82214050`. 🔴 One handle was tried and refuted: searching -for functions that touch `+16`/`+17`/`+18`/`+20`/`+28`/`+32`/`+36` returns -dozens of unrelated matches, because those offsets recur across unrelated object -layouts. Offset shape is not an identifier. `Pattern`'s two live values +`Probability` and the line pick are settled above. **Still open: everything +after the hand-off to `sub_822109B0`** — how `Priority` orders the queue, whether +`Interval` is a cooldown per event or per speaker, what `IntervalFluctuation` +randomises, and where `+36`'s bits are set. `+32` (the disc `Yes` mask) is still +read by nothing that has been identified. + +🔴 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 +matches, because those offsets recur across unrelated layouts. **Offset shape is +not an identifier** — the call graph is. The bounded version of the same search, +restricted to this compilation unit *after* the call graph had established which +functions matter, is sound and is what pinned `+17`/`+18`. `Pattern`'s two live values are named, but that `Window` means an on-screen text window is a reading of the name, not something measured. @@ -330,6 +334,76 @@ fact that `+32` is excluded from the comparison changes nothing on the shipped disc. Worth recording precisely because it is the kind of gap that looks like a latent bug until it is counted. +## ✅ Who fires a bark, and the first half of the policy + +**Settled 2026-08-27**, reached through the **call graph**, not through offset +shape (that route is refuted — see the note below). + +### The event names are executable strings, so the firing sites name themselves + +**36 of the 64 event names ship as strings** in `0x820AEEC8`…`0x820B2160`, and the +functions referencing them are the sites that raise each event: + +| site | events it raises | reading | +|---|---|---| +| `sub_823800A8` | 17 — `DAMAGED*`, `HP_LESS_*`, `KILLED*`, `KILL_UNIT`, `LOST_{BRIDGE,SHIELD,THRUSTER,TURRET}_BY_PLAYER`, `SHIELD_DOWNED*`, `ATTACK_SUCCEEDED` | damage / destruction | +| `sub_82320B48` | 15 — all twelve `ORDER_`/`ORDOR_` names plus `COUNTER`, `COUNTER_TO_PLAYER`, `FOUND_PLAYER` | squadron orders | +| `sub_8237B020` | 9 — `PLAYER_HP_LESS_*`, `KILL_PLAYER`, `DAMAGED_TO_PLAYER`, `AVOIDED_BY_PLAYER`, `DAMAGED`, `KILLED`, `SHIELD_DOWNED` | player state | +| `sub_823B1438` | `PLAYER_AMMO_LESS_10/40` | ammunition | +| `sub_82381B10` | `AVOID_ATTACK`, `AVOIDED_PLAYER_ATTACK` | evasion | +| `sub_82398CC0` / `sub_823969D0` / `sub_82362F18` | `ATTACK_IS_AVOIDED` / `EXTENDED` / `FOUND_PLAYER` | one each | + +[`squadron-orders.md`](squadron-orders.md) already names `sub_82320B48` as the +selector for the twelve-string enum and warns it is a comms axis. **What it does +not say** is that the function is one of eight *bark firing sites* that share a +single entry point, and that it also raises three non-order events. + +### `sub_8220FA98` — the shared entry, and the `Probability` roll + +All eight sites call it, and nothing else in the file does more. It takes a lock +around `this+4`, bails unless the enable byte at `[this+8843]` is set, then: + +1. looks the **speaker** up in the map at `[this+8820]`; +2. indexes a vector inside that entry by `[this+8816] − 1` (a byte; the natural + reading is the phase, but that is a reading, not measured); +3. looks the **event name** up in *that* entry's map → the 40-byte rule object; +4. rolls: `f0 = rand01 · 100.0`, and **if `f0 > [rule+16]` it returns without + firing.** + +✅ **`Probability` is a straight per-occurrence percentage.** The scaling +constant at `0x820856F8` is exactly **100.0**, read from the image — which is +what turns the disc census (`1,2,3,5,10,20,25,30,40,50,60,100`) from "looks like +per cent" into per cent. `sub_8220D970` builds the uniform: two `rand()` calls +folded into 16 bits, converted to float. + +### `sub_82210670` — pick a line + +Called only when the roll passes. In order: + +1. **Duplicate suppression.** It walks the list at `[this+8736]` for a node whose + `+8` and `+12` match this (speaker, rule) pair; if one is pending it returns + immediately. The same bark cannot be queued twice. +2. **Eligibility.** `MessageCount` is recomputed from the vector + (`([rule+8] − [rule+4]) >> 2`), and a message is eligible only if its bit is + **clear** in the mask at **`[rule+36]`** — the word the loader zeroes at + construction. So `+36` is a runtime *exclusion* mask over the message list; + "already spoken" is the obvious reading, and the write site was not found this + iteration. +3. **Uniform pick.** `f0 = eligible_count · rand01`, then walk the eligible bits + subtracting `1.0` (the constant at `0x8208583C`) until the counter runs out or + `f0` drops to `0.0` (`0x8209FD28`). That index is the chosen line. +4. **Hand-off.** It reads `[rule+18]` `Pattern`, `[rule+17]` `Priority` and + `[rule+28]` `EffectiveTime`, and passes them with the chosen message to + `sub_822109B0`. + +✅ **A measured negative worth having:** the pick path reads `+17`, `+18`, `+28` +and `+36` — and **not** `+20` (`Interval`), `+24` (`IntervalFluctuation`) or +`+32` (the disc `Yes` mask). Those three are consumed somewhere after the +hand-off, not on the way in. Within the whole chatter compilation unit +(`0x8220C000`…`0x82216000`), `+17` and `+18` are read by exactly two functions — +`sub_82213840` (the comparator) and `sub_82210670` — which is what makes this +attribution safe where a binary-wide offset search is not. + ## 🟡 Not settled * The *policy* the seven fields drive — see "what this does not settle" above.