Files
skate-3-rust-engine/docs/hails-additions/audio-specs/world-speech.md
T
Hails 7ef35c45b0 Audio: world gaps closed (tazer, ped falls, speech details, echo, main-cast speech, NPC board slide)
Tazer and PedBodyFall ported from the retail mechanism and checked against the recordings; ped speech gets its PEAK filter, the voice float, the speech inputs, the phone and photographer lines, first-free-stream choice and the queue clock; the speech echo send (pre-gain send -> high pass -> distance delay -> low pass -> environment bus); the main-cast speech channel for pros, NPC skaters' own speech and message pairs, with its opt-in setup decode; the NPC skater's board slide. Engine and mod surfaces extended (reactions, tazer, body fall, phone / photo events); retail values are setup-data defaults. Local player output byte-identical.
2026-10-03 21:17:18 -06:00

318 lines
22 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.
# World speech (peds) — data, mechanism, port state (2026-10-03)
Port:
- `crates/skate-audio/src/world/speech.rs`: the index;
- `speech_manager.rs`: value → event, the gate, the request words;
- `speech_rules.rs`: the `.evt` parser and the line and take choice;
- `peds.rs`: `PedSpeech`.
Setup: `tools/asset_pipeline/world_audio.py`:
- `speech_index`, with `rules` and each clip's `id` / `history`;
- `world_tuning` → `speech_tuning`;
- opt-in `decode_speech`.
Tests: the skate-audio `world::speech_*` unit tests, and `tests/world_speech.rs` on the dev install export.
Trace check: the local tool `speech_takes.py <sessions…>` (the take of each READ, the history rule, the sequence
records).
Dev install staging: `stage_world_audio.py [--decode 501,104,205,101|all]` (local tool).
Harness: `tests/world_sources.rs` `a_bumped_ped_warns_with_a_line_of_its_voice`.
## Data (`data/audio/english/livingworldspeech.big`, EB v3)
- **Clips.** 3,011 `.dat` clips named `<event>_<voice>[_<voice name>]_<line>.dat`. Examples:
- `501_59_busm1_Warn_n`
- `1901_53_Shout`
- `806_47_adtf2_Int_c14_tour`
- Voice ids run 41–96 (`adtm1`, `adtf1/2/4`, `grn1/2`, `joc2/3`, `busm1–3`, `busw1–3`, `tenm1–3`, `stdf1`,
`tenf2/3`, `secg1/2`, `torm1–3`, `torf1`, `bum1–3`, `sktm1/4/5`, `sktf1–3`). Clips without a name in the file name use
a generic voice (53, 91, …).
- **Archives inside** (EB v3, read at boot):
- `livingworldhdr.big`: one `<clip>.hdr` per clip. Layout: +2 u16 take count, +14 u16 offsets of each take's `.sth` row.
- `livingworldsth.big`: one `<clip>.sth` per clip. 12-byte rows = u32 byte offset of the take in the `.dat` + the
take's 8-byte EA SNR header.
- **Takes.** All are codec 3 (EA-XMA2), mono, 36 kHz. A clip holds 1–48 takes, 20,096 in all and 14.3 h. Free-roam events come to
about 9.3 h, about 2.4 GB as PCM16; events 101/104/205/501 alone are 518 MB.
- **Decoding.** vgmstream decodes `<take>.snr` (the 8 header bytes) plus `<take>.sns` (the `.dat` slice) pairs.
- **Recomp reads.** READ lines point at a take's offset inside the clip, not only the clip start (fixed in
`speech_nearfar.py`). `speech_takes.py` maps each READ to its clip and take.
- **`livingworld_Events.evt`** (114 KB): the speech library's event table, 81 events (`501_warn` = 0x2012, …).
- **Decoded:** `world_audio.parse_evt` and `speech_rules::EventTable::parse`. All four banks parse: maincast bank 0,
living world 1, announcer 3, cameraman 4.
- **Header:**
- +4: u32 offset of the name table (32-byte rows);
- +8: bank; +9: sub-bank;
- +0x10: u16 event count;
- +0x18: u16 event offsets × 4.
- **Event:**
- u16 id, u16 queue timeout (60–180), u16 priority (500–550);
- u8 record count;
- u8 external conditions (0 in all banks);
- u8 flags (high nibble = field count);
- u8 probability % (100);
- u8 flags2 (0), then one more byte;
- u16 record offsets × 4;
- 3-byte field descriptors `FF <request word> 04`.
- **Record:**
- weight code (0x39 = 4 × 25 everywhere in the living world);
- probability (100);
- clips << 2 | mode;
- locals;
- field count, then 3 pad bytes;
- one byte per clip (offset × 4);
- u32 field masks (0 = any);
- 8-byte clip entries (u16 `.hdr` id; the rest is 0 in the living world).
- **Fields:**
- 1 = speaker type bit;
- 2 = voice variant bit;
- 3 depends on the event: the near/far flag, zombie (1901, 201, 606, 609), the conversation partner's type (806 / 807),
or the conversation word (550–557);
- 4 = zombie for 603.
- **Type bits** (from the voices of their records):
| bit | type | voices |
|---|---|---|
| 0x1 | adult m | 41 / 42 / 43 |
| 0x2 | adult f | 46 / 47 / 49 |
| 0x4 | granny | 51 / 52 |
| 0x8 | jock | 55 / 56 |
| 0x10 | teen f | 72 / 73 / 74 |
| 0x20 | teen m | 69 / 70 / 71 |
| 0x40 | security | 75 / 76 / 77 |
| 0x80 | tourist m | 82 / 83 / 84 |
| 0x100 | tourist f | 85, **53** |
| 0x200 | skater m | 89, 91, 92, 90 |
| 0x400 | skater f | 94 / 95 / 96, 93 |
| 0x1000 | business m | 59–61 |
| 0x2000 | business w | 64–66 |
| 0x4000 | bum | 87, 88, 86 |
Variant bits: 1, 2, 4, 8 and 16.
- 133 of the 3,348 living-world clip references have no clip (for example 603's tazer lines for some voices). Those
records never play.
- **`.hdr`:**
- u16 id;
- +2 flags (0 in all banks);
- +3 take count;
- +4 runtime;
- +8 history length: the take count, or 0 for 685 clips (conversations 550–557, zombie lines, 497 Silence);
- +9 / +10 size scale;
- +16 u16 `.sth` row offsets;
- then the history ring (cursor + entries, 0xFF = empty).
## Mechanism (TU3)
- **State graphs.** The ped state graphs (`data/state/livingworldentities/pedestrian/aigraph/*.xml`) send
`SendSpeechEvent speechevent=… speechvalue=N` on state entry. The values are listed in `speech::SPEECH_VALUES`, for example
10 CollisionNearbyReaction (wanttoobserve), 25 StopCheer (slamreaction), 23 LongCheer (nearbyskatertrick),
20 Flee, 14 JoinChase, 17 AttemptTakeDown, 19 TakeDownSuccess, 12 ChaseResting, 66 EscapedEndChase.
- The warn (11, `pedestrian_dowarning.xml`) and takedown success are commented out ("moved to the code").
- The value lands in the ped audio state +136. Footstep packets read the same field (jump / collision).
- **SFXObj_PedestrianSpeech** process `sub_824D9908`:
1. When +136 changes, it builds a request to the speech manager `*(0x830CFDDC)`: `sub_824AB6C8` (or `sub_824AC438` with a
target).
2. flag = 1 if +148 > +156 else 2.
3. Values 7 / 8 are remapped to 30 after a 29, else 51.
4. Value 49 goes to `sub_824D9C70`.
5. Value 29 (photographer) repeats on a timer while a global flag is set.
- Ported: `PedSpeech::process` (49 and the 29 timer are not).
- **Speech manager** (`Sk8::Audio::TheSpeechSystem`, global `*(0x830CFDDC)`). **Ported** (`speech_manager.rs`).
- **Request block** (`sub_824D9908`):
- w0 = `+96` type bit, w1 = `+88` variant bit, w2 = flag, w3 = `+92`, w4 = `+120`, w5 = `+124`;
- speaker slot = `+84`.
- The living-world path needs `+116 != 0`. Otherwise `sub_824AC438` sends main-cast events (pros; not ported).
- **Value → event** (`sub_824AB6C8`): the table in `event_for_value`.
- Coin flips use `rand()`; bums use `rand() % 3`; guards get radio lines.
- Value 6 with `+71` also sends main-cast event 0x8017 (not ported).
- **The request** (`sub_824ABA18`), in order:
1. Game-state preconditions.
2. The vault tuning of the event (class `Hash_9C1F48F5D637E275`, `SPCHType_1_EventID`).
3. Timers (`sub_824A8C78`): one per speaker slot × 81 events, f32 seconds, starting at 0.
- The same event needs ≥ `+24` s.
- Any event needs ≥ `+8` s, when `+8` > 0.
- Not-follow needs strictly more than its time. Ids 0 / 294 / 8318 / 33245 / 24752 are skipped.
4. `sub_824A75F0`:
- probability: `rand() % 1000 × 0.1f` < `+20`, or ≥ 99.9;
- the player's speed × 3.6 within `+32` / `+36`;
- the challenge list;
- the `+40` / `+44` timers;
- the `+49..51` game flags;
- zombie mode needs `+60`.
5. w9 = 2 in zombie mode (virtual `[-620]+212` vfunc 156; the `zomb_Shout` records prove it).
6. `sub_824A73F0`: interrupt by priority (not ported).
7. `sub_824ABD90`: the request words of each event.
- The timers restart when the line starts (`sub_824A90B8` from `sub_824A84B8`).
- **Speech library** (generic, `sub_829717A0` …). **Ported** (`speech_rules.rs`).
- `sub_82971480`: the event probability (fails when `(draw>>16)·100>>16 > p`), then the queue slot (not ported).
- `sub_82973CB8`:
- the weighted record order (`sub_82972980`): weight = 4^(b>>5)·(b&31), scale table at 0x82FDB3D8;
- per record: a probability draw, the field match (`sub_82973BD8`) and the clips (`sub_82972D70`: at most 12; a
missing clip fails the record).
- Candidates per clip (`sub_82972660`): the takes not in the history ring; when every take is in it, the oldest one.
- Pick (`sub_82974220`):
- index = `(draw>>16)·n>>16`;
- redrawn (at most 32 draws) while it is among the last min(n/2, 10) picks of the same header in a global 32-entry
(index, header) ring;
- if every draw hits, the one whose match is oldest.
- `sub_82973408`: writes the history when the line starts.
- Generator: add-with-carry at 0x82FDB3C0. Retail shares this state with the grain player's title generator.
- **The queue is not ported** (`sub_82971340` / `sub_82971890` / `sub_82971DA8`):
- 16 request slots and 8 streams;
- highest priority wins, then the newest; older requests are dropped;
- timeout = event +2, in a clock whose unit is not known.
- The recomp plays several living-world lines at once (`overlap.py` (local script)), so the living-world
channel is not exclusive. How is not resolved.
## Reaction → event (measured; `speech_reads.py`, sessions 161849 / 163809 / 164620; the clip starts 0.03 s after)
| reaction | events (evidence) |
|---|---|
| bump → warn | 501 Warn_n (M) |
| bump → warn + taze (female) | 501 Warn_f (M); 330 / 331 tazer lines (name) |
| bump → flee | 108 ChsFlee (M) |
| slam nearby | 104 Slam_n (M) |
| collision nearby | 205 SpecCol, 204 Gasp, 202 HImpRct (M) |
| trick nearby | 101 SpecPos_n (M) |
| chase start | 105 SpecChs bystanders (M), 603 chaser (name) |
| greet / returngreet / conversation | 1901 Shout, 806 Int_c<n>, 807 Rct_c<n> (M) |
| ambient (no reaction) | phone 4402 → 805 → 4405 with 497 Silence; bums 206 / 207; guard radio 102; pro lines from maincast (906/101/130) |
`speech::REACTION_CUES` holds this table, for logs and as evidence. The real choice is `SpeechManager::request`.
`speech::choose` (uniform) is only a fallback for hosts without the rules export.
- **`_n` / `_f` lines: solved from the data.** Request flag 1 (ped `+148 > +156`) means far.
- The flag-1 records name `101_51_GenPos_Grn1_far` / `104_51_grn1_Slam_far`, and the flag-2 records `…_near`.
- All 366 `_f` / `_n` records of 101 / 104 / 202 / 203 / 501 split the same way (`tests/world_speech.rs`).
- **Settled 2026-10-03 (`world-audio-hookin-spec.md` §7.3 G2):** `+148` = the ped's distance to the listener,
`+156` = `aud_characteristics` field `A27215A909135B62` (20 m for peds, 30 for pros), so `_f` lines play beyond 20 m.
- (Was:) what `+148` / `+156` hold (a distance and a threshold?) is not traced. `speech_nearfar.py`'s distance join is still
unreliable.
- **Take choice vs the recomp** (`speech_takes.py`, 10 clean sessions; reads inside > 500 ms audio stalls left out):
- 57 of 57 clips with 2+ plays follow the history rule, including a full cycle. A uniform pick would have repeated a
take in about 13.6 of them.
- 15 of 15 multi-clip lines are one record, in order.
- The first takes of 806 lines are often 0 (6 of 13 in 164620). This may be chance, or a pairing we cannot see. Keep an
eye on it.
- **Coverage.** 40 voices have 501 and 104 lines, 39 have 205, 36 have 101 (harness print).
## Playback (port)
- `speech::SPEECH_BANK` (`1 << 22`) mixer bank. `SpeechSlots` maps (clip, take) → slot. Takes play as direct voices.
- The harness renders a busm1 warn take (5.49 s, peak −9 dBFS at unity gain).
- **Level / pan: open** (next step).
- The stream request (`sub_82973408` → `sub_82C5F310`) is 9 words: offset, size, channel, and the context (ped `+64`).
It goes to a SpeechBank stream. The voice's level is set where those requests are used; that code is not found yet.
- A lead: in 163809 a speech READ comes with a `GAIN … 0.0 → 1.0` and a `PITCH` ratio of ~0.52–0.70 on one voice
(0x40C43350).
- The speech manager reads the PedestrianSpeech owner outputs itself, and which of its 24 outputs is
not known (E0 out2 −1000 dB via B2 is the main volume candidate; out13/14 filters; out11/12 via B20 = 2–70 m
camera distance).
## Waiting on the engine
- **The ped system** must provide:
- speech values on state entry (`PedState.speech_value`);
- the speaker fields (`Speaker`: slot, type bit, variant bit, partner type);
- the near/far pair;
- zombie mode;
- the player's speed;
- a manager clock.
- **The game host:** world_sources logs requests (`AUDIO_WORLD speech …`) but plays nothing. To play, it needs:
1. the index and `rules` from `speech/livingworld.json` → `SpeechIndex::set_ids`, `EventTable`,
`Library::new(ClipHeader…)`;
2. `world_tuning.speech_tuning["1"]` → `EventTuning`;
3. `SpeechManager::request` for each `SpeechRequest`;
4. `SpeechIndex::picks_to_lines` → `SpeechSlots` with the decoded takes, played in sequence;
5. the level mapping.
The JSON → struct mapping is in `tests/world_speech.rs` `load()`.
- Maincast / cameraman / announcer speech (pro skater lines around the player, the cameraman's "own the spot"):
same format, not indexed yet.
## Next steps
1. Level / pan: find the code that uses the SpeechBank stream requests (the 9-word requests `sub_82C5F338` queues) and the
speech voice's GAIN / SEND. Or hook it (category `audio`) and join it with the ped distance.
2. What `+148` / `+156` are: find who writes the ped audio state, or add a first-pass hook on `sub_824D9908` that logs
both.
3. The request queue and stream count for the living world (why lines overlap in the recomp).
4. Main cast / cameraman / announcer: they use the same library and already parse. Their managers differ (other vault
types, `sub_824AC560`).
## Speech details resolved (2026-10-03, doc 15 "Gaps closed")
- **Block layout** (PedestrianSpeech update `sub_824D9370`, block = obj+44; PlayerSpeech `sub_824DA300`, obj+48):
+4 speaker voice id (parsed from the clip name by `sub_824A89E8`), +8 main level, +12 the per-voice float
(S+152), +16 raw azimuth, +20 pitch, +24 HP (ped out13 / skater out8), +28 LP (out14 / out9), +32 second level
(out15… / out10…), +36/+40/+44 the PEAK curves' values, +48 a delay = min(S+148 × 1.0 [field BF48032DB145C5B4]
× 1/344, 0.15) s refreshed every 4 frames (field 510BAFA32A76B340), +69 far flag, +72 the stream the speaker
holds (−1 none), +76 speaking, +80 level 21 (ped) / 13 (skater), +84/+88 filters 22/23 (ped) / 14/15 (skater).
- **PEAK filter = head shadow by azimuth.** raw out0 folded (> 32767 → 65536 − raw), then three 8-point curves of
record `B29C3B2C13D96482` `default` (holder `*(0x830CFDA4)+44`): centre `2C166907CF51DB88` (600 → 4000 at the
side → 600), gain `EA2C18D9CE5CBA3A` (0.4 → 0.1), Q `CF8679F540B82B2B` (3). Recomp 163809: 6 / 6 PEAK (centre,
gain) pairs lie on the two curves at one azimuth (≤ 1 Hz); ours at the joined geometry 3 / 6 within 10 % (camera
orientation estimated). Ported: `SpeechVoiceTuning`, export `world_tuning.speech_voice`, mixer stream graph.
- **Two sends.** The stream voice graph (`sub_82C5C318`): Rsp0 → PI20 → Sen0 (Send A, pre-gain, env) → Gai0 →
HI20 → LI20 → Sen0 (Send B) → Pn21 → Sen0. Recomp modules `+0x570` (A) and `+0x7D0` (B). A = level 21 (ped) /
13 (skater): 17 of 29 lines within 25 % (out15 there: 11); B = out15: 18 of 27. Send B feeds the stream slot's
echo submix (`sub_82C5E2D8`: Sub0 → HI20 → Del0 → LI20 → Pn21 → Sen0 → the env bus; delay = block +48; filters
likely 22 / 23): **not ported** (Send B is computed, not played).
- **Value 49** = the phone: `sub_824D9C70` stops the old ring and starts CellPhone_Rings (Splice index 6) container 5
(class `C1831BDB6CB1B1EA` `cellphone` field `031EFDF991638985`); `sub_824D9AD8` (end of the update) follows it
with the main level / pitch / azimuth and, when it ends, requests value **64** (`4402_cell_greet`). Recomp
164620: SPLC 6/5 at 88.73 → 4402 HelCel 89.73; 234.38 → 235.57; the rings' records last 0.84–1.13 s.
- **Value 29**: while system byte `*(0x830CFDC4)+912` is set, obj+160 accumulates dt; at ≥ 1.0 (image constant) it
resets and the request repeats (the flag's meaning not traced: `LivingWorldAudio::photo_flag`).
- **Obj:Speech inputs** (SFXObj_Speech process `sub_824E2050`, every frame): in0 / in1 / in4 = 32767 while a playing
line's speaker (block +4) is 37–38 / 75–77 / 1–29; in2 = speech-system word `+0x1BD28` == 2; in3 = a block with
+60 == 0 and +93 set (neither traced). F45 (+200 mB on ped out2 / out3) is in0 → not regular peds: the ~+0.6 dB
of the recomp's near lines stays unexplained (E0's other terms A[Global.2], F[Global.28] / [108], A[Global.112]).
Ported: in0 / in1 / in4.
- **Which stream**: a new line takes the first free stream (k0 when both are free: 95 of 109 new lines in
163809 / 164620 / 180430). The interrupt reads record `channel × 2 + block +72` (the speaker's own stream;
−1 aliases the previous channel's second record).
- **Queue timeout unit**: the library's clock = the speech system's time callback `sub_824A4E90` =
`[[0x830CFD94]+16]` (installed by `sub_82C5EC20`); its unit was not settled from the code (no writer found).
- **Constructor** `sub_824D90D8`: obj+36 (last value) = 68, obj+40 = 8318, +156 = −1 (ported: `PedSpeech::default`).
- **Main-cast path (pros, S+116 == 0)**: `sub_824AC438`: 29 → event 141 (speaker word 136), 28 → 11 (when
`sub_82487ED0` is false and system +1196 == 0), 30 / 51 → stop the speaker's stream, then 6 or 115 (`rand() & 1`),
53 / 54 → 77, else nothing; request `sub_824AC560` (event id → index in a 72-entry table at `0x8224CB80`, vault
record at mgr + (index + 194) × 8, tuning bank "0" — exported) with words [S+100, S+104, flag 2/1 swapped, 0,
S+108, S+112]. **Not ported:** needs the main-cast speech index + decode (`maincastspeech.big`).
## The echo send and the main cast (2026-10-03, ported; corrects "Two sends" and "Queue timeout unit" above)
- **The stream system** (`sub_82C5CEF0`, every frame per speech stream): gain = block +8 / 32767 × block +12 (the
per-voice float `S+152`, `aud_characteristics` `2087A3290483BB4F`, 0.8–1.4); `Send(desc+32)` = block +32 (ped
out15 / skater out10) × the float, the post-filter send straight into the environment bus; `Send(desc+16)` =
block +80 (ped out21 / skater out13) × the float, the pre-gain send into the slot's echo submix. The echo's
HI20 = block +84 (ped filter 22 / skater 14), LI20 = block +88 (23 / 15), Del0 = block +48, posted only when it
changed; Pn21 and Sen0 keep their class defaults. **Correction:** the first decode had Send B (out15) feeding
the echo; the code says the pre-gain send (out21) does, and out15 goes to the environment bus.
- **Delay refresh** (`sub_824D9370`): a per-speaker countdown; at ≤ 0 the delay is recomputed from the camera
distance and the count reloads with field `510BAFA32A76B340` (4).
- **Ported:** `skate_audio::bus::speech_echo` (one graph per stream slot, channel × 2 + k; mono, the Pn21 / Sen0
routing as a unity tap), the mixer's stream path (PEAK → echo send → gain → HPF / LPF → env send), the voice
float, the delay countdown. Setup `world_tuning.speech_voice` carries `delay_factor` / `delay_frames`.
- **Recomp, re-verified** (39 lines, 163809 / 164620 / 180430, with the voice float): pre-gain send (`+0x570`) =
out21 within 25 % in 17 of 29 (out15: 10); env send (`+0x7D0`) = out15 in 19 of 27; gain median 0.988
(p10 0.55, p90 1.36); filters 4 of 5; PEAK on the curves 6 / 6.
- **Queue timeout unit (settled):** `[[0x830CFD94]+16]` is the function behind the `GetVisualGameTick` Lua binding
(`0x8283C2D8`): visual game ticks, one per rendered frame (the console's ~30 fps), the port's console frames.
- **Main-cast channel** (bank 0, channel 0; `maincastspeech.big`, 1283 clips, 73 events, tuning `speech_tuning["0"]`):
- who: a model with no living-world type bit and a cast bit / word (`6F2933E977CF40DD` = the pros' bit 1 << (n−1),
`14FD437D190677C8` = the special cast 30–38, `D6EA428C2B43E23A` = the word another pro's line names it by);
- words (`sub_824AC898`, block w0 = cast bit, w1 = cast word, w2 = 1 near / 2 far, w4 / w5 = the other skater's
cast bit / `D6EA` word): events 0 / 1 → [w1, w0, w2]; 247 → [w0, w9, w1]; 251 → [w0, w1, w10];
254 → [w1, w5, w0, w4]; 268 → [w0, w1, w11]; 271 / 274 / 275 → [w0, w1, w12]; 280 → [w0, w1, w13];
287 / 288 → [w1, w0, w4, w5]; else [w0, w1];
- NPC skater speech (`SFXObj_PlayerSpeech` non-local process `sub_824DA1B0`, inputs from the skater entry via
`sub_824B6E80`: `+102` = entry `+392` bit 4, `+103` = bit 3, `+104` = entry `+396` bit 10, `+131` = entry `+388`
bit 5). Living-world voices (`sub_824DAAC0`): `+102` / `+103` → 8204 in mode 55, 8239 in modes 19 / 20, else
8234 | (`rand()` & 1); `+104` → 8202; the chase flag (system `+1100` bit 6) → 8205 when nothing else; a request
only on change. Pros (`sub_824DA768`): `+102` → 1 (16 in modes 19 / 20 by the player) or 288 by a pro
(`+112`); `+103` → 1 / 288 by `+116`; `+104` → 0 / 287 by `+108`; requested every frame the byte holds. The
skater's crash (`sub_824DB688`, `+131` rising, latched in obj `+193`): message 8229 / 125;
- skater message pairs (`sub_824DAC00`, living-world / main-cast): bail grunt 8206 / 115, crash 8229 / 125,
impact 8233 / 6 and 8241 / 125 (`sub_824BD358`), hit reaction 8309 / 253 and race punch 8310 / 250
(`sub_824EFA60`), gesture 8291 / 247 (`sub_824F8778`).
- **Main-cast decode:** the free-roam events (101, 104, 130, 150, 151, 201, 202, 335, 336, 338, 344, 500, 501, 601,
903, 906, 913, 1014): 6596 takes, 3.0 h, 922 MB of 44.1 kHz PCM16 (opt-in with the living world's).
- **Recomp check:** 82 main-cast lines streamed in 11 sessions (READs of `maincastspeech.big`: 906 `AiSlam` ×45,
101 `pos`, 150 `pro_pos`, 104 `Slam`, 130 `col`, 201 `grunt`): all reachable through the ported words for their
speaker (76 of an event the port sends; 130 `_col` has no ported sender).
- **Not ported:** the repeat times of speaker slots 30 / 31 (record `+52` / `+56`); values 30 / 51 stopping the
speaker's stream first; the cameraman line of the crash; the game modes (free skate passes 0).