paavohtl Claude Opus 4.6 (1M context) commited on
Commit
d575149
Β·
1 Parent(s): e209bb6

feat: port worm sound functions to Rust

Browse files

Port PlayWormSound (0x5150D0) and StopWormSound (0x515180) from naked
asm bridges to typed Rust functions in the sound module:

- ActiveSoundTable::stop_sound() β€” ports FUN_00546490, stops a streaming
sound by handle (slot lookup + DSSound::stop_channel + emitter release)
- sound::stop_worm_sound() β€” reads handle from CTaskWorm.sound_handle,
dispatches to DSSound::stop_channel or ActiveSoundTable::stop_sound
based on the 0x40000000 streaming flag bit, then clears the handle
- sound::play_worm_sound() β€” stops current sound, then starts new via
bridge to FUN_00546c20 (streaming load-and-play), stores new handle

Removes call_play_worm_sound and call_stop_worm_sound naked asm bridges
from weapon_release.rs. One bridge remains for FUN_00546c20 (streaming
audio start) in sound.rs using addr-as-param pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/address.rs CHANGED
@@ -510,6 +510,9 @@ pub mod va {
510
  fn/Fastcall DISPATCH_GLOBAL_SOUND = 0x0052_6270;
511
  /// RecordActiveSound
512
  fn/Usercall RECORD_ACTIVE_SOUND = 0x0054_6260;
 
 
 
513
  /// ComputeDistanceParams
514
  fn/Fastcall COMPUTE_DISTANCE_PARAMS = 0x0054_6300;
515
  /// DispatchLocalSound
 
510
  fn/Fastcall DISPATCH_GLOBAL_SOUND = 0x0052_6270;
511
  /// RecordActiveSound
512
  fn/Usercall RECORD_ACTIVE_SOUND = 0x0054_6260;
513
+ /// LoadAndPlayStreaming: usercall(EAX=worm, ESI=&sound_emitter) + stack(sound_id, 3, volume).
514
+ /// Checks game conditions, then starts a streaming sound. Returns handle | 0x40000000.
515
+ fn/Usercall LOAD_AND_PLAY_STREAMING = 0x0054_6C20;
516
  /// ComputeDistanceParams
517
  fn/Fastcall COMPUTE_DISTANCE_PARAMS = 0x0054_6300;
518
  /// DispatchLocalSound
crates/openwa-core/src/audio/active_sound.rs CHANGED
@@ -46,3 +46,38 @@ pub struct ActiveSoundEntry {
46
  }
47
 
48
  const _: () = assert!(core::mem::size_of::<ActiveSoundEntry>() == 0x18);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
 
48
  const _: () = assert!(core::mem::size_of::<ActiveSoundEntry>() == 0x18);
49
+
50
+ impl ActiveSoundTable {
51
+ /// Stop an active streaming sound by handle. Port of FUN_00546490.
52
+ ///
53
+ /// The `handle` should have the 0x40000000 bit already cleared (the caller
54
+ /// strips it before calling). The low 6 bits index into the entry table.
55
+ ///
56
+ /// Returns true if the sound was found and stopped.
57
+ pub unsafe fn stop_sound(&mut self, handle: u32) -> bool {
58
+ let slot = (handle & 0x3F) as usize;
59
+ let entry = &mut self.entries[slot];
60
+
61
+ if entry.sequence != handle || entry.channel_handle == 0 {
62
+ return false;
63
+ }
64
+
65
+ // Stop the DSSound channel via the DDGame's sound system
66
+ let ddgame = &*self.ddgame;
67
+ let sound = ddgame.sound;
68
+ if !sound.is_null() {
69
+ ((*(*sound).vtable).stop_channel)(sound, entry.channel_handle as i32);
70
+ }
71
+ entry.channel_handle = 0;
72
+
73
+ // Release the emitter reference
74
+ if !entry.emitter.is_null() {
75
+ // Decrement ref count at emitter+8
76
+ let ref_count = entry.emitter.add(8) as *mut i32;
77
+ *ref_count -= 1;
78
+ entry.emitter = core::ptr::null_mut();
79
+ }
80
+
81
+ true
82
+ }
83
+ }
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -13,7 +13,8 @@ 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};
 
