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

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