feat: convert sound dispatch helpers and fix ActiveSoundEntry fields
Browse filesReplace 3 sound queue dispatch functions with Rust:
- IsSoundSuppressed (0x5261E0): mute flag + frame counter check
- DispatchGlobalSound (0x526270): suppression + DSSound::play_sound
- PlaySoundPooled_Direct (0x546B50): direct play bypassing queue
Fix ActiveSoundEntry field names based on RecordActiveSound analysis:
- +0x10: channel_index → sequence (running counter/unique ID)
- +0x14: flags → channel_handle (DSSound channel, 0 = free)
- +0x600: count → counter (ring buffer insertion counter)
Add 9 dispatch helper addresses to address.rs and label in Ghidra.
Remove DSSound vtable validation (all 24 slots are Rust pointers).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/address.rs
CHANGED
|
@@ -388,6 +388,45 @@ pub mod va {
|
|
| 388 |
pub const PLAY_SOUND_LOCAL: u32 = 0x004F_DFE0;
|
| 389 |
pub const PLAY_SOUND_GLOBAL: u32 = 0x0054_6E20;
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
// === Speech / Voice Lines ===
|
| 392 |
|
| 393 |
/// Speech line table in .rdata: array of {u32 id, *const u8 name},
|
|
|
|
| 388 |
pub const PLAY_SOUND_LOCAL: u32 = 0x004F_DFE0;
|
| 389 |
pub const PLAY_SOUND_GLOBAL: u32 = 0x0054_6E20;
|
| 390 |
|
| 391 |
+
// --- Sound queue dispatch (bridge: queue → DSSound) ---
|
| 392 |
+
|
| 393 |
+
/// IsSoundSuppressed — checks mute flag + frame counter.
|
| 394 |
+
/// fastcall(ECX=ddgame). Returns 0 if sound OK, 1 if suppressed.
|
| 395 |
+
pub const IS_SOUND_SUPPRESSED: u32 = 0x0052_61E0;
|
| 396 |
+
|
| 397 |
+
/// DispatchGlobalSound — suppression check + DSSound vtable slot 3 (play_sound).
|
| 398 |
+
/// fastcall(ECX=?, EDX=task_turn_game) + 4 stack params (sound_id, flags, volume, pitch).
|
| 399 |
+
pub const DISPATCH_GLOBAL_SOUND: u32 = 0x0052_6270;
|
| 400 |
+
|
| 401 |
+
/// RecordActiveSound — inserts into 64-entry ring buffer (ActiveSoundTable).
|
| 402 |
+
/// usercall(EAX=table, ESI=emitter) + 4 stack(pos_x, pos_y, volume, channel_handle).
|
| 403 |
+
pub const RECORD_ACTIVE_SOUND: u32 = 0x0054_6260;
|
| 404 |
+
|
| 405 |
+
/// ComputeDistanceParams — computes volume/pan from world position.
|
| 406 |
+
/// fastcall(ECX=out_pan, EDX=out_volume) + 3 stack(ddgame_offset, pos_x, pos_y).
|
| 407 |
+
/// If GameInfo+0xF38C == 0, returns volume=0x10000, pan=0.
|
| 408 |
+
pub const COMPUTE_DISTANCE_PARAMS: u32 = 0x0054_6300;
|
| 409 |
+
|
| 410 |
+
/// DispatchLocalSound — core: distance attenuation + DSSound slot 4 + RecordActiveSound.
|
| 411 |
+
/// usercall(EAX=base_volume, EDI=ddgame_offset) + 4 stack(sound_id, flags, pos_x, pos_y).
|
| 412 |
+
pub const DISPATCH_LOCAL_SOUND: u32 = 0x0054_6360;
|
| 413 |
+
|
| 414 |
+
/// PlayLocalNoEmitter — thin wrapper → DispatchLocalSound.
|
| 415 |
+
/// thiscall(ECX=?) + 3 stack(sound_id, flags, pitch).
|
| 416 |
+
pub const PLAY_LOCAL_NO_EMITTER: u32 = 0x0054_6430;
|
| 417 |
+
|
| 418 |
+
/// PlayLocalWithEmitter — gets pos from emitter vtable, then DispatchLocalSound.
|
| 419 |
+
/// usercall(ESI=emitter) + stack params.
|
| 420 |
+
pub const PLAY_LOCAL_WITH_EMITTER: u32 = 0x0054_63F0;
|
| 421 |
+
|
| 422 |
+
/// PlaySoundPooled_Direct — bypasses queue, same suppression + DSSound slot 4.
|
| 423 |
+
/// fastcall(ECX=?, EDX=task) + 3 stack params.
|
| 424 |
+
pub const PLAY_SOUND_POOLED_DIRECT: u32 = 0x0054_6B50;
|
| 425 |
+
|
| 426 |
+
/// Distance3D_Attenuation — elliptical distance model for positional audio.
|
| 427 |
+
/// usercall(EAX=camera_pos_ptr) + 6 stack params. Returns volume + pan via out ptrs.
|
| 428 |
+
pub const DISTANCE_3D_ATTENUATION: u32 = 0x0054_30F0;
|
| 429 |
+
|
| 430 |
// === Speech / Voice Lines ===
|
| 431 |
|
| 432 |
/// Speech line table in .rdata: array of {u32 id, *const u8 name},
|
crates/openwa-core/src/audio/active_sound.rs
CHANGED
|
@@ -15,8 +15,8 @@ use crate::fixed::Fixed;
|
|
| 15 |
pub struct ActiveSoundTable {
|
| 16 |
/// 0x000-0x5FF: 64 sound entries (entry 0 is unused/reserved).
|
| 17 |
pub entries: [ActiveSoundEntry; 64],
|
| 18 |
-
/// 0x600:
|
| 19 |
-
pub
|
| 20 |
/// 0x604: Back-pointer to owning DDGame.
|
| 21 |
pub ddgame: *mut DDGame,
|
| 22 |
}
|
|
@@ -39,10 +39,10 @@ pub struct ActiveSoundEntry {
|
|
| 39 |
pub pos_y: Fixed,
|
| 40 |
/// 0x0C: Volume (fixed-point 16.16, 0x10000 = 1.0).
|
| 41 |
pub volume: Fixed,
|
| 42 |
-
/// 0x10:
|
| 43 |
-
pub
|
| 44 |
-
/// 0x14:
|
| 45 |
-
pub
|
| 46 |
}
|
| 47 |
|
| 48 |
const _: () = assert!(core::mem::size_of::<ActiveSoundEntry>() == 0x18);
|
|
|
|
| 15 |
pub struct ActiveSoundTable {
|
| 16 |
/// 0x000-0x5FF: 64 sound entries (entry 0 is unused/reserved).
|
| 17 |
pub entries: [ActiveSoundEntry; 64],
|
| 18 |
+
/// 0x600: Running counter — incremented on each insert, masked to index (& 0x3F).
|
| 19 |
+
pub counter: u32,
|
| 20 |
/// 0x604: Back-pointer to owning DDGame.
|
| 21 |
pub ddgame: *mut DDGame,
|
| 22 |
}
|
|
|
|
| 39 |
pub pos_y: Fixed,
|
| 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: u32,
|
| 44 |
+
/// 0x14: DSSound channel handle (return from play_sound_pooled). 0 = free slot.
|
| 45 |
+
pub channel_handle: u32,
|
| 46 |
}
|
| 47 |
|
| 48 |
const _: () = assert!(core::mem::size_of::<ActiveSoundEntry>() == 0x18);
|
crates/openwa-wormkit/src/replacements/sound.rs
CHANGED
|
@@ -133,6 +133,108 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 133 |
1
|
| 134 |
}
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
// ============================================================
|
| 137 |
// Hook installation
|
| 138 |
// ============================================================
|
|
@@ -156,6 +258,25 @@ pub fn install() -> Result<(), String> {
|
|
| 156 |
trampoline_play_sound_local as *const (),
|
| 157 |
)?;
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
// Patch DSSound vtable: replace trivial slots with Rust implementations.
|
| 160 |
patch_dssound_vtable()?;
|
| 161 |
}
|
|
|
|
| 133 |
1
|
| 134 |
}
|
| 135 |
|
| 136 |
+
// ============================================================
|
| 137 |
+
// Sound dispatch helpers (bridge: queue → DSSound)
|
| 138 |
+
// ============================================================
|
| 139 |
+
|
| 140 |
+
/// IsSoundSuppressed (0x5261E0) — thiscall(ECX=DDGame*), plain RET.
|
| 141 |
+
/// Returns 0 if sound playback is allowed, 1 if suppressed.
|
| 142 |
+
///
|
| 143 |
+
/// Checks:
|
| 144 |
+
/// - GameInfo+0xF348 (byte): sound mute flag
|
| 145 |
+
/// - DDGame+0x5CC (i32) vs GameInfo+0xF344 (i32): frame counter ≥ threshold
|
| 146 |
+
unsafe extern "thiscall" fn hook_is_sound_suppressed(ddgame: *mut DDGame) -> u32 {
|
| 147 |
+
let gi = (*ddgame).game_info as *const u8;
|
| 148 |
+
// Mute flag
|
| 149 |
+
if *gi.add(0xF348) != 0 {
|
| 150 |
+
return 1;
|
| 151 |
+
}
|
| 152 |
+
// Frame counter must be ≥ sound start threshold
|
| 153 |
+
let frame_counter = *(ddgame as *const u8).add(0x5CC).cast::<i32>();
|
| 154 |
+
let threshold = *gi.add(0xF344).cast::<i32>();
|
| 155 |
+
if frame_counter < threshold {
|
| 156 |
+
return 1;
|
| 157 |
+
}
|
| 158 |
+
0
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/// DispatchGlobalSound (0x526270) — fastcall(ECX=unused, EDX=task_turn_game) + 4 stack, RET 0x10.
|
| 162 |
+
/// Checks sound suppression, then calls DSSound vtable slot 3 (play_sound).
|
| 163 |
+
unsafe extern "fastcall" fn hook_dispatch_global_sound(
|
| 164 |
+
_ecx: u32,
|
| 165 |
+
task_turn_game: u32,
|
| 166 |
+
sound_id: u32,
|
| 167 |
+
flags: u32,
|
| 168 |
+
volume: u32,
|
| 169 |
+
pitch: u32,
|
| 170 |
+
) -> u32 {
|
| 171 |
+
let ddgame = *((task_turn_game as *const u8).add(0x488) as *const *mut DDGame);
|
| 172 |
+
let gi = (*ddgame).game_info as *const u8;
|
| 173 |
+
|
| 174 |
+
// Suppression check (same logic as IsSoundSuppressed but also different return for suppressed)
|
| 175 |
+
if *gi.add(0xF348) != 0 {
|
| 176 |
+
return 0xFFFF_FFFF; // -1
|
| 177 |
+
}
|
| 178 |
+
let frame_counter = *(ddgame as *const u8).add(0x5CC).cast::<i32>();
|
| 179 |
+
let threshold = *gi.add(0xF344).cast::<i32>();
|
| 180 |
+
if frame_counter < threshold {
|
| 181 |
+
return 0xFFFF_FFFF; // -1
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Get DSSound
|
| 185 |
+
let dssound = (*ddgame).sound;
|
| 186 |
+
if dssound.is_null() {
|
| 187 |
+
return 0;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Call DSSound vtable slot 3: play_sound(sound_id, flags, volume, pitch, 0)
|
| 191 |
+
let vtable = *(dssound as *const *const u32);
|
| 192 |
+
let play_fn: unsafe extern "thiscall" fn(*mut u8, u32, u32, u32, u32, u32) -> u32 =
|
| 193 |
+
core::mem::transmute(*vtable.add(3));
|
| 194 |
+
play_fn(dssound as *mut u8, sound_id, flags, volume, pitch, 0)
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/// PlaySoundPooled_Direct (0x546B50) — fastcall(ECX=unused, EDX=task) + 3 stack, RET 0xC.
|
| 198 |
+
/// Bypasses queue, checks suppression + fast-forward, calls DSSound vtable slot 4 directly.
|
| 199 |
+
unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
|
| 200 |
+
_ecx: u32,
|
| 201 |
+
task: u32,
|
| 202 |
+
param1: u32,
|
| 203 |
+
param2: u32,
|
| 204 |
+
param3: u32,
|
| 205 |
+
) -> u32 {
|
| 206 |
+
let ddgame = *((task as *const u8).add(0x2C) as *const *mut DDGame);
|
| 207 |
+
let gi = (*ddgame).game_info as *const u8;
|
| 208 |
+
|
| 209 |
+
// Suppression check
|
| 210 |
+
if *gi.add(0xF348) != 0 {
|
| 211 |
+
return 0xFFFF_FFFF;
|
| 212 |
+
}
|
| 213 |
+
let frame_counter = *(ddgame as *const u8).add(0x5CC).cast::<i32>();
|
| 214 |
+
let threshold = *gi.add(0xF344).cast::<i32>();
|
| 215 |
+
if frame_counter < threshold {
|
| 216 |
+
return 0xFFFF_FFFF;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// Fast-forward check (unique to this function)
|
| 220 |
+
let ff_active = *(ddgame as *const u8).add(0x98B0).cast::<i32>();
|
| 221 |
+
if ff_active != 0 {
|
| 222 |
+
return 0xFFFF_FFFF;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// Get DSSound
|
| 226 |
+
let dssound = (*ddgame).sound;
|
| 227 |
+
if dssound.is_null() {
|
| 228 |
+
return 0;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Call DSSound vtable slot 4: play_sound_pooled(param1, param2, 0x10000, param3, 0)
|
| 232 |
+
let vtable = *(dssound as *const *const u32);
|
| 233 |
+
let play_fn: unsafe extern "thiscall" fn(*mut u8, u32, u32, u32, u32, u32) -> u32 =
|
| 234 |
+
core::mem::transmute(*vtable.add(4));
|
| 235 |
+
play_fn(dssound as *mut u8, param1, param2, 0x10000, param3, 0)
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
// ============================================================
|
| 239 |
// Hook installation
|
| 240 |
// ============================================================
|
|
|
|
| 258 |
trampoline_play_sound_local as *const (),
|
| 259 |
)?;
|
| 260 |
|
| 261 |
+
// Sound dispatch helpers (bridge: queue → DSSound)
|
| 262 |
+
let _ = hook::install(
|
| 263 |
+
"IsSoundSuppressed",
|
| 264 |
+
va::IS_SOUND_SUPPRESSED,
|
| 265 |
+
hook_is_sound_suppressed as *const (),
|
| 266 |
+
)?;
|
| 267 |
+
|
| 268 |
+
let _ = hook::install(
|
| 269 |
+
"DispatchGlobalSound",
|
| 270 |
+
va::DISPATCH_GLOBAL_SOUND,
|
| 271 |
+
hook_dispatch_global_sound as *const (),
|
| 272 |
+
)?;
|
| 273 |
+
|
| 274 |
+
let _ = hook::install(
|
| 275 |
+
"PlaySoundPooled_Direct",
|
| 276 |
+
va::PLAY_SOUND_POOLED_DIRECT,
|
| 277 |
+
hook_play_sound_pooled_direct as *const (),
|
| 278 |
+
)?;
|
| 279 |
+
|
| 280 |
// Patch DSSound vtable: replace trivial slots with Rust implementations.
|
| 281 |
patch_dssound_vtable()?;
|
| 282 |
}
|
crates/openwa-wormkit/src/validation/mod.rs
CHANGED
|
@@ -112,7 +112,7 @@ fn validate_addresses(result: &mut ValidationResult) {
|
|
| 112 |
("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE),
|
| 113 |
("PCLandscape vtable", va::PC_LANDSCAPE_VTABLE),
|
| 114 |
("LandscapeShader vtable", va::LANDSCAPE_SHADER_VTABLE),
|
| 115 |
-
|
| 116 |
("TaskStateMachine vtable", va::TASK_STATE_MACHINE_VTABLE),
|
| 117 |
("OpenGLCPU vtable", va::OPENGL_CPU_VTABLE),
|
| 118 |
("WaterEffect vtable", va::WATER_EFFECT_VTABLE),
|
|
|
|
| 112 |
("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE),
|
| 113 |
("PCLandscape vtable", va::PC_LANDSCAPE_VTABLE),
|
| 114 |
("LandscapeShader vtable", va::LANDSCAPE_SHADER_VTABLE),
|
| 115 |
+
// DSSound vtable excluded: all 24 slots patched to Rust function pointers
|
| 116 |
("TaskStateMachine vtable", va::TASK_STATE_MACHINE_VTABLE),
|
| 117 |
("OpenGLCPU vtable", va::OPENGL_CPU_VTABLE),
|
| 118 |
("WaterEffect vtable", va::WATER_EFFECT_VTABLE),
|