17
 
18
  use crate::hook;
19
  use crate::log_line;
@@ -159,6 +160,102 @@ pub(crate) unsafe fn play_sound_local(
159
  true
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  // ============================================================
163
  // Sound dispatch helpers (bridge: queue β†’ DSSound)
164
  // ============================================================
@@ -284,6 +381,12 @@ pub fn install() -> Result<(), String> {
284
 
285
  // Patch DSSound vtable: replace trivial slots with Rust implementations.
286
  patch_dssound_vtable()?;
 
 
 
 
 
 
287
  }
288
 
289
  Ok(())
 
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::worm::CTaskWorm;
17
+ use openwa_core::task::{CGameTask, CTask, Task};
18
 
19
  use crate::hook;
20
  use crate::log_line;
 
160
  true
161
  }
162
 
163
+ // ============================================================
164
+ // Worm sound functions (streaming sound handle at CTaskWorm+0x3B0)
165
+ // ============================================================
166
+
167
+ /// Stop the worm's active streaming sound. Port of FUN_00515180.
168
+ ///
169
+ /// Reads the sound handle from `worm.sound_handle`, dispatches to either
170
+ /// DSSound::stop_channel (regular) or ActiveSoundTable::stop_sound (streaming,
171
+ /// handle has bit 30 set), then clears the handle.
172
+ pub(crate) unsafe fn stop_worm_sound(worm: *mut CTaskWorm) {
173
+ let handle = (*worm).sound_handle;
174
+ if handle != 0 {
175
+ let ddgame = &*(*worm).ddgame();
176
+ let sound = ddgame.sound;
177
+ if !sound.is_null() && (handle as i32) >= 0 {
178
+ if handle & 0x40000000 != 0 {
179
+ // Streaming sound β€” stop via ActiveSoundTable
180
+ if !ddgame.active_sounds.is_null() {
181
+ (*ddgame.active_sounds).stop_sound(handle & !0x40000000);
182
+ }
183
+ } else {
184
+ // Regular DSSound channel
185
+ ((*(*sound).vtable).stop_channel)(sound, handle as i32);
186
+ }
187
+ }
188
+ }
189
+ (*worm).sound_handle = 0;
190
+ }
191
+
192
+ /// Stop current worm sound, then start a new streaming sound.
193
+ /// Port of FUN_005150D0.
194
+ ///
195
+ /// Stops the current sound (same logic as [`stop_worm_sound`] but with
196
+ /// reversed condition order matching the original), then calls the WA
197
+ /// streaming load-and-play function (FUN_00546c20) and stores the new handle.
198
+ pub(crate) unsafe fn play_worm_sound(worm: *mut CTaskWorm, sound_id: SoundId, volume: Fixed) {
199
+ let handle = (*worm).sound_handle;
200
+ if handle != 0 {
201
+ let ddgame = &*(*worm).ddgame();
202
+ let sound = ddgame.sound;
203
+ if !sound.is_null() && (handle as i32) >= 0 {
204
+ if handle & 0x40000000 == 0 {
205
+ // Regular DSSound channel β€” stop via vtable
206
+ ((*(*sound).vtable).stop_channel)(sound, handle as i32);
207
+ } else {
208
+ // Streaming sound β€” stop via ActiveSoundTable
209
+ if !ddgame.active_sounds.is_null() {
210
+ (*ddgame.active_sounds).stop_sound(handle & !0x40000000);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ // Start new streaming sound via WA function (FUN_00546c20)
216
+ let new_handle = call_load_and_play_streaming(worm, sound_id.0, volume.0 as u32);
217
+ (*worm).sound_handle = new_handle;
218
+ }
219
+
220
+ /// Bridge to FUN_00546c20 β€” load and play a streaming sound.
221
+ /// usercall(EAX=worm, ESI=&worm.sound_emitter) + stack(sound_id, 3, volume), plain RET.
222
+ /// Returns new sound handle (with bit 30 set for streaming, 0 on failure, -1 if suppressed).
223
+ unsafe fn call_load_and_play_streaming(
224
+ worm: *mut CTaskWorm,
225
+ sound_id: u32,
226
+ volume: u32,
227
+ ) -> u32 {
228
+ let addr = LOAD_AND_PLAY_STREAMING_ADDR.load(core::sync::atomic::Ordering::Relaxed);
229
+ call_load_and_play_streaming_bridge(worm, sound_id, volume, addr)
230
+ }
231
+
232
+ static LOAD_AND_PLAY_STREAMING_ADDR: core::sync::atomic::AtomicU32 =
233
+ core::sync::atomic::AtomicU32::new(0);
234
+
235
+ #[unsafe(naked)]
236
+ unsafe extern "C" fn call_load_and_play_streaming_bridge(
237
+ _worm: *mut CTaskWorm,
238
+ _sound_id: u32,
239
+ _volume: u32,
240
+ _addr: u32,
241
+ ) -> u32 {
242
+ core::arch::naked_asm!(
243
+ "push esi",
244
+ "push ebx",
245
+ // Stack: 2 saves(8) + ret(4) = 12 to first arg
246
+ "mov eax, [esp+12]", // worm β†’ EAX (must be EAX when target is called)
247
+ "lea esi, [eax+0xE8]", // &worm.sound_emitter β†’ ESI
248
+ "mov ebx, [esp+24]", // addr (12 + 3*4 = 24) β†’ EBX (temp)
249
+ "push [esp+20]", // volume (12+8=20)
250
+ "push 3", // flags = 3
251
+ "push [esp+24]", // sound_id (12+4+8=24, shifted by 2 pushes)
252
+ "call ebx", // call target; EAX = worm is preserved
253
+ "pop ebx",
254
+ "pop esi",
255
+ "ret",
256
+ );
257
+ }
258
+
259
  // ============================================================
260
  // Sound dispatch helpers (bridge: queue β†’ DSSound)
261
  // ============================================================
 
381
 
382
  // Patch DSSound vtable: replace trivial slots with Rust implementations.
383
  patch_dssound_vtable()?;
384
+
385
+ // Initialize bridge addresses for worm sound functions
386
+ LOAD_AND_PLAY_STREAMING_ADDR.store(
387
+ openwa_core::rebase::rb(va::LOAD_AND_PLAY_STREAMING),
388
+ core::sync::atomic::Ordering::Relaxed,
389
+ );
390
  }
391
 
392
  Ok(())
crates/openwa-wormkit/src/replacements/weapon_release.rs CHANGED
@@ -290,30 +290,27 @@ unsafe extern "cdecl" fn weapon_release_impl(
290
  let w = &*worm; // re-borrow after mutation above
291
  let entry = w.active_weapon_entry;
292
 
293
- let play_worm_sound_addr = rb(va::PLAY_WORM_SOUND);
294
- let stop_worm_sound_addr = rb(va::STOP_WORM_SOUND);
295
-
296
  match FireType::try_from((*entry).fire_type) {
297
  Ok(FireType::Projectile) => {
298
  match (*entry).special_subtype {
299
  1 => {
300
  if w.sound_handle == 0 {
301
- call_play_worm_sound(worm, 0x1004E, 0x10000, play_worm_sound_addr);
302
  }
303
  do_effect = true;
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 => {
315
  if w.sound_handle == 0 {
316
- call_play_worm_sound(worm, 0x1004F, 0x10000, play_worm_sound_addr);
317
  }
318
  do_effect = true;
319
  effect_state = 0x73;
@@ -330,7 +327,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
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
  _ => {}
336
  }
@@ -364,7 +361,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
364
  }
365
  Ok(S::ScalesOfJustice) => {
366
  if w.sound_handle == 0 {
367
- call_play_worm_sound(worm, 0x10035, 0x10000, play_worm_sound_addr);
368
  }
369
  }
370
  _ => {}
@@ -415,43 +412,6 @@ fn write_u32(buf: &mut [u8], offset: usize, value: u32) {
415
  }
416
 
417
  // ── Bridge functions ────────────────────────────────────────
418
- // All bridges pass the runtime target address as the last cdecl parameter,
419
- // matching the pattern used in weapon.rs (avoids sym + jmp indirection issues).
420
-
421
- /// PlayWormSound (0x5150D0): usercall(EDI=worm) + stack(sound_handle_id, volume), RET 0x8.
422
- #[unsafe(naked)]
423
- unsafe extern "C" fn call_play_worm_sound(
424
- _worm: *mut CTaskWorm,
425
- _sound_id: u32,
426
- _volume: u32,
427
- _addr: u32,
428
- ) {
429
- core::arch::naked_asm!(
430
- "push edi",
431
- // Stack: 1 save(4) + ret(4) = 8 to first arg
432
- "mov edi, [esp+8]", // worm
433
- "mov eax, [esp+20]", // addr
434
- "push [esp+16]", // volume
435
- "push [esp+16]", // sound_id (shifted +4)
436
- "call eax",
437
- "pop edi",
438
- "ret",
439
- );
440
- }
441
-
442
- /// StopWormSound (0x515180): usercall(ESI=worm), plain RET.
443
- #[unsafe(naked)]
444
- unsafe extern "C" fn call_stop_worm_sound(_worm: *mut CTaskWorm, _addr: u32) {
445
- core::arch::naked_asm!(
446
- "push esi",
447
- // Stack: 1 save(4) + ret(4) = 8 to first arg
448
- "mov esi, [esp+8]", // worm
449
- "mov eax, [esp+12]", // addr
450
- "call eax",
451
- "pop esi",
452
- "ret",
453
- );
454
- }
455
 
456
  /// SpawnEffect (0x547C30): usercall(EAX=0x80000, ECX=speed_x, ESI=worm) + 7 stack, RET 0x1C.
457
  #[unsafe(naked)]
 
290
  let w = &*worm; // re-borrow after mutation above
291
  let entry = w.active_weapon_entry;
292
 
 
 
 
293
  match FireType::try_from((*entry).fire_type) {
294
  Ok(FireType::Projectile) => {
295
  match (*entry).special_subtype {
296
  1 => {
297
  if w.sound_handle == 0 {
298
+ sound::play_worm_sound(worm, SoundId(0x1004E), Fixed::ONE);
299
  }
300
  do_effect = true;
301
  effect_state = 0x73;
302
  }
303
  2 => {
304
  sound::play_sound_local(task, KnownSoundId::ThrowRelease, 3, Fixed::ONE, Fixed::ONE);
305
+ sound::stop_worm_sound(worm);
306
  }
307
  3 | 7 | 0xB | 0xC => {
308
  sound::play_sound_local(task, KnownSoundId::RocketRelease, 3, Fixed::ONE, Fixed::ONE);
309
+ sound::stop_worm_sound(worm);
310
  }
311
  4 => {
312
  if w.sound_handle == 0 {
313
+ sound::play_worm_sound(worm, SoundId(0x1004F), Fixed::ONE);
314
  }
315
  do_effect = true;
316
  effect_state = 0x73;
 
327
  }
328
  10 => {
329
  sound::play_sound_local(task, KnownSoundId::LongbowRelease, 3, Fixed::ONE, Fixed::ONE);
330
+ sound::stop_worm_sound(worm);
331
  }
332
  _ => {}
333
  }
 
361
  }
362
  Ok(S::ScalesOfJustice) => {
363
  if w.sound_handle == 0 {
364
+ sound::play_worm_sound(worm, SoundId(0x10035), Fixed::ONE);
365
  }
366
  }
367
  _ => {}
 
412
  }
413
 
414
  // ── Bridge functions ────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
  /// SpawnEffect (0x547C30): usercall(EAX=0x80000, ECX=speed_x, ESI=worm) + 7 stack, RET 0x1C.
417
  #[unsafe(naked)]