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:
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user