feat: port worm sound functions to Rust
Browse filesPort 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>
|
@@ -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
|
|
@@ -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 |
+
}
|
|
@@ -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::
|
|
|
|
| 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(())
|
|
@@ -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 |
-
|
| 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 |
-
|
| 309 |
}
|
| 310 |
3 | 7 | 0xB | 0xC => {
|
| 311 |
sound::play_sound_local(task, KnownSoundId::RocketRelease, 3, Fixed::ONE, Fixed::ONE);
|
| 312 |
-
|
| 313 |
}
|
| 314 |
4 => {
|
| 315 |
if w.sound_handle == 0 {
|
| 316 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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)]
|