fix(client): bound actor sound channels with a recycling pool (#489)#540
Merged
Conversation
Per-step actor sounds were fire-and-forget: the footstep EmitSound in the client loop, the attack/hit/death EmitSound in PlayActorSound, and the P_Sound packet handler all discarded the channel handle EmitSound returns and never reclaimed it. With several actors moving and fighting, the audio backend's sources accumulate until allocation fails and the client hard- crashes (no RuntimeError, empty Client Log) -- issue #489. The managed SoundZone path already avoided this by keeping its handle and ChannelPlaying()-checking it; the per-step actor paths did not. Add EmitActorSound() in Actors3D.bb: a fixed-size ring of channel handles that StopChannel()s the slot it is about to reuse (LRU reclaim), capping the live actor-sound channel count at ActorSoundPoolSize (24) regardless of session length or actor count. Route the three unmanaged actor-positioned EmitSound sites through it. The Snd=0 guard also skips emitting silence on a missing/unregistered sound id, a small improvement over the old raw call. The ClientNet.bb comment shifted downstream handler line numbers, so the auto-generated packet index is regenerated (line-number shifts only, no packet semantics changed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replicate EmitActorSound's bookkeeping verbatim (non-Strict, audio primitives stubbed, pool size reduced to 4 for legible assertions) and assert: Snd=0 short-circuits, the first fill reclaims nothing, the cursor stays in [0,size), the oldest slot is reclaimed LRU on wrap, the live channel count is capped at the pool size, and a finished slot isn't needlessly re-stopped. Pins the issue #489 fix against regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
|
Dang this one is huge nice work! |
6 tasks
CoreyRDean
added a commit
that referenced
this pull request
Jun 9, 2026
…sounds docstring Keep the tool header in sync with the extended PLAN (reviewer follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rockinraymond
pushed a commit
to rockinraymond/rcce2-mythic
that referenced
this pull request
Jun 9, 2026
…ase project The default project shipped silent of footsteps and creature attack/hit/death cries: every actor template's MSpeechIDs/FSpeechIDs Speech arrays were cleared as a data-side workaround for RydeTec#489 (fire-and-forget EmitSound exhausted the client's audio channels and hard-crashed it after a sustained session). That engine bug is fixed -- actor sounds now route through the bounded recycling channel pool EmitActorSound (RydeTec#540), which caps the live channel count -- so the PR that landed the fix explicitly deferred re-enabling the data to the default-project owner. This is that follow-up. Wire the Speech arrays via the existing surgical tool (tools/projectgen/ set_actor_sounds.py), extended to also cover the Orc/Warlord mini-boss (Grukk), which is a clone of the Orc/Raider on the same Troll mesh and so reuses the troll vocalizations -- otherwise the boss would stay mute. Final wiring: - Human/Fighter (player): FootstepDry=0, FootstepWet=1 - Rat/Critter: Attack1=16, Hit1=17, Death=18 - Orc/Raider, Orc/Warlord: Attack1=19, Hit1=20, Death=21, FootstepDry=1 This lights up actor audio in BOTH clients at once -- the Rust client already reads these same arrays for combat voice (RydeTec#484/RydeTec#495/RydeTec#504) and was silent too. Verification (data-level; live Graphics3D launch is blocked in this env): field-level diff vs HEAD shows exactly 8 speech-array changes and 0 non-speech changes; codec re-encode is byte-stable (2672 -> 2672 bytes); every wired sound id resolves in Sounds.dat (the tool asserts this) and audit_media_refs is clean. The crash that motivated disabling these is unit-tested fixed by RydeTec#540's ActorSoundPoolTest (channel count provably capped); residual risk is acoustic and fully reversible by re-running the tool with cleared arrays. Update docs/sample-project-guide.md: the per-actor-sound note no longer claims the bug is unfixed -- it now describes the wired-on state and the pooled path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #489.
Non-technical summary
After a sustained play session with several audible actors moving and fighting, the game client (
bin/Client.exe) would hard-crash — no error dialog, empty log. This PR fixes that. Actor sound effects (footsteps, combat/death cries, and server-triggered positional sounds) now reuse a small fixed set of audio channels instead of allocating a fresh one every time and never giving it back. The client can now play actor audio indefinitely without exhausting the sound backend.Technical summary
Per-step actor sounds were fire-and-forget: three sites called
EmitSound(...)and discarded the channel handle it returns, so the OpenAL backend's sources accumulated until allocation failed and the process crashed (issue #489). The managedSoundZonepath already avoided this by keeping its handle andChannelPlaying()-checking it; the per-step actor paths did not.EmitActorSound(Snd, EN)inActors3D.bb— routes every actor sound through a fixed-size ring (ActorSoundPoolSize = 24) of channel handles. Before reusing a slot itStopChannel()s whatever is still playing there (LRU reclaim), so the live actor-sound channel count is capped at the pool size regardless of session length or actor count. Mirrors the existingSoundZonereuse pattern.EmitSound(Client.bb), the attack/hit/deathEmitSoundinPlayActorSound(Actors3D.bb), and theP_Soundpacket handler (ClientNet.bb) — the last is a server-triggerable sibling of the same leak, included per the repo's sibling-asymmetry doctrine.Snd = 0guard skips emitting silence for a missing/unregistered sound id — a small improvement over the old rawEmitSound(0, ...).ClientNet.bbcomment shifted downstream handler line numbers, so the auto-generateddocs/protocol/index.mdis regenerated (line-number shifts only — no packet semantics changed).No breaking changes. The helper is additive; the call-site swaps are behavior-preserving except for the channel cap and the
Snd=0skip.Acceptance criteria + test results
New non-Strict regression test
src/Tests/Modules/ActorSoundPoolTest.bb(passes undertest.bat ActorSoundPool) replicates the pool bookkeeping verbatim with the audio primitives stubbed, and pins:Snd=0short-circuits — returns 0, no channel consumed, cursor unchanged, nothing stopped.poolSizeemits hit empty slots; cursor wraps to 0.[0, poolSize)across many emits (Dim index always in bounds).K - poolSizechannels were stopped, so at mostpoolSizeare ever live.ChannelPlayingFalse) is overwritten without a needlessStopChannel.compile.batclean across all five engine targets + all seven tools (Actors3D.bbis shared by Client and GUE).gen_packet_index.sh --check,gen_bvm_reference.sh --check) pass.Trade-offs, deferred follow-ups, remaining gap
Graphics3Dis blocked in this environment, so the no-crash outcome is established by the issue's own bisection + the unit-tested channel cap + static reasoning, not a live multi-actor session. The fix is the approach the issue itself recommends.data/(currently disabled as the data-side workaround) is deferred to the default-project owner — it's a content change, not an engine change, and is safe to flip once this lands.