re: the bark firing sites and the Probability roll

36 of the 64 event names are executable strings, so the string-xref join names
the eight sites that raise them; all eight share one entry, sub_8220FA98.
Probability is a per-cent roll -- the scaling constant at 0x820856F8 is exactly
100.0.  sub_82210670 suppresses an already-queued (speaker, rule) pair, treats
a message as eligible only if its bit is clear in the runtime mask at +36, and
picks uniformly, then hands Pattern/Priority/EffectiveTime to sub_822109B0.

Measured negative: the pick path never reads +20, +24 or +32.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Claude (auto)
2026-08-27 17:04:18 +00:00
parent 6ee041c1bf
commit 8dc270091b
3 changed files with 136 additions and 9 deletions

View File

@@ -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.

View File

@@ -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 |

View File

@@ -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.