This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/preset-message-rules.md
Claude (auto) c4fd90baa9 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
2026-08-27 16:35:56 +00:00

280 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The reactive-chatter rule table
**✅ Settled 2026-08-27** (static; regenerator
`tools/re-capture/preset_messages.py`, artefact
[`data/preset-messages.txt`](../data/preset-messages.txt)).
The corpus already knew *that* `PresetMessage_*` is reactive combat chatter —
[`ixud-localised-text.md`](ixud-localised-text.md) names the IXUD family
("the bulk: reactive combat chatter keyed by speaker class") and
[`mission-phase-deployment.md`](../mission-phase-deployment.md) found the link
that reaches it (`message\UnitMessageSet_S<NN>.tbl` gives every crew a
`PresetMessage_Phase1/2/3`). What neither says is **what is inside the table** —
which events exist, what governs when a line fires, and how a line reaches a
recording. That is this document.
## Two files per set
A chatter set is a **pair** of IDXD objects in each `dat/GP_MAIN_GAME_<lang>.pak`:
| name | role |
|---|---|
| `message\PresetMessage_<who>.tbl` | the **rule** table — this document |
| `message\PresetMessage_<who>_msg.tbl` | the **message** table |
The `_msg` half is *not* new: it is exactly the record layout
[`cutscene-message-table.md`](cutscene-message-table.md) settled for
`MSG_DEMO_*` — a `Generic` count record plus `Message_NNN` records with `ID` /
`PageCount` and 9 positional fields per page. What that document does not say is
that the same layout carries **137 more objects per language pack** for
in-mission chatter, against the 32 cutscene ones it counted. The control run
confirms the split cleanly: of the 273 `PresetMessage_*` names that hit an IDXD
entry, **136 are rule tables and 137 are `Generic`+`Message_NNN` message
tables** (3 of those are `Generic`-only, i.e. empty).
## The rule table
**144 per language pack, 864 across the six `GP_MAIN_GAME_<lang>.pak`** — one
identical count in each, and `ORDOR_SQUADRON_EXTENDED` (the misspelling is the
game's, see below) is the largest single record name on the disc for exactly
that reason.
Every one of the 144 has the **same 65 records**: 64 event records plus a
`Sperkers` (sic) roster. One record-name tuple, no variants.
### Event records
All 9 216 event records (144 × 64) share **one** named-field schema, with no
exceptions:
```
EffectiveTime Interval Priority Probability MessageCount
IntervalFluctuation Pattern
```
After the seven named fields come the record's positional payload —
[a record's unnamed positional fields are its payload] — in **pairs**:
(message id, Yes/No) e.g. ('MSG_BIRD_020', 'No')
`MessageCount` is the number of pairs, and that identity is **exact**:
`MessageCount · 2 == positional-field count` in **9 216 of 9 216** records, zero
mismatches. That is what pins the pairing. The second element of each pair is
`No` 8 940 times and `Yes` 388 times; its meaning is not settled.
### The 64 events
The full census with per-event table counts is in the artefact. The shape of
that census is itself informative:
* **Player-facing events are voiced only by the wingman tables.** Every
`LOCKON_1..12`, `LOCKON_UP`, `LOCKON_DOWN`, `PLAYER_HP_LESS_10/30/50` and
`PLAYER_AMMO_LESS_00/10/40` record carries lines in **exactly 13** of the 144
tables — the same 13 as `ORDER_WINGMAN_{FORMATION,ATTACK,COVER,EXTENDED}`.
* The squad/squadron order events carry lines in **7** tables each.
* Generic combat events are near-universal: `DAMAGED_BY_PLAYER_GREATER_50` 128,
`DAMAGED` 124, `HP_LESS_10` and `HP_LESS_50` 121 each.
* **Five events are declared everywhere and populated nowhere** —
`DAMAGED_BY_PLAYER_50_30`, `HP_LESS_30`, `RETURNED`, `TALK`, `TM_TO_PLAYER`
have `MessageCount 0` in all 144 tables. They are slots the shipped data never
uses.
Note the near-miss pairs that are *not* redundant: `HP_LESS_*` (the speaker's
own hull) versus `PLAYER_HP_LESS_*`, and `AVOIDED_BY_PLAYER` versus
`AVOIDED_PLAYER_ATTACK`. The `_PLAYER` half of each pair is the smaller
population.
### `Sperkers`
The last record is a roster of speaker names as **field names** with empty
values — `CharacterELLEN`, `CharacterADANCAMAEL`, … **49 distinct speakers**
across the 144 tables. Most tables name one speaker (84 of 144); the rest name
2–11, and three name none.
## The join to audio is the settled cue table
The pair's first element is a message id, and it resolves through the cue index
[`sound-cue-table.md`](sound-cue-table.md) already documents — `MSG_` becomes
`VOICE_`, then `SOUNDS` in `dat/tables.pak` gives the numeric id and `FILES` the
`.slb` bank.
**2 388 of 2 405 distinct message ids resolve (99.3 %).** The 17 that do not
are characterised, not swept up: 8 are ordinary four-letter-family ids
(`MSG_ADAN_569/668/788/789`, `MSG_ADPL_045/046/047`, `MSG_RHIN_341`) and 9 are
`MSG_VOICE_*`, seven of them the consecutive run `MSG_VOICE_D_443`…`_449`. A
consecutive run with no recording reads as content cut late. All 17 are listed
in the artefact.
⚠️ **The wrinkle in that doc is real and I walked into it.** Rewriting `MSG_` to
`VOICE_` unconditionally gives `VOICE_VOICE_D_443`, which resolved only 2 353 of
2 405 and would have been written up as "52 lines with no recording", with 44 of
them a naming artefact. `sound-cue-table.md` states the exception explicitly —
a name that already carries `VOICE_` keeps the one it has — and applying it
moves the count to 2 388. Recorded here because the same trap is one line of
code away in any port.
## Naming — five routes, and all 144 land
The rule tables are pak entries keyed by `name_hash`, so they had to be named
before any of this could be read. Five routes, each an independent transform:
| route | source | named |
|---|---|---|
| 1 | any `PresetMessage_*` string appearing in any IDXD object, under `message\` | 136/144 |
| 2 | the `PresetMessage_Phase1/2/3` values of the 22 `UnitMessageSet_S<NN>.tbl` | 136/144 |
| 3 | strip `_msg` from a **message**-table name to get its rule table's name | 137/144 |
| 4 | predicted from the `Sperkers` roster (below) | 6/144 |
| 5 | sweep of the naming grammar over the 86 `<who>` tokens routes 1–4 expose | 1/144 |
| | **union** | **144/144** |
Routes 1 and 2 name **the same set** (checked as a set identity, not a matching
count). Route 3 is a strict superset of route 1 and hits **no** entry that is not
a rule table. Every one of the 144 has its `_msg` companion present.
Names are of the form `PresetMessage_<who>[_<NN>][_S<stage>-<phase>].tbl`, e.g.
`PresetMessage_Ellen_03_S03-3.tbl`, `PresetMessage_ADAN_Camael.tbl` — a set is
scoped to a character *and* a stage-phase, which is what makes 144 of them.
### ✅ The roster predicts the name
Route 4 is the interesting one, because the prediction came first. Among the
tables already named, the two-speaker ones expose a transparent rule:
<FACTION>_Fleet<X><Y> speaks (<FACTION>OP<Y>, <FACTION><X>)
TCAF_..Ship<X> speaks (ADANPL<X>, TCAF<X>)
`TCAF_Fleet{AA,AB,BA,CA,CB}` and `TCAF_17thFleet{AA,AB,BA}` and
`TCAF_17thFleetShipC` are named; **six gaps in those series** were predicted from
the six unnamed tables' rosters and then hashed:
| roster | predicted name | result |
|---|---|---|
| `TCAFOPB`, `TCAFB` | `TCAF_FleetBB` | ✅ `0x59d48b31` |
| `TCAFOPB`, `TCAFB` | `TCAF_17thFleetBB` | ✅ `0x9d29575d` |
| `TCAFOPA`, `TCAFC` | `TCAF_17thFleetCA` | ✅ `0x9d4c5353` |
| `TCAFOPB`, `TCAFC` | `TCAF_17thFleetCB` | ✅ `0x9e527c53` |
| `ADANPLA`, `TCAFA` | `TCAF_17thFleetShipA` | ✅ `0xee14c77c` |
| `ADANPLB`, `TCAFB` | `TCAF_17thFleetShipB` | ✅ `0xef1af07c` |
**6 of 6.** The control matters as much as the hits: four same-shaped names the
series does *not* contain — `TCAF_FleetCC`, `TCAF_17thFleetCC`, `TCAF_ShipA_01`,
`TCAF_17thFleetShipC_01` — score **0 of 4**, so this is not "any plausible name
hits an unnamed slot".
### 🔴 Refuted: the undeclared tables are not tutorial chatter
The eight tables that neither route 1 nor route 2 names are undeclared because
**nothing on the disc declares them** — no `UnitMessageSet` entry, and their own
names appear as strings nowhere. `UnitMessageSet_S<NN>.tbl` exists for 22 of the
28 stages and the six missing are exactly **S18–S23, the tutorials**, so
"the undeclared eight are tutorial chatter" was the obvious reading. It is
**wrong**: the eight are `PresetMessage_Katana_14_S16-2` and
`PresetMessage_Margras_04_S14-1` — both **story** stages — plus the six TCAF
fleet/ship tables above, which carry no stage tag at all. Not one is a tutorial.
What that leaves is a different and smaller claim: the tutorials ship **no**
`UnitMessageSet`, and no rule table is tagged `_S18`…`_S23`, so on this evidence
the six tutorial missions have no reactive chatter of their own.
## ✅ Correction to `squadron-orders.md`
That document renders the voice-line enum at `0x820AEEB0` as
`ORDER_{WINGMAN,SQUAD,SQUADRON}_{FORMATION,ATTACK,COVER,EXTENDED}`. The
executable's own strings are **not** uniform: the WINGMAN and SQUAD names are
`ORDER_`, and all four SQUADRON names are misspelled **`ORDOR_`**. The disc data
carries the identical misspelling, which is how the two sides were joined.
That document's 🔴 warning — that this enum is a UI/comms axis and is **not** the
order-class enum — is **confirmed and now explained**: all twelve names are
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
* 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.