re: the chatter phase merge is not additive; 224 authored variants never play

sub_82215A58 reads CrewCount + PresetMessage_Phase1/2/3 and reaches the loader
from ONE call site, so the three phase tables fold into one map keyed by the
event record name.  Every merge collides; sub_82213840 reconciles on the
message list plus +16/+17/+18/+20/+24/+28 (NOT the +32 Yes mask), and the
incumbent always wins.  Measured: 26432 collisions, 26208 identical, 224
different (189 differ only in the message list), 0 mask-only differences.

Refuted handle: intersecting functions by the object's offsets finds dozens of
unrelated layouts -- offset shape is not an identifier.

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:50:42 +00:00
parent c4fd90baa9
commit 6ee041c1bf
5 changed files with 179 additions and 5 deletions

View File

@@ -4199,3 +4199,49 @@ event or per speaker, and what `IntervalFluctuation` randomises against all live
in the *consumer* of the 40-byte object, which this iteration did not read.
`Pattern`'s `Window` is named, not measured. Also still open from the earlier
entries: which of the 49 speakers are wingmen.
---
## ✅🔴 2026-08-27 — how the three phase tables merge, and one refuted handle
Continues the entry above. **Docs checked:** `preset-message-rules.md` (mine),
`mission-phase-deployment.md` and `mission-phase-membership.md` (they own
`UnitMessageSet_S<NN>.tbl` and its `CrewCount` + `PresetMessage_Phase1/2/3`
what they do **not** say is what happens when the three tables are loaded),
`idxd-tag-hash.md` (owns the hash family the map key comes from).
**The three phase tables become ONE map.** `sub_82215A58` references exactly
`CrewCount` and `PresetMessage_Phase1/2/3` and reaches the loader from a **single**
call site (`0x82215D98`). The map is keyed by a hash of the *event record's name*
(`sub_82455C78`). All 64 event names recur in every phase table, so **every merge
collides**.
**`sub_82213840` is the reconciliation comparator** — called from nowhere but
the loader. It compares the two message-id vectors elementwise plus `+16`
`Probability`, `+17` `Priority`, `+18` `Pattern`, `+20` `Interval`, `+24`
`IntervalFluctuation`, `+28` `EffectiveTime`. It does **not** compare `+32` (the
`Yes` mask) or `+36`. On a collision the loader frees the newcomer and keeps the
incumbent either way; a mismatch also clears the byte it returns, which the
caller propagates.
**Measured on disc** (regenerator extended, numbers in `data/preset-messages.txt`):
298 `UnitMessageSet` records name ≥2 phase tables; **26 432** duplicate-key
insertions; **26 208 (99.15 %)** identical; **224 different**, of which **189
differ only in the message list**. ⇒ **The phase tables are not additive**
phase 1 wins and **224 authored variants never play**. A port that merges
additively, or lets a later phase override, will not match the game.
**A negative worth keeping:** **zero** pairs agree on the six compared fields
while disagreeing on the `Yes` mask, so the comparator's exclusion of `+32`
changes nothing on the shipped disc. It looks like a latent bug until counted.
🔴 **Refuted handle for the firing policy.** I tried to find the consumer by
intersecting functions that touch `+16`/`+17`/`+18`/`+20`/`+28`/`+32`/`+36`.
It returns dozens of unrelated functions across the binary — **offset shape is
not an identifier**, which the corpus already warns about and I re-learned by
spending a query on it. The remaining handle is the map's consumer reached via
`sub_82215A58``sub_82214050`.
**Still open:** the firing policy proper (who rolls `Probability`, compares
`Priority`, ticks `Interval`, what `IntervalFluctuation` randomises); 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` (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) (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

@@ -162,6 +162,19 @@ unresolved : 17
MSG_VOICE_D_447
MSG_VOICE_D_449
phase merge (the loader folds Phase1/2/3 into one map):
UnitMessageSet records naming >= 2 phase tables : 298
duplicate-key insertions to reconcile : 26432
identical on the compared fields : 26208
DIFFERENT -> loader clears its ok flag : 224
compared fields equal but Yes mask differs : 0
x189 MESSAGE_LIST
x17 Probability, Priority, Pattern, Interval, EffectiveTime, MESSAGE_LIST
x14 Probability, Priority, Pattern, Interval, MESSAGE_LIST
x2 Probability, Priority, Interval, IntervalFluctuation, MESSAGE_LIST
x1 Probability, Priority, Pattern, Interval, IntervalFluctuation, EffectiveTime, MESSAGE_LIST
x1 Probability, Priority, Interval, IntervalFluctuation
naming route 1 (any harvested PresetMessage_ string): 136/144
naming route 2 (UnitMessageSet_S* declarations) : 136/144
naming route 3 (strip _msg from a message table) : 137/144

View File

@@ -265,13 +265,71 @@ can never reach, because it skips the record first. Thirteen dead records out of
### 🟡 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
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
are named, but that `Window` means an on-screen text window is a reading of the
name, not something measured.
## ✅ How the three phase tables are merged — and what that costs
**Settled 2026-08-27.** `sub_82215A58` is the `UnitMessageSet` reader: it
references exactly `CrewCount`, `PresetMessage_Phase1`, `PresetMessage_Phase2`
and `PresetMessage_Phase3`, and it reaches the loader from **one** call site
(`0x82215D98`). So a crew's three phase tables are loaded into **one** map, not
three. The map is keyed by a hash of the *event record's name*
(`sub_82455C78`, the rolling-accumulator / character-sum-top-byte shape
[`idxd-tag-hash.md`](idxd-tag-hash.md) documents; which member of that family
this entry point is was not pinned here, and the finding does not depend on it).
Because all 64 event names recur in every phase table, **every merge collides**.
`sub_82213980`'s insert therefore has a reconciliation arm, and
`sub_82213840` — a function called from nowhere else — is its comparator.
### `sub_82213840` compares six of the eight fields
It walks the two message-id vectors elementwise, then compares `+16`
(`Probability`), `+17` (`Priority`), `+18` (`Pattern`), `+20` (`Interval`),
`+24` (`IntervalFluctuation`) and `+28` (`EffectiveTime`), and returns a bool.
It does **not** look at `+32` (the `Yes` mask) or `+36`.
On a colliding key the loader compares, and then — whatever the answer —
**frees the newcomer and keeps the incumbent**. A mismatch additionally clears
the byte the loader returns (`lbz r3, 80(r31)` at the epilogue); the caller
turns that into a flag at `0x82215D9C``0x82215DDC` and propagates it.
### What the disc actually does with that
Measured over the 22 `UnitMessageSet_S<NN>.tbl` (numbers in the artefact):
| | |
|---|---|
| `UnitMessageSet` records naming ≥ 2 phase tables | **298** |
| duplicate-key insertions to reconcile | **26 432** |
| identical on the compared fields | **26 208** (99.15 %) |
| **different** — loader clears its flag | **224** |
| compared fields equal but `Yes` mask differs | **0** |
Of the 224, **189 differ only in the message list** — per-phase dialogue
variants for the same event and the same timings. The other 35 also move
`Probability`/`Priority`/`Pattern`/`Interval`.
**The phase tables are not additive.** For a given crew and event, phase 1's
rule wins and the later phases' are discarded — so **224 authored variants never
play**. A port that merges them additively, or that lets a later phase override,
will not behave like the game.
**The comparator's blind spot is never exercised.** There is no record pair
that agrees on the six compared fields and disagrees on the `Yes` mask, so the
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.
## 🟡 Not settled
* The *policy* the seven fields drive — see "what this does not settle" above.