re: the seven chatter fields, read off sub_82213980

The one function referencing all seven field-name strings is the rule table's
loader.  Interval / IntervalFluctuation / EffectiveTime are SECONDS, emitted
as *60 frame counts; Probability is a percentage and zero skips the record;
Pattern is a 4-arm enum of which only Sound and Window ship; the Yes/No pair
element is a u32 bitmask, which is why MessageCount is clamped to 32 (max on
disc is 26).  40-byte object layout recorded.  13 dead records characterised.

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 16:35:56 +00:00
parent 6a81268cf1
commit c4fd90baa9
5 changed files with 190 additions and 5 deletions

View File

@@ -190,11 +190,90 @@ record names in this chatter rule table, i.e. they are the twelve *events*
"a squadron order was issued", each with its own lines, priority and interval.
They are triggers for barks, not order types.
## ✅ The seven fields, read off the engine's own loader
**Settled 2026-08-27** from `sub_82213980`, the one function in the executable
that references all seven field-name strings (they sit contiguously at
`0x820A5794``0x820A5800`; the query is the string-xref join, and the seven
xrefs land in that single function). It walks the rule table's records through
`sub_82448AA0` / `sub_824482D0` / `sub_82448BC8` and builds a **40-byte** runtime
object per event. The disc-side value census in the artefact agrees with it at
every point.
| field | disc values (all 9 216 records) | what the loader does |
|---|---|---|
| `EffectiveTime` | `0` 8566, `3` 453, `5` 187, `10` 10 | `× 60``[obj+28]` |
| `Interval` | `0` 6820, `3` 821, `4` 1, `5` 894, `30` 182, `120` 498 | `× 60``[obj+20]` |
| `IntervalFluctuation` | `0` 8403, `2` 813 | `× 60``[obj+24]` |
| `Priority` | `1``7`, `9` (no `8`); `1` is 7 738 | byte → `[obj+17]` |
| `Probability` | `0` 6788, then `1`,`2`,`3`,`5`,`10`,`20`,`25`,`30`,`40`,`50`,`60`,`100` | byte → `[obj+16]`; **`0` makes the loader skip the record entirely** |
| `Pattern` | `Sound` 8395, `Window` 821 | string → small enum → `[obj+18]` |
| `MessageCount` | `0``26` | pair count; **clamped to 32** |
**The three time fields are SECONDS, converted to 60 Hz frames.** The loader
emits `mulli rN, rN, 60` for `Interval`, `IntervalFluctuation` and
`EffectiveTime` and for nothing else. Every disc value is a small integer
(`120` s = two minutes is the largest), which is what makes seconds the only
reading that survives.
**`Pattern` is a delivery channel, and the enum has two dead arms.** The
loader `strcmp`s the value against `"Log"` → 2, `"Window"` → 3, `"Demo"` → 4,
defaulting to **1**. The disc only ever says `Sound` (→ default 1) or `Window`
(→ 3); **`Log` and `Demo` are declared in code and never used in data**. Do not
conflate this with the radio *delivery category* (`None`/`Emergency`/`Killed`/
`Noise`) that `mission-phase-deployment.md` found in `ScriptMessage_S02_msg.tbl`
— different table, different axis. (`Killed` does sit at `0x820A5784`,
immediately before this string block, which is exactly the kind of adjacency
that invites the mistake.)
✅ **The `Yes`/`No` element is a 32-bit mask, and that is why `MessageCount` is
capped at 32.** The loader runs two passes over the positional payload:
`sub_82448BC8(rec, 2·i)` collects the **even** slots (the message ids) and
`sub_82448BC8(rec, 2·i+1)` the **odd** ones, `strcmp`s each against `"Yes"`
(`0x820A57A4`), and sets `1 << i` in a `u32` stored at `[obj+32]`. The clamp and
the mask width are the same constant. **Confirmation from the data:** the largest
`MessageCount` on the disc is **26**, no record exceeds the clamp, and no
record's `Yes` count ever exceeds its `MessageCount`. This also independently
re-derives the pair layout from the executable side, having first been inferred
from `MessageCount · 2 == positional count`.
### The 40-byte runtime object
```
+16 u8 Probability (per cent)
+17 u8 Priority (1..9)
+18 u8 Pattern (1 = Sound, 3 = Window; 2 = Log and 4 = Demo unused)
+20 u32 Interval * 60 (frames)
+24 u32 IntervalFluctuation * 60
+28 u32 EffectiveTime * 60
+32 u32 the Yes bitmask
+36 u32 zero at construction
```
`[obj+16]`'s byte is also the loader's gate: it is read with `sub_824482D0` and
sign-extended, and a **zero `Probability` skips the record before the object is
ever allocated**.
### Residual, characterised
Cross-tabbing `MessageCount == 0` against `Probability == 0` over all 9 216
records: **6 786** are zero on both (the event is simply not used by that
speaker), **2 417** are set on both, **11** carry a `Probability` but no lines,
and **2** carry lines with `Probability 0` — those two ship dialogue the loader
can never reach, because it skips the record first. Thirteen dead records out of
9 216.
### 🟡 What this does *not* settle
The loader tells us the units and the storage, not the policy. `Priority`'s
comparison rule, whether `Interval` is a cooldown per event or per speaker, and
what `IntervalFluctuation` randomises against are all in the *consumer* of the
40-byte object, which this iteration did not read. `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.
## 🟡 Not settled
* What `EffectiveTime`, `Interval`, `IntervalFluctuation`, `Priority`,
`Probability` and `Pattern` mean numerically — they are read here as a schema,
not measured against the running game.
* The `Yes`/`No` second element of each pair.
* The *policy* the seven fields drive — see "what this does not settle" above.
* Which of the 49 speakers is a wingman — the "13 tables" partition above is a
count, not a verified roster.