paavohtl Claude Opus 4.6 (1M context) commited on
Commit
62e3695
Β·
1 Parent(s): 9d2738d

fix: split SoundId into newtype + KnownSoundId enum

Browse files

SoundId 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
- /// Sound effect IDs. Range 1-126, contiguous.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  ///
3
  /// Source: wkJellyWorm Constants.h
4
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5
  #[repr(u32)]
6
- pub enum SoundId {
7
  Morse = 1,
8
  CrowdPart1 = 2,
9
  CrowdPart2 = 3,
@@ -132,12 +146,12 @@ pub enum SoundId {
132
  RocketPowerdown = 126,
133
  }
134
 
135
- impl SoundId {
136
  pub const MIN: u32 = 1;
137
  pub const MAX: u32 = 126;
138
  }
139
 
140
- impl TryFrom<u32> for SoundId {
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 as u32,
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 = SoundId::try_from(sound_id)
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
- let sid = match SoundId::try_from(sound_id) {
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 = SoundId::try_from(sound_id)
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, sid, flags, volume, pitch) {
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::SoundId;
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, SoundId::IndianAnthem, 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,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, SoundId::NukeFlash, 5, Fixed::ONE, Fixed::ONE);
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, SoundId::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, SoundId::RocketRelease, 3, Fixed::ONE, Fixed::ONE);
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, SoundId::ShotgunFire, 3, Fixed::ONE, Fixed::ONE);
323
  do_effect = true;
324
  effect_state = 0x75;
325
  }
326
  6 => {
327
- sound::play_sound_local(task, SoundId::HandgunFire, 3, Fixed::ONE, Fixed::ONE);
328
  do_effect = true;
329
  effect_state = 0x73;
330
  }
331
  10 => {
332
- sound::play_sound_local(task, SoundId::LongbowRelease, 3, Fixed::ONE, Fixed::ONE);
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
- if let Ok(sid) = SoundId::try_from(team_sound_raw) {
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, SoundId::BaseballBatRelease, 3, Fixed::ONE, Fixed::ONE,
353
  );
354
  }
355
  Ok(S::Girder) => {
356
- if let Ok(sid) = SoundId::try_from((*entry).fire_method as u32) {
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
- if let Ok(sid) = SoundId::try_from((*entry).fire_params.spread as u32) {
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, SoundId::Teleport, 3, Fixed::ONE, Fixed::ONE);
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) => {