re: the bark queue, Priority ordering, and Yes = a one-shot line

sub_822109B0 allocates 68-byte nodes from a pool capped at 128; the Pattern
byte at +23 doubles as the occupancy flag.  Priority orders the pending list
descending; the >=10 front-push arm is dead because the disc's range is 1..9.

The tick sub_8220FC50 does rule[+36] |= (1 << node[+25]) & rule[+32], so a
line enters the used mask only if its Yes bit is set: Yes = one-shot, No =
repeatable.  388 one-shot lines on the disc.  Closes what reads +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:17:01 +00:00
parent 8dc270091b
commit f78565fe9e
3 changed files with 121 additions and 6 deletions

View File

@@ -4298,3 +4298,49 @@ the unbounded one is what was refuted.**
queue, whether `Interval` is a per-event or per-speaker cooldown, what
`IntervalFluctuation` randomises, where `+36`'s bits get set, and what reads
`+32`. Also still open: which of the 49 speakers are wingmen.
---
## ✅ 2026-08-27 — the bark queue, `Priority`, and `Yes`/`No` settled
Continues the chatter thread, bounded to the CU as the previous entry proposed.
**Docs checked:** `preset-message-rules.md` (mine). Nothing else in the corpus
mentions `sub_822109B0`, `sub_8220FC50` or `sub_82210AF0`.
**The pending queue.** `sub_822109B0` allocates 68-byte nodes from a pool at
`this+32`; `[this+8740]` is the count and **the enqueue returns 0 once it reaches
128** — barks past that are dropped. A slot is free iff `[node+23] == 0`, and
`+23` holds **`Pattern`**, which is never 0 for a loaded rule: the presentation
field doubles as the occupancy flag. Node layout recorded in the doc.
**`Priority` orders the list, and its fast path is dead data-wise.**
Insertion tests `Priority` against **10**: ≥ 10 pushes straight to the front with
no ordering; < 10 splices ahead of the first node with a strictly smaller
`Priority`, leaving the list **descending by `Priority`**. **The disc's range is
1…9**, so the front-push arm never runs on retail data — every shipped bark takes
the ordered path. `sub_82210AF0` returns the largest `[node+20]` among pending
nodes of *higher* priority whose `[node+24] != 0x10`: a preemption/ducking query.
**`Yes`/`No` is a ONE-SHOT flag, and the `+32`/`+36` pair closes.** In the
per-frame tick `sub_8220FC50` (callers `sub_821AB650`, `sub_821B5FB8`), at
`0x82210160`:
rule[+36] = ((1 << node[+25]) & rule[+32]) | rule[+36]
The line just spoken enters the **used** mask **only if its `Yes` bit is set**.
`sub_82210670` picks only from messages whose `+36` bit is clear, so `Yes` = the
line is heard once and never again, `No` = it stays in the pool. That is what the
census counts: **8 940 `No`, 388 `Yes`** ⇒ 388 one-shot lines on the disc. It also
answers the previous entry's open question "what reads `+32`" — nothing, until a
line finishes, and then only to gate this AND.
✅ The tick branches on `Pattern < 3` (taken side uses `this+8776`), so `Window`
(3) is a different presentation path from `Sound` (1) — the two values the disc
actually ships.
**Still open, and now it is only two fields:** whether `Interval` (`+20`) is a
cooldown per event, per rule or per speaker, and what `IntervalFluctuation`
(`+24`) randomises against. Neither is read by the entry, pick, enqueue or tick
paths read so far — `sub_82210320` was checked as a candidate and is a different
structure (a deque at `this+8744`, with a divide-by-60 frames→seconds), not the
rule object. Also still open: 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`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0 (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); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128 (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

@@ -265,11 +265,11 @@ can never reach, because it skips the record first. Thirteen dead records out of
### 🟡 What this does *not* settle
`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.
`Probability`, the pick, the queue, `Priority` and the `Yes`/`No` flag are all
settled above. **What remains is only the two interval fields:** whether
`Interval` (`+20`) is a cooldown per event, per rule or per speaker, and what
`IntervalFluctuation` (`+24`) randomises against. Neither is read by the entry,
the pick, the enqueue or the tick paths read so far.
🔴 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
@@ -404,6 +404,75 @@ hand-off, not on the way in. Within the whole chatter compilation unit
`sub_82213840` (the comparator) and `sub_82210670` — which is what makes this
attribution safe where a binary-wide offset search is not.
## ✅ The queue, `Priority`, and what `Yes`/`No` actually means
**Settled 2026-08-27** from `sub_822109B0` (enqueue) and `sub_8220FC50` (the
per-frame tick, called from `sub_821AB650` and `sub_821B5FB8`).
### The pending queue
`sub_822109B0` allocates from a fixed pool: **68-byte nodes starting at
`this+32`**, with `[this+8740]` as the live count and `[this+8736]` as the list
head. **If the count is already 128 it returns 0 and the bark is dropped.**
A slot is free iff its byte at `+23` is zero — and `+23` holds **`Pattern`**,
which is never 0 for a loaded rule (the enum is 1…4). The presentation field
doubles as the occupancy flag.
| node | content |
|---|---|
| `+4` | the chosen message id |
| `+8` | the speaker entry |
| `+12` | the rule object |
| `+16` | `EffectiveTime` (frames) |
| `+20`, `+21` | zero at enqueue |
| `+22` | `Priority` |
| `+23` | `Pattern` — also the in-use flag |
| `+24` | state byte, zero at enqueue |
| `+25` | the chosen message **index** |
### `Priority` orders the list, and its fast path is dead
Insertion compares `Priority` against **10**. At or above 10 the node is pushed
straight onto the front with no ordering; below 10 it is spliced ahead of the
first node with a strictly smaller `Priority`, leaving the list **descending by
`Priority`**. **No shipped record reaches 10** — the disc's range is 1…9 — so the
front-push arm never runs on the retail data, and every bark takes the ordered
path.
`sub_82210AF0` (called only from the tick) walks the list and returns the largest
`[node+20]` among pending nodes whose `Priority` is **higher** than the one being
considered and whose `[node+24]` is not `0x10` — a preemption / ducking query
against higher-priority speech already queued.
### ✅ `Yes`/`No` is a one-shot flag — the `+32`/`+36` pair closes
The tick ends a line with exactly this, at `0x82210160`:
```
r10 = node[+25] ; the index of the line just spoken
r9 = rule[+32] ; the Yes mask, straight off the disc
r8 = rule[+36] ; the used mask, zeroed by the loader
rule[+36] = ((1 << r10) & r9) | r8
```
The spoken line's bit enters the **used** mask **only if its `Yes` bit is set**.
Since `sub_82210670` treats a message as eligible only when its bit in `+36` is
clear, the meaning is exact:
* **`Yes` → the line is one-shot.** Once spoken it is permanently removed from
that rule's pool.
* **`No` → the line repeats.** Its bit never enters `+36`.
That is what the census counts: **8 940 `No` and 388 `Yes`** — 388 lines across
the disc are authored to be heard once. It also closes the earlier open question
"what reads `+32`": nothing until a line finishes, and then only to gate this
one AND.
The tick also branches on `Pattern < 3` (using `this+8776` on the taken side), so
`Window` (3) is on a different presentation path from `Sound` (1) — consistent
with the two values the disc ships.
## 🟡 Not settled
* The *policy* the seven fields drive — see "what this does not settle" above.