fix: split SoundId into newtype + KnownSoundId enum
Browse filesSoundId is now an opaque newtype wrapping u32, accepting any WA sound
ID including speech/voice lines (IDs > 126). The known SFX enum is
renamed to KnownSoundId with From<KnownSoundId> for SoundId.
Fixes worm voice lines being silently dropped β the previous refactoring
made hook trampolines reject IDs outside the 1-126 enum range via
TryFrom, but WA's speech system uses higher IDs. Hooks now wrap the
raw u32 in SoundId unconditionally.
play_sound_local accepts impl Into<SoundId> so both KnownSoundId
variants and raw SoundId values work at call sites. Dynamic sound IDs
from weapon table data use SoundId(raw) directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/audio/mod.rs
CHANGED
|
@@ -15,6 +15,6 @@ pub use dssound::{
|
|
| 15 |
};
|
| 16 |
pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
|
| 17 |
pub use music::Music;
|
| 18 |
-
pub use sound::SoundId;
|
| 19 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
| 20 |
pub use streaming::StreamingAudio;
|
|
|
|
| 15 |
};
|
| 16 |
pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
|
| 17 |
pub use music::Music;
|
| 18 |
+
pub use sound::{KnownSoundId, SoundId};
|
| 19 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
| 20 |
pub use streaming::StreamingAudio;
|
crates/openwa-core/src/audio/sound.rs
CHANGED
|
@@ -1,9 +1,23 @@
|
|
| 1 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
///
|
| 3 |
/// Source: wkJellyWorm Constants.h
|
| 4 |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 5 |
#[repr(u32)]
|
| 6 |
-
pub enum
|
| 7 |
Morse = 1,
|
| 8 |
CrowdPart1 = 2,
|
| 9 |
CrowdPart2 = 3,
|
|
@@ -132,12 +146,12 @@ pub enum SoundId {
|
|
| 132 |
RocketPowerdown = 126,
|
| 133 |
}
|
| 134 |
|
| 135 |
-
impl
|
| 136 |
pub const MIN: u32 = 1;
|
| 137 |
pub const MAX: u32 = 126;
|
| 138 |
}
|
| 139 |
|
| 140 |
-
impl TryFrom<u32> for
|
| 141 |
type Error = u32;
|
| 142 |
|
| 143 |
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
|
|
| 1 |
+
/// Opaque sound ID. WA uses a wider range than just the known SFX enum
|
| 2 |
+
/// (e.g., speech/voice lines use IDs above 126). This newtype wraps the
|
| 3 |
+
/// raw u32 value and can be constructed from [`KnownSoundId`] variants.
|
| 4 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 5 |
+
#[repr(transparent)]
|
| 6 |
+
pub struct SoundId(pub u32);
|
| 7 |
+
|
| 8 |
+
impl From<KnownSoundId> for SoundId {
|
| 9 |
+
#[inline]
|
| 10 |
+
fn from(known: KnownSoundId) -> Self {
|
| 11 |
+
Self(known as u32)
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/// Known sound effect IDs. Range 1-126, contiguous.
|
| 16 |
///
|
| 17 |
/// Source: wkJellyWorm Constants.h
|
| 18 |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 19 |
#[repr(u32)]
|
| 20 |
+
pub enum KnownSoundId {
|
| 21 |
Morse = 1,
|
| 22 |
CrowdPart1 = 2,
|
| 23 |
CrowdPart2 = 3,
|
|
|
|
| 146 |
RocketPowerdown = 126,
|
| 147 |
}
|
| 148 |
|
| 149 |
+
impl KnownSoundId {
|
| 150 |
pub const MIN: u32 = 1;
|
| 151 |
pub const MAX: u32 = 126;
|
| 152 |
}
|
| 153 |
|
| 154 |
+
impl TryFrom<u32> for KnownSoundId {
|
| 155 |
type Error = u32;
|
| 156 |
|
| 157 |
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
crates/openwa-wormkit/src/replacements/sound.rs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
use std::sync::atomic::Ordering;
|
| 11 |
|
| 12 |
use openwa_core::address::va;
|
| 13 |
-
use openwa_core::audio::{play_sound, play_sound_pooled, SoundId};
|
| 14 |
use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
|
| 15 |
use openwa_core::fixed::Fixed;
|
| 16 |
use openwa_core::task::{CGameTask, CTask};
|
|
@@ -42,7 +42,7 @@ pub unsafe fn queue_sound(
|
|
| 42 |
}
|
| 43 |
let entry = &mut g.sound_queue[g.sound_queue_count as usize];
|
| 44 |
*entry = SoundQueueEntry {
|
| 45 |
-
sound_id: sound_id
|
| 46 |
flags,
|
| 47 |
volume: volume.0 as u32,
|
| 48 |
pitch: pitch.0 as u32,
|
|
@@ -70,7 +70,7 @@ unsafe extern "thiscall" fn hook_play_sound_global(
|
|
| 70 |
pitch: Fixed,
|
| 71 |
) -> u32 {
|
| 72 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 73 |
-
let sound_name =
|
| 74 |
.map(|s| format!("{s:?}"))
|
| 75 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 76 |
let _ = log_line(&format!(
|
|
@@ -79,11 +79,7 @@ unsafe extern "thiscall" fn hook_play_sound_global(
|
|
| 79 |
));
|
| 80 |
}
|
| 81 |
|
| 82 |
-
|
| 83 |
-
Ok(s) => s,
|
| 84 |
-
Err(_) => return 0,
|
| 85 |
-
};
|
| 86 |
-
queue_sound((*this).base.ddgame, sid, flags, volume, pitch).is_some() as u32
|
| 87 |
}
|
| 88 |
|
| 89 |
// ============================================================
|
|
@@ -103,7 +99,7 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 103 |
flags: u32,
|
| 104 |
) -> u32 {
|
| 105 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 106 |
-
let sound_name =
|
| 107 |
.map(|s| format!("{s:?}"))
|
| 108 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 109 |
let _ = log_line(&format!(
|
|
@@ -112,13 +108,9 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 112 |
));
|
| 113 |
}
|
| 114 |
|
| 115 |
-
let sid = match SoundId::try_from(sound_id) {
|
| 116 |
-
Ok(s) => s,
|
| 117 |
-
Err(_) => return 0,
|
| 118 |
-
};
|
| 119 |
let gt = &*task;
|
| 120 |
let ddgame = gt.base.ddgame;
|
| 121 |
-
let entry = match queue_sound(ddgame,
|
| 122 |
Some(e) => e,
|
| 123 |
None => return 0,
|
| 124 |
};
|
|
@@ -145,14 +137,14 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 145 |
/// increments the task's local sound count. Returns true on success.
|
| 146 |
pub(crate) unsafe fn play_sound_local(
|
| 147 |
task: *mut CGameTask,
|
| 148 |
-
sound_id: SoundId,
|
| 149 |
flags: u32,
|
| 150 |
volume: Fixed,
|
| 151 |
pitch: Fixed,
|
| 152 |
) -> bool {
|
| 153 |
let gt = &*task;
|
| 154 |
let ddgame = gt.base.ddgame;
|
| 155 |
-
let entry = match queue_sound(ddgame, sound_id, flags, volume, pitch) {
|
| 156 |
Some(e) => e,
|
| 157 |
None => return false,
|
| 158 |
};
|
|
|
|
| 10 |
use std::sync::atomic::Ordering;
|
| 11 |
|
| 12 |
use openwa_core::address::va;
|
| 13 |
+
use openwa_core::audio::{play_sound, play_sound_pooled, KnownSoundId, SoundId};
|
| 14 |
use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
|
| 15 |
use openwa_core::fixed::Fixed;
|
| 16 |
use openwa_core::task::{CGameTask, CTask};
|
|
|
|
| 42 |
}
|
| 43 |
let entry = &mut g.sound_queue[g.sound_queue_count as usize];
|
| 44 |
*entry = SoundQueueEntry {
|
| 45 |
+
sound_id: sound_id.0,
|
| 46 |
flags,
|
| 47 |
volume: volume.0 as u32,
|
| 48 |
pitch: pitch.0 as u32,
|
|
|
|
| 70 |
pitch: Fixed,
|
| 71 |
) -> u32 {
|
| 72 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 73 |
+
let sound_name = KnownSoundId::try_from(sound_id)
|
| 74 |
.map(|s| format!("{s:?}"))
|
| 75 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 76 |
let _ = log_line(&format!(
|
|
|
|
| 79 |
));
|
| 80 |
}
|
| 81 |
|
| 82 |
+
queue_sound((*this).base.ddgame, SoundId(sound_id), flags, volume, pitch).is_some() as u32
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
// ============================================================
|
|
|
|
| 99 |
flags: u32,
|
| 100 |
) -> u32 {
|
| 101 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 102 |
+
let sound_name = KnownSoundId::try_from(sound_id)
|
| 103 |
.map(|s| format!("{s:?}"))
|
| 104 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 105 |
let _ = log_line(&format!(
|
|
|
|
| 108 |
));
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
let gt = &*task;
|
| 112 |
let ddgame = gt.base.ddgame;
|
| 113 |
+
let entry = match queue_sound(ddgame, SoundId(sound_id), flags, volume, pitch) {
|
| 114 |
Some(e) => e,
|
| 115 |
None => return 0,
|
| 116 |
};
|
|
|
|
| 137 |
/// increments the task's local sound count. Returns true on success.
|
| 138 |
pub(crate) unsafe fn play_sound_local(
|
| 139 |
task: *mut CGameTask,
|
| 140 |
+
sound_id: impl Into<SoundId>,
|
| 141 |
flags: u32,
|
| 142 |
volume: Fixed,
|
| 143 |
pitch: Fixed,
|
| 144 |
) -> bool {
|
| 145 |
let gt = &*task;
|
| 146 |
let ddgame = gt.base.ddgame;
|
| 147 |
+
let entry = match queue_sound(ddgame, sound_id.into(), flags, volume, pitch) {
|
| 148 |
Some(e) => e,
|
| 149 |
None => return false,
|
| 150 |
};
|
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
| 16 |
use core::sync::atomic::{AtomicU32, Ordering};
|
| 17 |
|
| 18 |
use openwa_core::address::va;
|
| 19 |
-
use openwa_core::audio::
|
| 20 |
use openwa_core::engine::ddgame::{self, TeamArenaRef};
|
| 21 |
use openwa_core::fixed::Fixed;
|
| 22 |
use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
|
|
@@ -614,7 +614,7 @@ unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
|
|
| 614 |
}
|
| 615 |
|
| 616 |
// PlaySoundGlobal(IndianAnthem, 8, 0x10000, 0x10000)
|
| 617 |
-
sound::queue_sound(ddgame,
|
| 618 |
|
| 619 |
// Message 0x51 (PoisonWorm): buf[0]=shot_count, buf[4]=2, buf[8]=team_index
|
| 620 |
let team = lookup_team_task(worm);
|
|
@@ -627,7 +627,7 @@ unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
|
|
| 627 |
}
|
| 628 |
|
| 629 |
// PlaySoundGlobal(NukeFlash, 5, 0x10000, 0x10000)
|
| 630 |
-
sound::queue_sound(ddgame,
|
| 631 |
}
|
| 632 |
|
| 633 |
// ββ Air Strike sub-function bridges βββββββββββββββββββββββ
|
|
|
|
| 16 |
use core::sync::atomic::{AtomicU32, Ordering};
|
| 17 |
|
| 18 |
use openwa_core::address::va;
|
| 19 |
+
use openwa_core::audio::KnownSoundId;
|
| 20 |
use openwa_core::engine::ddgame::{self, TeamArenaRef};
|
| 21 |
use openwa_core::fixed::Fixed;
|
| 22 |
use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
|
|
|
|
| 614 |
}
|
| 615 |
|
| 616 |
// PlaySoundGlobal(IndianAnthem, 8, 0x10000, 0x10000)
|
| 617 |
+
sound::queue_sound(ddgame, KnownSoundId::IndianAnthem.into(), 8, Fixed::ONE, Fixed::ONE);
|
| 618 |
|
| 619 |
// Message 0x51 (PoisonWorm): buf[0]=shot_count, buf[4]=2, buf[8]=team_index
|
| 620 |
let team = lookup_team_task(worm);
|
|
|
|
| 627 |
}
|
| 628 |
|
| 629 |
// PlaySoundGlobal(NukeFlash, 5, 0x10000, 0x10000)
|
| 630 |
+
sound::queue_sound(ddgame, KnownSoundId::NukeFlash.into(), 5, Fixed::ONE, Fixed::ONE);
|
| 631 |
}
|
| 632 |
|
| 633 |
// ββ Air Strike sub-function bridges βββββββββββββββββββββββ
|
crates/openwa-wormkit/src/replacements/weapon_release.rs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
//! Convention: usercall(EAX=CTaskWorm*) + 4 stack params, RET 0x10.
|
| 7 |
|
| 8 |
use openwa_core::address::va;
|
| 9 |
-
use openwa_core::audio::SoundId;
|
| 10 |
use openwa_core::fixed::Fixed;
|
| 11 |
use openwa_core::game::Weapon;
|
| 12 |
use openwa_core::log::log_line;
|
|
@@ -304,11 +304,11 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 304 |
effect_state = 0x73;
|
| 305 |
}
|
| 306 |
2 => {
|
| 307 |
-
sound::play_sound_local(task,
|
| 308 |
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 309 |
}
|
| 310 |
3 | 7 | 0xB | 0xC => {
|
| 311 |
-
sound::play_sound_local(task,
|
| 312 |
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 313 |
}
|
| 314 |
4 => {
|
|
@@ -319,17 +319,17 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 319 |
effect_state = 0x73;
|
| 320 |
}
|
| 321 |
5 => {
|
| 322 |
-
sound::play_sound_local(task,
|
| 323 |
do_effect = true;
|
| 324 |
effect_state = 0x75;
|
| 325 |
}
|
| 326 |
6 => {
|
| 327 |
-
sound::play_sound_local(task,
|
| 328 |
do_effect = true;
|
| 329 |
effect_state = 0x73;
|
| 330 |
}
|
| 331 |
10 => {
|
| 332 |
-
sound::play_sound_local(task,
|
| 333 |
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 334 |
}
|
| 335 |
_ => {}
|
|
@@ -338,9 +338,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 338 |
Ok(FireType::Rope) => {
|
| 339 |
if w._unknown_2cc == 0 || w._unknown_2c8 == 1 {
|
| 340 |
let team_sound_raw = (*w.ddgame()).team_sound_id(team_id);
|
| 341 |
-
|
| 342 |
-
sound::play_sound_local(task, sid, 3, Fixed::ONE, Fixed::ONE);
|
| 343 |
-
}
|
| 344 |
}
|
| 345 |
}
|
| 346 |
// Type 3 (Strike): no sound
|
|
@@ -349,23 +347,19 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 349 |
match S::try_from((*entry).special_subtype) {
|
| 350 |
Ok(S::PneumaticDrill) => {
|
| 351 |
sound::play_sound_local(
|
| 352 |
-
task,
|
| 353 |
);
|
| 354 |
}
|
| 355 |
Ok(S::Girder) => {
|
| 356 |
-
|
| 357 |
-
sound::play_sound_local(task, sid, 3, Fixed::ONE, Fixed::ONE);
|
| 358 |
-
}
|
| 359 |
}
|
| 360 |
Ok(S::BaseballBat) => {
|
| 361 |
// Sound ID from fire_params.spread (polymorphic use of field)
|
| 362 |
-
|
| 363 |
-
sound::play_sound_local(task, sid, 3, Fixed::ONE, Fixed::ONE);
|
| 364 |
-
}
|
| 365 |
}
|
| 366 |
Ok(S::AirStrike) => {
|
| 367 |
if w._unknown_208 == 0 {
|
| 368 |
-
sound::play_sound_local(task,
|
| 369 |
}
|
| 370 |
}
|
| 371 |
Ok(S::ScalesOfJustice) => {
|
|
|
|
| 6 |
//! Convention: usercall(EAX=CTaskWorm*) + 4 stack params, RET 0x10.
|
| 7 |
|
| 8 |
use openwa_core::address::va;
|
| 9 |
+
use openwa_core::audio::{KnownSoundId, SoundId};
|
| 10 |
use openwa_core::fixed::Fixed;
|
| 11 |
use openwa_core::game::Weapon;
|
| 12 |
use openwa_core::log::log_line;
|
|
|
|
| 304 |
effect_state = 0x73;
|
| 305 |
}
|
| 306 |
2 => {
|
| 307 |
+
sound::play_sound_local(task, KnownSoundId::ThrowRelease, 3, Fixed::ONE, Fixed::ONE);
|
| 308 |
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 309 |
}
|
| 310 |
3 | 7 | 0xB | 0xC => {
|
| 311 |
+
sound::play_sound_local(task, KnownSoundId::RocketRelease, 3, Fixed::ONE, Fixed::ONE);
|
| 312 |
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 313 |
}
|
| 314 |
4 => {
|
|
|
|
| 319 |
effect_state = 0x73;
|
| 320 |
}
|
| 321 |
5 => {
|
| 322 |
+
sound::play_sound_local(task, KnownSoundId::ShotgunFire, 3, Fixed::ONE, Fixed::ONE);
|
| 323 |
do_effect = true;
|
| 324 |
effect_state = 0x75;
|
| 325 |
}
|
| 326 |
6 => {
|
| 327 |
+
sound::play_sound_local(task, KnownSoundId::HandgunFire, 3, Fixed::ONE, Fixed::ONE);
|
| 328 |
do_effect = true;
|
| 329 |
effect_state = 0x73;
|
| 330 |
}
|
| 331 |
10 => {
|
| 332 |
+
sound::play_sound_local(task, KnownSoundId::LongbowRelease, 3, Fixed::ONE, Fixed::ONE);
|
| 333 |
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 334 |
}
|
| 335 |
_ => {}
|
|
|
|
| 338 |
Ok(FireType::Rope) => {
|
| 339 |
if w._unknown_2cc == 0 || w._unknown_2c8 == 1 {
|
| 340 |
let team_sound_raw = (*w.ddgame()).team_sound_id(team_id);
|
| 341 |
+
sound::play_sound_local(task, SoundId(team_sound_raw), 3, Fixed::ONE, Fixed::ONE);
|
|
|
|
|
|
|
| 342 |
}
|
| 343 |
}
|
| 344 |
// Type 3 (Strike): no sound
|
|
|
|
| 347 |
match S::try_from((*entry).special_subtype) {
|
| 348 |
Ok(S::PneumaticDrill) => {
|
| 349 |
sound::play_sound_local(
|
| 350 |
+
task, KnownSoundId::BaseballBatRelease, 3, Fixed::ONE, Fixed::ONE,
|
| 351 |
);
|
| 352 |
}
|
| 353 |
Ok(S::Girder) => {
|
| 354 |
+
sound::play_sound_local(task, SoundId((*entry).fire_method as u32), 3, Fixed::ONE, Fixed::ONE);
|
|
|
|
|
|
|
| 355 |
}
|
| 356 |
Ok(S::BaseballBat) => {
|
| 357 |
// Sound ID from fire_params.spread (polymorphic use of field)
|
| 358 |
+
sound::play_sound_local(task, SoundId((*entry).fire_params.spread as u32), 3, Fixed::ONE, Fixed::ONE);
|
|
|
|
|
|
|
| 359 |
}
|
| 360 |
Ok(S::AirStrike) => {
|
| 361 |
if w._unknown_208 == 0 {
|
| 362 |
+
sound::play_sound_local(task, KnownSoundId::Teleport, 3, Fixed::ONE, Fixed::ONE);
|
| 363 |
}
|
| 364 |
}
|
| 365 |
Ok(S::ScalesOfJustice) => {
|