feat: port streaming sound chain to Rust, unify SoundId/SoundSlot
Browse filesFully port play_worm_sound/play_worm_sound_2 β the streaming sound
playback chain no longer calls into WA.exe via naked asm bridges.
New Rust implementations (sound_ops.rs):
- distance_3d_attenuation: stereo panning with i64 fixed-point math
- compute_distance_params: reads listener pos + attenuation from DDGame
- record_active_sound: ring buffer insert into ActiveSoundTable
- dispatch_local_sound: 3D audio + play_sound_pooled + record
- load_and_play_streaming / load_and_play_streaming_positional
Removed: naked asm bridges (call_load_and_play_streaming,
call_load_and_play_streaming_positional, LOAD_AND_PLAY_STREAMING_ADDR).
Unify SoundId and SoundSlot into a single SoundId type β they were
the same packed u32 format (low 16 = slot index, bit 16 = loop,
bit 17 = raw volume). Flag accessors moved to SoundId.
Other changes:
- Add Fixed Div/Div<i32> operators
- Name DDGame+0x77B8 (level_width_sound), GameInfo+0xF38C
(sound_attenuation), DDGame listener_pos() accessor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/audio/active_sound.rs +2 -2
- crates/openwa-core/src/audio/dssound.rs +8 -42
- crates/openwa-core/src/audio/mod.rs +1 -1
- crates/openwa-core/src/audio/sound.rs +35 -3
- crates/openwa-core/src/audio/sound_ops.rs +307 -104
- crates/openwa-core/src/engine/ddgame.rs +15 -2
- crates/openwa-core/src/engine/game_info.rs +7 -2
- crates/openwa-core/src/fixed.rs +19 -1
- crates/openwa-core/src/task/worm.rs +2 -2
- crates/openwa-wormkit/src/replacements/sound.rs +21 -28
|
@@ -40,7 +40,7 @@ pub struct ActiveSoundEntry {
|
|
| 40 |
/// 0x0C: Volume (fixed-point 16.16, 0x10000 = 1.0).
|
| 41 |
pub volume: Fixed,
|
| 42 |
/// 0x10: Running counter value at time of insertion (unique ID).
|
| 43 |
-
pub sequence:
|
| 44 |
/// 0x14: DSSound channel handle (return from play_sound_pooled). 0 = free slot.
|
| 45 |
pub channel_handle: u32,
|
| 46 |
}
|
|
@@ -54,7 +54,7 @@ impl ActiveSoundTable {
|
|
| 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:
|
| 58 |
let slot = (handle & 0x3F) as usize;
|
| 59 |
let entry = &mut self.entries[slot];
|
| 60 |
|
|
|
|
| 40 |
/// 0x0C: Volume (fixed-point 16.16, 0x10000 = 1.0).
|
| 41 |
pub volume: Fixed,
|
| 42 |
/// 0x10: Running counter value at time of insertion (unique ID).
|
| 43 |
+
pub sequence: i32,
|
| 44 |
/// 0x14: DSSound channel handle (return from play_sound_pooled). 0 = free slot.
|
| 45 |
pub channel_handle: u32,
|
| 46 |
}
|
|
|
|
| 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: i32) -> bool {
|
| 58 |
let slot = (handle & 0x3F) as usize;
|
| 59 |
let entry = &mut self.entries[slot];
|
| 60 |
|
|
@@ -2,41 +2,7 @@ use crate::fixed::Fixed;
|
|
| 2 |
|
| 3 |
use windows::Win32::Media::Audio::DirectSound::IDirectSoundBuffer;
|
| 4 |
|
| 5 |
-
|
| 6 |
-
///
|
| 7 |
-
/// Layout:
|
| 8 |
-
/// - Bits 0-15: sound slot index (1-499)
|
| 9 |
-
/// - Bit 16: loop flag (play in a loop)
|
| 10 |
-
/// - Bit 17: raw volume flag (skip master volume scaling)
|
| 11 |
-
#[derive(Clone, Copy)]
|
| 12 |
-
#[repr(transparent)]
|
| 13 |
-
pub struct SoundSlot(pub u32);
|
| 14 |
-
|
| 15 |
-
impl SoundSlot {
|
| 16 |
-
/// Sound slot index (0-499). 0 = invalid/empty.
|
| 17 |
-
#[inline]
|
| 18 |
-
pub fn index(self) -> usize {
|
| 19 |
-
(self.0 & 0xFFFF) as usize
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
/// Whether the sound should loop continuously.
|
| 23 |
-
#[inline]
|
| 24 |
-
pub fn is_looping(self) -> bool {
|
| 25 |
-
self.0 & 0x10000 != 0
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/// Whether volume should be used directly (skip master volume scaling).
|
| 29 |
-
#[inline]
|
| 30 |
-
pub fn is_raw_volume(self) -> bool {
|
| 31 |
-
self.0 & 0x20000 != 0
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
/// Return a copy with the loop flag cleared.
|
| 35 |
-
#[inline]
|
| 36 |
-
pub fn without_loop(self) -> Self {
|
| 37 |
-
Self(self.0 & 0xFFFE_FFFF)
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
|
| 41 |
/// Volume-to-dB attenuation table (64 entries of i16).
|
| 42 |
/// Copied from WA.exe .rdata at 0x6A6A60.
|
|
@@ -69,10 +35,10 @@ type Ptr32 = u32;
|
|
| 69 |
#[repr(C)]
|
| 70 |
#[derive(Clone, Copy)]
|
| 71 |
pub struct ChannelDescriptor {
|
| 72 |
-
/// +0x00: The
|
| 73 |
/// Stored by core_play_sound; checked by set_master_volume/set_channel_volume
|
| 74 |
/// for the raw-volume flag (bit 17).
|
| 75 |
-
pub slot:
|
| 76 |
/// +0x04: Buffer pool index (0..63), or -1 if not pooled.
|
| 77 |
/// Set by play_sound_pooled; checked by update_channels/stop_channel/allocate_channel.
|
| 78 |
pub pool_idx: i32,
|
|
@@ -128,7 +94,7 @@ pub struct DSSoundVtable {
|
|
| 128 |
/// play_sound β wrapper around core play. RET 0x14 = 5 stack params.
|
| 129 |
pub play_sound: fn(
|
| 130 |
this: *mut DSSound,
|
| 131 |
-
slot:
|
| 132 |
priority: i32,
|
| 133 |
volume: Fixed,
|
| 134 |
pan: Fixed,
|
|
@@ -137,7 +103,7 @@ pub struct DSSoundVtable {
|
|
| 137 |
/// play_sound_pooled β allocates from buffer pool, plays. RET 0x14.
|
| 138 |
pub play_sound_pooled: fn(
|
| 139 |
this: *mut DSSound,
|
| 140 |
-
slot:
|
| 141 |
priority: i32,
|
| 142 |
volume: Fixed,
|
| 143 |
pan: Fixed,
|
|
@@ -508,7 +474,7 @@ pub unsafe extern "thiscall" fn destructor(this: *mut DSSound, flags: u8) -> *mu
|
|
| 508 |
/// Returns channel descriptor index on success, -1 on failure.
|
| 509 |
unsafe fn core_play_sound(
|
| 510 |
snd: &mut DSSound,
|
| 511 |
-
slot:
|
| 512 |
priority: i32,
|
| 513 |
frequency: Fixed,
|
| 514 |
volume: Fixed,
|
|
@@ -652,7 +618,7 @@ unsafe fn allocate_channel(snd: &mut DSSound, priority: i32) -> i32 {
|
|
| 652 |
/// Params: slot (with flags), priority, frequency, volume, pan.
|
| 653 |
pub unsafe extern "thiscall" fn play_sound(
|
| 654 |
this: *mut DSSound,
|
| 655 |
-
slot:
|
| 656 |
priority: i32,
|
| 657 |
frequency: Fixed,
|
| 658 |
volume: Fixed,
|
|
@@ -667,7 +633,7 @@ pub unsafe extern "thiscall" fn play_sound(
|
|
| 667 |
/// Returns pool_id (1-based) on success, 0 on failure.
|
| 668 |
pub unsafe extern "thiscall" fn play_sound_pooled(
|
| 669 |
this: *mut DSSound,
|
| 670 |
-
slot:
|
| 671 |
priority: i32,
|
| 672 |
frequency: Fixed,
|
| 673 |
volume: Fixed,
|
|
|
|
| 2 |
|
| 3 |
use windows::Win32::Media::Audio::DirectSound::IDirectSoundBuffer;
|
| 4 |
|
| 5 |
+
use crate::audio::sound::SoundId;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
/// Volume-to-dB attenuation table (64 entries of i16).
|
| 8 |
/// Copied from WA.exe .rdata at 0x6A6A60.
|
|
|
|
| 35 |
#[repr(C)]
|
| 36 |
#[derive(Clone, Copy)]
|
| 37 |
pub struct ChannelDescriptor {
|
| 38 |
+
/// +0x00: The SoundId that was used to start this channel.
|
| 39 |
/// Stored by core_play_sound; checked by set_master_volume/set_channel_volume
|
| 40 |
/// for the raw-volume flag (bit 17).
|
| 41 |
+
pub slot: SoundId,
|
| 42 |
/// +0x04: Buffer pool index (0..63), or -1 if not pooled.
|
| 43 |
/// Set by play_sound_pooled; checked by update_channels/stop_channel/allocate_channel.
|
| 44 |
pub pool_idx: i32,
|
|
|
|
| 94 |
/// play_sound β wrapper around core play. RET 0x14 = 5 stack params.
|
| 95 |
pub play_sound: fn(
|
| 96 |
this: *mut DSSound,
|
| 97 |
+
slot: SoundId,
|
| 98 |
priority: i32,
|
| 99 |
volume: Fixed,
|
| 100 |
pan: Fixed,
|
|
|
|
| 103 |
/// play_sound_pooled β allocates from buffer pool, plays. RET 0x14.
|
| 104 |
pub play_sound_pooled: fn(
|
| 105 |
this: *mut DSSound,
|
| 106 |
+
slot: SoundId,
|
| 107 |
priority: i32,
|
| 108 |
volume: Fixed,
|
| 109 |
pan: Fixed,
|
|
|
|
| 474 |
/// Returns channel descriptor index on success, -1 on failure.
|
| 475 |
unsafe fn core_play_sound(
|
| 476 |
snd: &mut DSSound,
|
| 477 |
+
slot: SoundId,
|
| 478 |
priority: i32,
|
| 479 |
frequency: Fixed,
|
| 480 |
volume: Fixed,
|
|
|
|
| 618 |
/// Params: slot (with flags), priority, frequency, volume, pan.
|
| 619 |
pub unsafe extern "thiscall" fn play_sound(
|
| 620 |
this: *mut DSSound,
|
| 621 |
+
slot: SoundId,
|
| 622 |
priority: i32,
|
| 623 |
frequency: Fixed,
|
| 624 |
volume: Fixed,
|
|
|
|
| 633 |
/// Returns pool_id (1-based) on success, 0 on failure.
|
| 634 |
pub unsafe extern "thiscall" fn play_sound_pooled(
|
| 635 |
this: *mut DSSound,
|
| 636 |
+
slot: SoundId,
|
| 637 |
priority: i32,
|
| 638 |
frequency: Fixed,
|
| 639 |
volume: Fixed,
|
|
@@ -14,7 +14,7 @@ pub use dssound::{
|
|
| 14 |
set_master_volume, set_pan, set_volume_params, stop_channel,
|
| 15 |
sub_destructor as dssound_sub_destructor, update_channels,
|
| 16 |
};
|
| 17 |
-
pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable
|
| 18 |
pub use music::Music;
|
| 19 |
pub use sound::{KnownSoundId, SoundId};
|
| 20 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
|
|
|
| 14 |
set_master_volume, set_pan, set_volume_params, stop_channel,
|
| 15 |
sub_destructor as dssound_sub_destructor, update_channels,
|
| 16 |
};
|
| 17 |
+
pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
|
| 18 |
pub use music::Music;
|
| 19 |
pub use sound::{KnownSoundId, SoundId};
|
| 20 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
|
@@ -1,10 +1,42 @@
|
|
| 1 |
-
///
|
| 2 |
-
///
|
| 3 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
| 1 |
+
/// Packed sound ID + playback flags, passed as a single u32.
|
| 2 |
+
///
|
| 3 |
+
/// Layout:
|
| 4 |
+
/// - Bits 0-15: sound slot index (1-499 for SFX, higher for speech)
|
| 5 |
+
/// - Bit 16: loop flag (play in a loop)
|
| 6 |
+
/// - Bit 17: raw volume flag (skip master volume scaling)
|
| 7 |
+
///
|
| 8 |
+
/// Used throughout the sound system: queue insertion, DSSound playback,
|
| 9 |
+
/// and streaming dispatch all operate on this same packed format.
|
| 10 |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 11 |
#[repr(transparent)]
|
| 12 |
pub struct SoundId(pub u32);
|
| 13 |
|
| 14 |
+
impl SoundId {
|
| 15 |
+
/// Sound slot index (low 16 bits). 0 = invalid/empty.
|
| 16 |
+
#[inline]
|
| 17 |
+
pub fn index(self) -> usize {
|
| 18 |
+
(self.0 & 0xFFFF) as usize
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/// Whether the sound should loop continuously (bit 16).
|
| 22 |
+
#[inline]
|
| 23 |
+
pub fn is_looping(self) -> bool {
|
| 24 |
+
self.0 & 0x10000 != 0
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/// Whether volume should be used directly, skipping master volume scaling (bit 17).
|
| 28 |
+
#[inline]
|
| 29 |
+
pub fn is_raw_volume(self) -> bool {
|
| 30 |
+
self.0 & 0x20000 != 0
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/// Return a copy with the loop flag cleared.
|
| 34 |
+
#[inline]
|
| 35 |
+
pub fn without_loop(self) -> Self {
|
| 36 |
+
Self(self.0 & 0xFFFE_FFFF)
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
impl From<KnownSoundId> for SoundId {
|
| 41 |
#[inline]
|
| 42 |
fn from(known: KnownSoundId) -> Self {
|
|
@@ -13,9 +13,7 @@
|
|
| 13 |
//! - DispatchGlobalSound (0x526270)
|
| 14 |
//! - PlaySoundPooled_Direct (0x546B50)
|
| 15 |
|
| 16 |
-
use
|
| 17 |
-
|
| 18 |
-
use crate::audio::{play_sound, play_sound_pooled, SoundId, SoundSlot};
|
| 19 |
use crate::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
|
| 20 |
use crate::fixed::Fixed;
|
| 21 |
use crate::task::worm::CTaskWorm;
|
|
@@ -141,9 +139,9 @@ pub unsafe fn play_worm_sound(worm: *mut CTaskWorm, sound_id: SoundId, volume: F
|
|
| 141 |
}
|
| 142 |
}
|
| 143 |
}
|
| 144 |
-
// Start new streaming sound
|
| 145 |
// FUN_005150D0 hardcodes flags=3
|
| 146 |
-
let new_handle =
|
| 147 |
(*worm).sound_handle = new_handle;
|
| 148 |
}
|
| 149 |
|
|
@@ -178,17 +176,17 @@ pub unsafe fn play_worm_sound_2(
|
|
| 178 |
let worm_y = *((worm as *const u8).add(0x88) as *const i32);
|
| 179 |
let new_handle = if worm_y < -0x270F_FFFF && sound_id.0 == 0x36 {
|
| 180 |
// Special teleport case: play at weapon target position
|
| 181 |
-
|
| 182 |
worm,
|
| 183 |
-
sound_id
|
| 184 |
flags,
|
| 185 |
-
volume
|
| 186 |
-
(*worm).weapon_param_1 as
|
| 187 |
-
(*worm).weapon_param_2 as
|
| 188 |
)
|
| 189 |
} else {
|
| 190 |
-
// Normal case: play streaming sound
|
| 191 |
-
|
| 192 |
};
|
| 193 |
|
| 194 |
(*worm).sound_handle_2 = new_handle;
|
|
@@ -213,10 +211,10 @@ pub unsafe fn is_sound_suppressed(ddgame: *const DDGame) -> bool {
|
|
| 213 |
/// Returns 0xFFFFFFFF if suppressed, 0 if no DSSound, otherwise the channel handle.
|
| 214 |
pub unsafe fn dispatch_global_sound(
|
| 215 |
ddgame_wrapper: *const DDGameWrapper,
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
) -> u32 {
|
| 221 |
let g = &*(*ddgame_wrapper).ddgame;
|
| 222 |
let gi = &*g.game_info;
|
|
@@ -230,14 +228,7 @@ pub unsafe fn dispatch_global_sound(
|
|
| 230 |
return 0;
|
| 231 |
}
|
| 232 |
|
| 233 |
-
play_sound(
|
| 234 |
-
dssound,
|
| 235 |
-
SoundSlot(sound_id),
|
| 236 |
-
flags as i32,
|
| 237 |
-
Fixed(volume as i32),
|
| 238 |
-
Fixed(pitch as i32),
|
| 239 |
-
Fixed(0),
|
| 240 |
-
) as u32
|
| 241 |
}
|
| 242 |
|
| 243 |
/// Direct pooled sound playback β port of PlaySoundPooled_Direct (0x546B50).
|
|
@@ -245,9 +236,9 @@ pub unsafe fn dispatch_global_sound(
|
|
| 245 |
/// Bypasses queue, checks suppression + fast-forward, calls DSSound play_sound_pooled.
|
| 246 |
pub unsafe fn play_sound_pooled_direct(
|
| 247 |
task: *const CTask,
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
) -> i32 {
|
| 252 |
let g = &*(*task).ddgame;
|
| 253 |
let gi = &*g.game_info;
|
|
@@ -265,97 +256,309 @@ pub unsafe fn play_sound_pooled_direct(
|
|
| 265 |
return 0;
|
| 266 |
}
|
| 267 |
|
| 268 |
-
play_sound_pooled(dssound,
|
| 269 |
}
|
| 270 |
|
| 271 |
// ============================================================
|
| 272 |
-
//
|
| 273 |
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
pub static LOAD_AND_PLAY_STREAMING_ADDR: AtomicU32 = AtomicU32::new(0);
|
| 278 |
|
| 279 |
-
///
|
| 280 |
-
///
|
| 281 |
-
///
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
worm: *mut CTaskWorm,
|
| 284 |
-
sound_id:
|
| 285 |
flags: u32,
|
| 286 |
-
volume:
|
| 287 |
-
) ->
|
| 288 |
-
let
|
| 289 |
-
|
| 290 |
-
}
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
}
|
| 316 |
|
| 317 |
-
///
|
| 318 |
-
///
|
| 319 |
-
///
|
| 320 |
-
|
|
|
|
|
|
|
| 321 |
worm: *mut CTaskWorm,
|
| 322 |
-
sound_id:
|
| 323 |
flags: u32,
|
| 324 |
-
volume:
|
| 325 |
-
x:
|
| 326 |
-
y:
|
| 327 |
-
) ->
|
| 328 |
-
let
|
| 329 |
-
|
| 330 |
-
}
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
"pop esi",
|
| 359 |
-
"ret",
|
| 360 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
}
|
|
|
|
| 13 |
//! - DispatchGlobalSound (0x526270)
|
| 14 |
//! - PlaySoundPooled_Direct (0x546B50)
|
| 15 |
|
| 16 |
+
use crate::audio::{play_sound, play_sound_pooled, SoundId};
|
|
|
|
|
|
|
| 17 |
use crate::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
|
| 18 |
use crate::fixed::Fixed;
|
| 19 |
use crate::task::worm::CTaskWorm;
|
|
|
|
| 139 |
}
|
| 140 |
}
|
| 141 |
}
|
| 142 |
+
// Start new streaming sound β fully ported, no WA bridge needed
|
| 143 |
// FUN_005150D0 hardcodes flags=3
|
| 144 |
+
let new_handle = load_and_play_streaming(worm, sound_id, 3, volume);
|
| 145 |
(*worm).sound_handle = new_handle;
|
| 146 |
}
|
| 147 |
|
|
|
|
| 176 |
let worm_y = *((worm as *const u8).add(0x88) as *const i32);
|
| 177 |
let new_handle = if worm_y < -0x270F_FFFF && sound_id.0 == 0x36 {
|
| 178 |
// Special teleport case: play at weapon target position
|
| 179 |
+
load_and_play_streaming_positional(
|
| 180 |
worm,
|
| 181 |
+
sound_id,
|
| 182 |
flags,
|
| 183 |
+
volume,
|
| 184 |
+
Fixed((*worm).weapon_param_1 as i32),
|
| 185 |
+
Fixed((*worm).weapon_param_2 as i32),
|
| 186 |
)
|
| 187 |
} else {
|
| 188 |
+
// Normal case: play streaming sound β fully ported
|
| 189 |
+
load_and_play_streaming(worm, sound_id, flags, volume)
|
| 190 |
};
|
| 191 |
|
| 192 |
(*worm).sound_handle_2 = new_handle;
|
|
|
|
| 211 |
/// Returns 0xFFFFFFFF if suppressed, 0 if no DSSound, otherwise the channel handle.
|
| 212 |
pub unsafe fn dispatch_global_sound(
|
| 213 |
ddgame_wrapper: *const DDGameWrapper,
|
| 214 |
+
slot: SoundId,
|
| 215 |
+
priority: i32,
|
| 216 |
+
frequency: Fixed,
|
| 217 |
+
volume: Fixed,
|
| 218 |
) -> u32 {
|
| 219 |
let g = &*(*ddgame_wrapper).ddgame;
|
| 220 |
let gi = &*g.game_info;
|
|
|
|
| 228 |
return 0;
|
| 229 |
}
|
| 230 |
|
| 231 |
+
play_sound(dssound, slot, priority, frequency, volume, Fixed::ZERO) as u32
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
|
| 234 |
/// Direct pooled sound playback β port of PlaySoundPooled_Direct (0x546B50).
|
|
|
|
| 236 |
/// Bypasses queue, checks suppression + fast-forward, calls DSSound play_sound_pooled.
|
| 237 |
pub unsafe fn play_sound_pooled_direct(
|
| 238 |
task: *const CTask,
|
| 239 |
+
slot: SoundId,
|
| 240 |
+
priority: i32,
|
| 241 |
+
volume: Fixed,
|
| 242 |
) -> i32 {
|
| 243 |
let g = &*(*task).ddgame;
|
| 244 |
let gi = &*g.game_info;
|
|
|
|
| 256 |
return 0;
|
| 257 |
}
|
| 258 |
|
| 259 |
+
play_sound_pooled(dssound, slot, priority, Fixed::ONE, volume, Fixed::ZERO)
|
| 260 |
}
|
| 261 |
|
| 262 |
// ============================================================
|
| 263 |
+
// Local/streaming sound β full Rust port
|
| 264 |
// ============================================================
|
| 265 |
+
//
|
| 266 |
+
// Replaces the former naked asm bridges to WA's LoadAndPlayStreaming (0x546C20)
|
| 267 |
+
// and LoadAndPlayStreamingPositional (0x546BB0) with pure Rust implementations
|
| 268 |
+
// of the entire call chain:
|
| 269 |
+
//
|
| 270 |
+
// load_and_play_streaming β dispatch_local_sound
|
| 271 |
+
// β compute_distance_params β distance_3d_attenuation
|
| 272 |
+
// β DSSound::play_sound_pooled (vtable, already Rust)
|
| 273 |
+
// β ActiveSoundTable::record_sound
|
| 274 |
+
|
| 275 |
+
use crate::audio::active_sound::ActiveSoundTable;
|
| 276 |
+
|
| 277 |
+
/// 3D stereo distance attenuation β port of Distance3D_Attenuation (0x5430F0).
|
| 278 |
+
///
|
| 279 |
+
/// Computes volume attenuation and stereo pan based on the distance between
|
| 280 |
+
/// a sound source and the listener, using an elliptical model.
|
| 281 |
+
///
|
| 282 |
+
/// The function models two "ears" at Β±half_width from the listener's X position.
|
| 283 |
+
/// For each ear, it computes an inverse-distance attenuation from the sound source
|
| 284 |
+
/// using: `attenuation = scaledΒ² / (scaledΒ² + dyΒ² + dx_earΒ²)`, where `scaled` is
|
| 285 |
+
/// the characteristic distance (abs(half_width) * 0xDDB3 / 0x10000 β 0.866 * half_width).
|
| 286 |
+
///
|
| 287 |
+
/// Returns `(volume, pan)` as Fixed values where:
|
| 288 |
+
/// - `volume` = max(left_atten, right_atten), clamped to Fixed::ONE
|
| 289 |
+
/// - `pan` = right_atten - left_atten (positive = sound is to the right)
|
| 290 |
+
///
|
| 291 |
+
/// Convention: EAX = &listener_pos[x,y], stdcall(sound_x, sound_y, level_width_fixed,
|
| 292 |
+
/// attenuation_factor, out_volume, out_pan), RET 0x18.
|
| 293 |
+
fn distance_3d_attenuation(
|
| 294 |
+
listener_x: Fixed,
|
| 295 |
+
listener_y: Fixed,
|
| 296 |
+
sound_x: Fixed,
|
| 297 |
+
sound_y: Fixed,
|
| 298 |
+
level_width_fixed: Fixed,
|
| 299 |
+
attenuation_factor: i32,
|
| 300 |
+
) -> (Fixed, Fixed) {
|
| 301 |
+
/// β3/2 β 0.866, stored as Fixed (0xDDB3 / 0x10000)
|
| 302 |
+
const SQRT3_HALF: Fixed = Fixed(0xDDB3);
|
| 303 |
+
|
| 304 |
+
// Step 1: half_width = level_width / attenuation_factor
|
| 305 |
+
// Uses 64-bit intermediate, clamped so abs() can't overflow i32
|
| 306 |
+
let wide = (level_width_fixed.0 as i64) * (Fixed::ONE.0 as i64);
|
| 307 |
+
let half_width_64 = wide / (attenuation_factor as i64);
|
| 308 |
+
let half_width = Fixed(if half_width_64 > 0x7FFFFFFF {
|
| 309 |
+
0x7FFFFFFF
|
| 310 |
+
} else if half_width_64 < -0x7FFFFFFF {
|
| 311 |
+
// Clamp to -0x7FFFFFFF (not i32::MIN) so abs() in step 2 can't overflow
|
| 312 |
+
-0x7FFFFFFF
|
| 313 |
+
} else {
|
| 314 |
+
half_width_64 as i32
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
// Step 2: characteristic distance = abs(half_width) * β3/2
|
| 318 |
+
let scaled = half_width.abs() * SQRT3_HALF;
|
| 319 |
+
|
| 320 |
+
// Steps 3-5 use i64 intermediates to match the original's __allmul/__alldiv
|
| 321 |
+
// precision. The original keeps scaled_sq/dy_sq as full 64-bit values through
|
| 322 |
+
// the attenuation computation; truncating to i32 (Fixed) would lose precision.
|
| 323 |
+
let scaled_i = scaled.0 as i64;
|
| 324 |
+
let dy_i = (sound_y.0 - listener_y.0) as i64;
|
| 325 |
+
let dx = sound_x - listener_x;
|
| 326 |
+
let half = half_width / 2;
|
| 327 |
+
|
| 328 |
+
// squared terms: aΒ² / ONE (fixed-point squares, 64-bit)
|
| 329 |
+
let scaled_sq = (scaled_i * scaled_i) / Fixed::ONE.0 as i64;
|
| 330 |
+
let dy_sq = (dy_i * dy_i) / Fixed::ONE.0 as i64;
|
| 331 |
+
|
| 332 |
+
// Compute per-ear inverse-distance attenuation:
|
| 333 |
+
// atten = (scaled_sq * ONE) / (scaled_sq + dy_sq + dx_earΒ²)
|
| 334 |
+
|
| 335 |
+
// Step 4: Left ear β distance from sound to left ear = -(half + dx)
|
| 336 |
+
let left_dist = -(half + dx).0 as i64;
|
| 337 |
+
let left_dist_sq = (left_dist * left_dist) / Fixed::ONE.0 as i64;
|
| 338 |
+
let left_denom = scaled_sq + dy_sq + left_dist_sq;
|
| 339 |
+
let left_atten = if left_denom == 0 {
|
| 340 |
+
Fixed::ONE
|
| 341 |
+
} else {
|
| 342 |
+
Fixed(((scaled_sq * Fixed::ONE.0 as i64) / left_denom) as i32).min(Fixed::ONE)
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
+
// Step 5: Right ear β distance from sound to right ear = half - dx
|
| 346 |
+
let right_dist = (half - dx).0 as i64;
|
| 347 |
+
let right_dist_sq = (right_dist * right_dist) / Fixed::ONE.0 as i64;
|
| 348 |
+
let right_denom = right_dist_sq + dy_sq + scaled_sq;
|
| 349 |
+
let right_atten = if right_denom == 0 {
|
| 350 |
+
Fixed::ONE
|
| 351 |
+
} else {
|
| 352 |
+
Fixed(((scaled_sq * Fixed::ONE.0 as i64) / right_denom) as i32).min(Fixed::ONE)
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
+
// Step 6: volume = max(left, right), pan = right - left
|
| 356 |
+
(left_atten.max(right_atten), right_atten - left_atten)
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/// Compute distance-based volume and pan for a local sound β port of
|
| 360 |
+
/// ComputeDistanceParams (0x546300).
|
| 361 |
+
///
|
| 362 |
+
/// Reads the attenuation factor from GameInfo and the level width from DDGame.
|
| 363 |
+
/// If attenuation is disabled (factor == 0), returns full volume and center pan.
|
| 364 |
+
///
|
| 365 |
+
/// Convention: fastcall(ECX=&out_pan, EDX=&out_volume, stack=[table, x, y]), RET 0xC.
|
| 366 |
+
unsafe fn compute_distance_params(ddgame: *const DDGame, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
|
| 367 |
+
let gi = &*(*ddgame).game_info;
|
| 368 |
+
let attenuation = gi.sound_attenuation;
|
| 369 |
+
|
| 370 |
+
if attenuation == 0 {
|
| 371 |
+
// No 3D audio β full volume, center pan
|
| 372 |
+
return (Fixed::ONE, Fixed::ZERO);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
let level_width_fixed = Fixed((*ddgame).level_width_sound << 16);
|
| 376 |
+
let (lx, ly) = (*ddgame).listener_pos();
|
| 377 |
+
|
| 378 |
+
distance_3d_attenuation(Fixed(lx), Fixed(ly), x, y, level_width_fixed, attenuation)
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/// Record an active local sound in the tracking table β port of
|
| 382 |
+
/// RecordActiveSound (0x546260).
|
| 383 |
+
///
|
| 384 |
+
/// Finds a free slot in the 64-entry ring buffer (probing by incrementing
|
| 385 |
+
/// counter until channel_handle == 0), then writes the entry.
|
| 386 |
+
///
|
| 387 |
+
/// Convention: usercall(EAX=table, ESI=emitter) + stdcall(x, y, volume, channel_handle),
|
| 388 |
+
/// RET 0x10. Returns the counter value (used as handle with 0x40000000 bit).
|
| 389 |
+
unsafe fn record_active_sound(
|
| 390 |
+
table: *mut ActiveSoundTable,
|
| 391 |
+
emitter: *mut u8,
|
| 392 |
+
x: i32,
|
| 393 |
+
y: i32,
|
| 394 |
+
volume: i32,
|
| 395 |
+
channel_handle: i32,
|
| 396 |
+
) -> i32 {
|
| 397 |
+
let t = &mut *table;
|
| 398 |
+
|
| 399 |
+
// Probe for a free slot (channel_handle == 0)
|
| 400 |
+
loop {
|
| 401 |
+
t.counter = t.counter.wrapping_add(1);
|
| 402 |
+
let slot = (t.counter & 0x3F) as usize;
|
| 403 |
+
if t.entries[slot].channel_handle == 0 {
|
| 404 |
+
break;
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
let slot = (t.counter & 0x3F) as usize;
|
| 409 |
+
let entry = &mut t.entries[slot];
|
| 410 |
+
entry.pos_x = Fixed(x);
|
| 411 |
+
entry.pos_y = Fixed(y);
|
| 412 |
+
entry.emitter = emitter;
|
| 413 |
+
entry.volume = Fixed(volume);
|
| 414 |
+
entry.sequence = t.counter as i32;
|
| 415 |
+
entry.channel_handle = channel_handle as u32;
|
| 416 |
+
|
| 417 |
+
// Increment emitter ref count (emitter+0x08)
|
| 418 |
+
if !emitter.is_null() {
|
| 419 |
+
let ref_count = emitter.add(8) as *mut i32;
|
| 420 |
+
*ref_count += 1;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
t.counter as i32
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
/// Dispatch a local (positional) sound β port of DispatchLocalSound (0x546360).
|
| 427 |
+
///
|
| 428 |
+
/// Clamps volume, computes 3D distance attenuation and pan, calls
|
| 429 |
+
/// DSSound::play_sound_pooled, then records the active sound.
|
| 430 |
+
///
|
| 431 |
+
/// Convention: usercall(EAX=volume, EDI=active_sound_table) +
|
| 432 |
+
/// stdcall(sound_slot, flags, x, y, emitter), RET 0x14.
|
| 433 |
+
unsafe fn dispatch_local_sound(
|
| 434 |
+
table: *mut ActiveSoundTable,
|
| 435 |
+
volume: Fixed,
|
| 436 |
+
sound_slot: SoundId,
|
| 437 |
+
flags: i32,
|
| 438 |
+
pos: (Fixed, Fixed),
|
| 439 |
+
emitter: *mut u8,
|
| 440 |
+
) -> i32 {
|
| 441 |
+
let volume = volume.min(Fixed::ONE);
|
| 442 |
+
|
| 443 |
+
let ddgame = (*table).ddgame;
|
| 444 |
+
let (volume_atten, pan) = compute_distance_params(ddgame, pos.0, pos.1);
|
| 445 |
+
|
| 446 |
+
let scaled_volume = volume * volume_atten;
|
| 447 |
+
|
| 448 |
+
// Call DSSound::play_sound_pooled (vtable slot 4)
|
| 449 |
+
let sound = (*ddgame).sound;
|
| 450 |
+
let sound_vt = &*(*sound).vtable;
|
| 451 |
+
let handle =
|
| 452 |
+
(sound_vt.play_sound_pooled)(sound, sound_slot, flags, Fixed::ONE, scaled_volume, pan);
|
| 453 |
+
|
| 454 |
+
if handle == 0 {
|
| 455 |
+
return 0;
|
| 456 |
+
}
|
| 457 |
|
| 458 |
+
record_active_sound(table, emitter, pos.0 .0, pos.1 .0, volume.0, handle)
|
| 459 |
+
}
|
|
|
|
| 460 |
|
| 461 |
+
/// Load and play a streaming sound β port of LoadAndPlayStreaming (0x546C20).
|
| 462 |
+
///
|
| 463 |
+
/// Checks suppression conditions, gets emitter position via vtable call,
|
| 464 |
+
/// then dispatches as a local sound. Returns handle | 0x40000000 on success,
|
| 465 |
+
/// 0 on failure, -1 (0xFFFFFFFF) if suppressed.
|
| 466 |
+
///
|
| 467 |
+
/// Replaces the former naked asm bridge `call_load_and_play_streaming`.
|
| 468 |
+
pub unsafe fn load_and_play_streaming(
|
| 469 |
worm: *mut CTaskWorm,
|
| 470 |
+
sound_id: SoundId,
|
| 471 |
flags: u32,
|
| 472 |
+
volume: Fixed,
|
| 473 |
+
) -> i32 {
|
| 474 |
+
let ddgame = CTask::ddgame_raw(worm as *const CTask);
|
| 475 |
+
let gi = &*(*ddgame).game_info;
|
|
|
|
| 476 |
|
| 477 |
+
// Suppression checks (matching 0x546C20 exactly)
|
| 478 |
+
if gi.sound_mute != 0 || (*ddgame).frame_counter < gi.sound_start_frame {
|
| 479 |
+
return -1;
|
| 480 |
+
}
|
| 481 |
+
if (*ddgame).fast_forward_active != 0 {
|
| 482 |
+
return -1;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
let sound = (*ddgame).sound;
|
| 486 |
+
if sound.is_null() {
|
| 487 |
+
return 0;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
// Get emitter position via sound_emitter vtable[0] (GetPosition)
|
| 491 |
+
let task = worm as *mut CGameTask;
|
| 492 |
+
let emitter = &(*task).sound_emitter;
|
| 493 |
+
let mut pos_x: u32 = 0;
|
| 494 |
+
let mut pos_y: u32 = 0;
|
| 495 |
+
((*emitter.vtable).get_position)(emitter, &mut pos_x, &mut pos_y);
|
| 496 |
+
|
| 497 |
+
// Dispatch as local sound
|
| 498 |
+
let table = (*ddgame).active_sounds;
|
| 499 |
+
if table.is_null() {
|
| 500 |
+
return 0;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
let handle = dispatch_local_sound(
|
| 504 |
+
table,
|
| 505 |
+
volume,
|
| 506 |
+
sound_id,
|
| 507 |
+
flags as i32,
|
| 508 |
+
(Fixed(pos_x as i32), Fixed(pos_y as i32)),
|
| 509 |
+
emitter as *const _ as *mut u8,
|
| 510 |
);
|
| 511 |
+
if handle == 0 {
|
| 512 |
+
return 0;
|
| 513 |
+
}
|
| 514 |
+
handle | 0x40000000
|
| 515 |
}
|
| 516 |
|
| 517 |
+
/// Load and play a streaming sound at a specific position β port of
|
| 518 |
+
/// LoadAndPlayStreamingPositional (0x546BB0).
|
| 519 |
+
///
|
| 520 |
+
/// Same as `load_and_play_streaming` but uses explicit (x, y) coordinates
|
| 521 |
+
/// instead of reading from the emitter. Used by teleport sound special case.
|
| 522 |
+
pub unsafe fn load_and_play_streaming_positional(
|
| 523 |
worm: *mut CTaskWorm,
|
| 524 |
+
sound_id: SoundId,
|
| 525 |
flags: u32,
|
| 526 |
+
volume: Fixed,
|
| 527 |
+
x: Fixed,
|
| 528 |
+
y: Fixed,
|
| 529 |
+
) -> i32 {
|
| 530 |
+
let ddgame = CTask::ddgame_raw(worm as *const CTask);
|
| 531 |
+
let gi = &*(*ddgame).game_info;
|
|
|
|
| 532 |
|
| 533 |
+
// Suppression checks (matching 0x546BB0 exactly)
|
| 534 |
+
if gi.sound_mute != 0 || (*ddgame).frame_counter < gi.sound_start_frame {
|
| 535 |
+
return -1;
|
| 536 |
+
}
|
| 537 |
+
if (*ddgame).fast_forward_active != 0 {
|
| 538 |
+
return -1;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
let sound = (*ddgame).sound;
|
| 542 |
+
if sound.is_null() {
|
| 543 |
+
return 0;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
let table = (*ddgame).active_sounds;
|
| 547 |
+
if table.is_null() {
|
| 548 |
+
return 0;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
// Note: positional variant passes 0 for emitter (no ref tracking)
|
| 552 |
+
let handle = dispatch_local_sound(
|
| 553 |
+
table,
|
| 554 |
+
volume,
|
| 555 |
+
sound_id,
|
| 556 |
+
flags as i32,
|
| 557 |
+
(x, y),
|
| 558 |
+
core::ptr::null_mut(),
|
|
|
|
|
|
|
| 559 |
);
|
| 560 |
+
if handle == 0 {
|
| 561 |
+
return 0;
|
| 562 |
+
}
|
| 563 |
+
handle | 0x40000000
|
| 564 |
}
|
|
@@ -285,8 +285,13 @@ pub struct DDGame {
|
|
| 285 |
pub level_bound_min_y: i32,
|
| 286 |
/// 0x77A8: Level bound max Y (Fixed-point).
|
| 287 |
pub level_bound_max_y: i32,
|
| 288 |
-
/// 0x77AC-
|
| 289 |
-
pub _unknown_77ac: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
/// 0x77C0: Level width in pixels (set by PCLandscape constructor).
|
| 292 |
pub level_width: u32,
|
|
@@ -494,6 +499,14 @@ impl DDGame {
|
|
| 494 |
self.hud_status_text = load_string(0x70F);
|
| 495 |
}
|
| 496 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
}
|
| 498 |
|
| 499 |
// DDGame constructor code (create_ddgame, init helpers, usercall bridges)
|
|
|
|
| 285 |
pub level_bound_min_y: i32,
|
| 286 |
/// 0x77A8: Level bound max Y (Fixed-point).
|
| 287 |
pub level_bound_max_y: i32,
|
| 288 |
+
/// 0x77AC-0x77B7: Unknown
|
| 289 |
+
pub _unknown_77ac: [u8; 0x77B8 - 0x77AC],
|
| 290 |
+
/// 0x77B8: Level width for 3D sound distance computation (pixels, not fixed-point).
|
| 291 |
+
/// Read by ComputeDistanceParams, shifted left 16 before passing to Distance3D_Attenuation.
|
| 292 |
+
pub level_width_sound: i32,
|
| 293 |
+
/// 0x77BC-0x77BF: Unknown
|
| 294 |
+
pub _unknown_77bc: [u8; 0x77C0 - 0x77BC],
|
| 295 |
|
| 296 |
/// 0x77C0: Level width in pixels (set by PCLandscape constructor).
|
| 297 |
pub level_width: u32,
|
|
|
|
| 499 |
self.hud_status_text = load_string(0x70F);
|
| 500 |
}
|
| 501 |
}
|
| 502 |
+
|
| 503 |
+
/// Listener position for 3D audio β stored at DDGame+0x8CEC (screen_coords[3]).
|
| 504 |
+
///
|
| 505 |
+
/// Returns `(x, y)` as raw i32 values (fixed-point 16.16).
|
| 506 |
+
/// Used by ComputeDistanceParams / Distance3D_Attenuation.
|
| 507 |
+
pub fn listener_pos(&self) -> (i32, i32) {
|
| 508 |
+
(self.screen_coords[3].x, self.screen_coords[3].y)
|
| 509 |
+
}
|
| 510 |
}
|
| 511 |
|
| 512 |
// DDGame constructor code (create_ddgame, init helpers, usercall bridges)
|
|
@@ -144,8 +144,13 @@ pub struct GameInfo {
|
|
| 144 |
/// 0xF374: Display flags passed to DDDisplay::Init.
|
| 145 |
pub display_flags: u32,
|
| 146 |
|
| 147 |
-
/// 0xF378-
|
| 148 |
-
pub _unknown_f378: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
// --- Cluster 2: game options (populated by LoadOptions) ---
|
| 151 |
/// 0xF3A0: Unknown config byte (from global 0x7C0D38)
|
|
|
|
| 144 |
/// 0xF374: Display flags passed to DDDisplay::Init.
|
| 145 |
pub display_flags: u32,
|
| 146 |
|
| 147 |
+
/// 0xF378-0xF38B: Unknown
|
| 148 |
+
pub _unknown_f378: [u8; 0xF38C - 0xF378],
|
| 149 |
+
/// 0xF38C: Sound distance attenuation factor (i32). When nonzero, enables 3D
|
| 150 |
+
/// positional audio via Distance3D_Attenuation. Zero = all sounds at full volume.
|
| 151 |
+
pub sound_attenuation: i32,
|
| 152 |
+
/// 0xF390-0xF39F: Unknown
|
| 153 |
+
pub _unknown_f390: [u8; 0xF3A0 - 0xF390],
|
| 154 |
|
| 155 |
// --- Cluster 2: game options (populated by LoadOptions) ---
|
| 156 |
/// 0xF3A0: Unknown config byte (from global 0x7C0D38)
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
use core::ops::{Add, Mul, Neg, Sub};
|
| 2 |
use std::ops::{AddAssign, SubAssign};
|
| 3 |
|
| 4 |
/// 16.16 fixed-point number used throughout WA for coordinates and velocities.
|
|
@@ -104,6 +104,24 @@ impl Mul<i32> for Fixed {
|
|
| 104 |
}
|
| 105 |
}
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
impl Neg for Fixed {
|
| 108 |
type Output = Self;
|
| 109 |
#[inline]
|
|
|
|
| 1 |
+
use core::ops::{Add, Div, Mul, Neg, Sub};
|
| 2 |
use std::ops::{AddAssign, SubAssign};
|
| 3 |
|
| 4 |
/// 16.16 fixed-point number used throughout WA for coordinates and velocities.
|
|
|
|
| 104 |
}
|
| 105 |
}
|
| 106 |
|
| 107 |
+
impl Div for Fixed {
|
| 108 |
+
type Output = Self;
|
| 109 |
+
/// Fixed-point division: (self << 16) / rhs, using i64 intermediate.
|
| 110 |
+
#[inline]
|
| 111 |
+
fn div(self, rhs: Self) -> Self {
|
| 112 |
+
Self((((self.0 as i64) << Self::FRACTIONAL_BITS) / rhs.0 as i64) as i32)
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Divide a Fixed value by a plain integer (no shift β just `raw / n`).
|
| 117 |
+
impl Div<i32> for Fixed {
|
| 118 |
+
type Output = Self;
|
| 119 |
+
#[inline]
|
| 120 |
+
fn div(self, rhs: i32) -> Self {
|
| 121 |
+
Self(self.0 / rhs)
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
impl Neg for Fixed {
|
| 126 |
type Output = Self;
|
| 127 |
#[inline]
|
|
@@ -396,10 +396,10 @@ pub struct CTaskWorm {
|
|
| 396 |
/// 0x3B0: Streaming sound handle. Nonzero when a worm sound effect
|
| 397 |
/// (e.g., weapon charge-up) is actively playing. PlayWormSound stores the
|
| 398 |
/// new handle here; StopWormSound clears it.
|
| 399 |
-
pub sound_handle:
|
| 400 |
/// 0x3B4: Secondary sound handle, used by FUN_00515020 (teleport/weapon sounds).
|
| 401 |
/// Same stop/play semantics as `sound_handle` but a separate channel.
|
| 402 |
-
pub sound_handle_2:
|
| 403 |
/// 0x3B8β0x3DB: Unknown
|
| 404 |
pub _unknown_3b8: [u8; 0x3DC - 0x3B8],
|
| 405 |
/// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
|
|
|
|
| 396 |
/// 0x3B0: Streaming sound handle. Nonzero when a worm sound effect
|
| 397 |
/// (e.g., weapon charge-up) is actively playing. PlayWormSound stores the
|
| 398 |
/// new handle here; StopWormSound clears it.
|
| 399 |
+
pub sound_handle: i32,
|
| 400 |
/// 0x3B4: Secondary sound handle, used by FUN_00515020 (teleport/weapon sounds).
|
| 401 |
/// Same stop/play semantics as `sound_handle` but a separate channel.
|
| 402 |
+
pub sound_handle_2: i32,
|
| 403 |
/// 0x3B8β0x3DB: Unknown
|
| 404 |
pub _unknown_3b8: [u8; 0x3DC - 0x3B8],
|
| 405 |
/// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
|
|
@@ -15,7 +15,7 @@ use std::sync::atomic::Ordering;
|
|
| 15 |
|
| 16 |
use openwa_core::address::va;
|
| 17 |
use openwa_core::audio::sound_ops;
|
| 18 |
-
use openwa_core::audio::{KnownSoundId, SoundId
|
| 19 |
use openwa_core::engine::{DDGame, DDGameWrapper};
|
| 20 |
use openwa_core::fixed::Fixed;
|
| 21 |
use openwa_core::task::worm::CTaskWorm;
|
|
@@ -31,23 +31,22 @@ static SOUND_LOG_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::Ato
|
|
| 31 |
|
| 32 |
unsafe extern "thiscall" fn hook_play_sound_global(
|
| 33 |
this: *const CGameTask,
|
| 34 |
-
sound_id:
|
| 35 |
flags: u32,
|
| 36 |
volume: Fixed,
|
| 37 |
pitch: Fixed,
|
| 38 |
) -> u32 {
|
| 39 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 40 |
-
let sound_name = KnownSoundId::try_from(sound_id)
|
| 41 |
.map(|s| format!("{s:?}"))
|
| 42 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 43 |
let _ = log_line(&format!(
|
| 44 |
-
"[Sound] Global: task=0x{this:08X?} id={sound_id}({sound_name}) \
|
| 45 |
p3={flags} p4={volume} p5={pitch}"
|
| 46 |
));
|
| 47 |
}
|
| 48 |
|
| 49 |
-
sound_ops::queue_sound((*this).base.ddgame,
|
| 50 |
-
as u32
|
| 51 |
}
|
| 52 |
|
| 53 |
// ββ PlaySoundLocal (0x4FDFE0): usercall(EAX=pitch, ECX=volume, EDI=task, stack) ββ
|
|
@@ -59,20 +58,20 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 59 |
pitch: Fixed,
|
| 60 |
volume: Fixed,
|
| 61 |
task: *mut CGameTask,
|
| 62 |
-
sound_id:
|
| 63 |
flags: u32,
|
| 64 |
) -> u32 {
|
| 65 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 66 |
-
let sound_name = KnownSoundId::try_from(sound_id)
|
| 67 |
.map(|s| format!("{s:?}"))
|
| 68 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 69 |
let _ = log_line(&format!(
|
| 70 |
"[Sound] Local: pitch={pitch} volume={volume} task=0x{task:08X?} \
|
| 71 |
-
id={sound_id}({sound_name}) flags={flags}"
|
| 72 |
));
|
| 73 |
}
|
| 74 |
|
| 75 |
-
sound_ops::play_sound_local(task,
|
| 76 |
}
|
| 77 |
|
| 78 |
// ββ WormPlaySound2 (0x515020): usercall(EDI=worm) + stdcall(sound_id, volume, flags) ββ
|
|
@@ -93,11 +92,11 @@ unsafe extern "C" fn trampoline_worm_play_sound_2() {
|
|
| 93 |
|
| 94 |
unsafe extern "cdecl" fn play_worm_sound_2_cdecl(
|
| 95 |
worm: *mut CTaskWorm,
|
| 96 |
-
sound_id:
|
| 97 |
-
volume:
|
| 98 |
flags: u32,
|
| 99 |
) {
|
| 100 |
-
sound_ops::play_worm_sound_2(worm,
|
| 101 |
}
|
| 102 |
|
| 103 |
// ββ IsSoundSuppressed (0x5261E0): thiscall(ECX=DDGame*) ββ
|
|
@@ -111,12 +110,12 @@ unsafe extern "thiscall" fn hook_is_sound_suppressed(ddgame: *mut DDGame) -> u32
|
|
| 111 |
unsafe extern "fastcall" fn hook_dispatch_global_sound(
|
| 112 |
_ecx: u32,
|
| 113 |
ddgame_wrapper: *const DDGameWrapper,
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
) -> u32 {
|
| 119 |
-
sound_ops::dispatch_global_sound(ddgame_wrapper,
|
| 120 |
}
|
| 121 |
|
| 122 |
// ββ PlaySoundPooled_Direct (0x546B50): fastcall(ECX=unused, EDX=task) + 3 stack ββ
|
|
@@ -124,11 +123,11 @@ unsafe extern "fastcall" fn hook_dispatch_global_sound(
|
|
| 124 |
unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
|
| 125 |
_ecx: u32,
|
| 126 |
task: *const CTask,
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
) -> i32 {
|
| 131 |
-
sound_ops::play_sound_pooled_direct(task,
|
| 132 |
}
|
| 133 |
|
| 134 |
// ββ Hook installation ββ
|
|
@@ -175,12 +174,6 @@ pub fn install() -> Result<(), String> {
|
|
| 175 |
va::WORM_PLAY_SOUND_2,
|
| 176 |
trampoline_worm_play_sound_2 as *const (),
|
| 177 |
)?;
|
| 178 |
-
|
| 179 |
-
// Initialize bridge address for streaming sound functions
|
| 180 |
-
sound_ops::LOAD_AND_PLAY_STREAMING_ADDR.store(
|
| 181 |
-
openwa_core::rebase::rb(va::LOAD_AND_PLAY_STREAMING),
|
| 182 |
-
core::sync::atomic::Ordering::Relaxed,
|
| 183 |
-
);
|
| 184 |
}
|
| 185 |
|
| 186 |
Ok(())
|
|
|
|
| 15 |
|
| 16 |
use openwa_core::address::va;
|
| 17 |
use openwa_core::audio::sound_ops;
|
| 18 |
+
use openwa_core::audio::{KnownSoundId, SoundId};
|
| 19 |
use openwa_core::engine::{DDGame, DDGameWrapper};
|
| 20 |
use openwa_core::fixed::Fixed;
|
| 21 |
use openwa_core::task::worm::CTaskWorm;
|
|
|
|
| 31 |
|
| 32 |
unsafe extern "thiscall" fn hook_play_sound_global(
|
| 33 |
this: *const CGameTask,
|
| 34 |
+
sound_id: SoundId,
|
| 35 |
flags: u32,
|
| 36 |
volume: Fixed,
|
| 37 |
pitch: Fixed,
|
| 38 |
) -> u32 {
|
| 39 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 40 |
+
let sound_name = KnownSoundId::try_from(sound_id.0)
|
| 41 |
.map(|s| format!("{s:?}"))
|
| 42 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 43 |
let _ = log_line(&format!(
|
| 44 |
+
"[Sound] Global: task=0x{this:08X?} id={sound_id:?}({sound_name}) \
|
| 45 |
p3={flags} p4={volume} p5={pitch}"
|
| 46 |
));
|
| 47 |
}
|
| 48 |
|
| 49 |
+
sound_ops::queue_sound((*this).base.ddgame, sound_id, flags, volume, pitch).is_some() as u32
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
// ββ PlaySoundLocal (0x4FDFE0): usercall(EAX=pitch, ECX=volume, EDI=task, stack) ββ
|
|
|
|
| 58 |
pitch: Fixed,
|
| 59 |
volume: Fixed,
|
| 60 |
task: *mut CGameTask,
|
| 61 |
+
sound_id: SoundId,
|
| 62 |
flags: u32,
|
| 63 |
) -> u32 {
|
| 64 |
if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
|
| 65 |
+
let sound_name = KnownSoundId::try_from(sound_id.0)
|
| 66 |
.map(|s| format!("{s:?}"))
|
| 67 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 68 |
let _ = log_line(&format!(
|
| 69 |
"[Sound] Local: pitch={pitch} volume={volume} task=0x{task:08X?} \
|
| 70 |
+
id={sound_id:?}({sound_name}) flags={flags}"
|
| 71 |
));
|
| 72 |
}
|
| 73 |
|
| 74 |
+
sound_ops::play_sound_local(task, sound_id, flags, volume, pitch) as u32
|
| 75 |
}
|
| 76 |
|
| 77 |
// ββ WormPlaySound2 (0x515020): usercall(EDI=worm) + stdcall(sound_id, volume, flags) ββ
|
|
|
|
| 92 |
|
| 93 |
unsafe extern "cdecl" fn play_worm_sound_2_cdecl(
|
| 94 |
worm: *mut CTaskWorm,
|
| 95 |
+
sound_id: SoundId,
|
| 96 |
+
volume: Fixed,
|
| 97 |
flags: u32,
|
| 98 |
) {
|
| 99 |
+
sound_ops::play_worm_sound_2(worm, sound_id, volume, flags);
|
| 100 |
}
|
| 101 |
|
| 102 |
// ββ IsSoundSuppressed (0x5261E0): thiscall(ECX=DDGame*) ββ
|
|
|
|
| 110 |
unsafe extern "fastcall" fn hook_dispatch_global_sound(
|
| 111 |
_ecx: u32,
|
| 112 |
ddgame_wrapper: *const DDGameWrapper,
|
| 113 |
+
slot: SoundId,
|
| 114 |
+
priority: i32,
|
| 115 |
+
frequency: Fixed,
|
| 116 |
+
volume: Fixed,
|
| 117 |
) -> u32 {
|
| 118 |
+
sound_ops::dispatch_global_sound(ddgame_wrapper, slot, priority, frequency, volume)
|
| 119 |
}
|
| 120 |
|
| 121 |
// ββ PlaySoundPooled_Direct (0x546B50): fastcall(ECX=unused, EDX=task) + 3 stack ββ
|
|
|
|
| 123 |
unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
|
| 124 |
_ecx: u32,
|
| 125 |
task: *const CTask,
|
| 126 |
+
slot: SoundId,
|
| 127 |
+
priority: i32,
|
| 128 |
+
volume: Fixed,
|
| 129 |
) -> i32 {
|
| 130 |
+
sound_ops::play_sound_pooled_direct(task, slot, priority, volume)
|
| 131 |
}
|
| 132 |
|
| 133 |
// ββ Hook installation ββ
|
|
|
|
| 174 |
va::WORM_PLAY_SOUND_2,
|
| 175 |
trampoline_worm_play_sound_2 as *const (),
|
| 176 |
)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
Ok(())
|