refactor: add typed fields for sound dispatch, remove pointer arithmetic
Browse filesAdd DDGame.frame_counter (0x5CC) and GameInfo sound_start_frame
(0xF344) / sound_mute (0xF348) as proper struct fields. Use typed
pointers in all hook signatures instead of u32 casts — 32-bit target
means pointers and u32 are interchangeable in ABI.
Also remove DSSound vtable from validation (all 24 slots are Rust).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/engine/ddgame.rs
CHANGED
|
@@ -120,13 +120,25 @@ pub struct DDGame {
|
|
| 120 |
/// 0x54C: CTaskLand pointer (set by CTaskLand__InitLandscape at 0x5056F0).
|
| 121 |
/// The landscape/terrain task. Vtable at 0x664388.
|
| 122 |
pub task_land: *mut u8,
|
| 123 |
-
/// 0x550-
|
| 124 |
///
|
| 125 |
/// Runtime observations (not yet linked to code):
|
| 126 |
/// - 0x5C4: value matches code address 0x5755D0 (fixed-point normalize fn)
|
| 127 |
-
/// - 0x5C8-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
/// - 0x600-0x25FF: identity permutation [0,1,2,...,~2048] — purpose unknown
|
| 129 |
-
pub
|
| 130 |
|
| 131 |
/// 0x2600-0x2DFF: Block of 0xFFFFFFFF values at runtime (512 i32 entries).
|
| 132 |
/// May be unused slots in a parallel table to the 0x600 permutation.
|
|
|
|
| 120 |
/// 0x54C: CTaskLand pointer (set by CTaskLand__InitLandscape at 0x5056F0).
|
| 121 |
/// The landscape/terrain task. Vtable at 0x664388.
|
| 122 |
pub task_land: *mut u8,
|
| 123 |
+
/// 0x550-0x5CB: Unknown.
|
| 124 |
///
|
| 125 |
/// Runtime observations (not yet linked to code):
|
| 126 |
/// - 0x5C4: value matches code address 0x5755D0 (fixed-point normalize fn)
|
| 127 |
+
/// - 0x5C8-0x5CB: config-like values
|
| 128 |
+
pub _unknown_550: [u8; 0x5CC - 0x550],
|
| 129 |
+
|
| 130 |
+
/// 0x5CC: Game frame counter. Incremented at end of each frame in
|
| 131 |
+
/// GameFrameDispatcher. Compared against GameInfo+0xF344 (sound start
|
| 132 |
+
/// threshold) by IsSoundSuppressed and DispatchGlobalSound.
|
| 133 |
+
pub frame_counter: i32,
|
| 134 |
+
|
| 135 |
+
/// 0x5D0-0x25FF: Large unverified region.
|
| 136 |
+
///
|
| 137 |
+
/// Runtime observations:
|
| 138 |
+
/// - 0x5D0: frame processing flag (set 0/1 by GameFrameEndProcessor)
|
| 139 |
+
/// - 0x5D8-0x5FF: small config-like values (2048, 150, 3000, 696, 896, 100, 300)
|
| 140 |
/// - 0x600-0x25FF: identity permutation [0,1,2,...,~2048] — purpose unknown
|
| 141 |
+
pub _unknown_5d0: [u8; 0x2600 - 0x5D0],
|
| 142 |
|
| 143 |
/// 0x2600-0x2DFF: Block of 0xFFFFFFFF values at runtime (512 i32 entries).
|
| 144 |
/// May be unused slots in a parallel table to the 0x600 permutation.
|
crates/openwa-core/src/engine/game_info.rs
CHANGED
|
@@ -46,8 +46,20 @@ pub struct GameInfo {
|
|
| 46 |
/// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
|
| 47 |
pub land_dat_path: [u8; 14],
|
| 48 |
|
| 49 |
-
/// 0xDAFA-
|
| 50 |
-
pub _unknown_dafa: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
/// 0xF374: Display flags passed to DDDisplay::Init.
|
| 53 |
pub display_flags: u32,
|
|
|
|
| 46 |
/// 0xDAEC: Land data path ("data\land.dat", 14 bytes incl. null)
|
| 47 |
pub land_dat_path: [u8; 14],
|
| 48 |
|
| 49 |
+
/// 0xDAFA-0xF343: Unknown
|
| 50 |
+
pub _unknown_dafa: [u8; 0xF344 - 0xDAFA],
|
| 51 |
+
|
| 52 |
+
/// 0xF344: Sound start frame threshold (i32). Sound is suppressed when
|
| 53 |
+
/// DDGame.frame_counter < this value. Checked by IsSoundSuppressed and
|
| 54 |
+
/// DispatchGlobalSound.
|
| 55 |
+
pub sound_start_frame: i32,
|
| 56 |
+
|
| 57 |
+
/// 0xF348: Sound mute flag (byte). Nonzero = all sound suppressed.
|
| 58 |
+
/// Checked by IsSoundSuppressed, DispatchGlobalSound, PlaySoundPooled_Direct.
|
| 59 |
+
pub sound_mute: u8,
|
| 60 |
+
|
| 61 |
+
/// 0xF349-0xF373: Unknown
|
| 62 |
+
pub _unknown_f349: [u8; 0xF374 - 0xF349],
|
| 63 |
|
| 64 |
/// 0xF374: Display flags passed to DDDisplay::Init.
|
| 65 |
pub display_flags: u32,
|
crates/openwa-wormkit/src/replacements/sound.rs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
| 10 |
use std::sync::atomic::Ordering;
|
| 11 |
|
| 12 |
use openwa_core::address::va;
|
| 13 |
-
use openwa_core::engine::{DDGame, SoundQueueEntry};
|
| 14 |
use openwa_core::audio::SoundId;
|
| 15 |
-
use openwa_core::task::CGameTask;
|
| 16 |
|
| 17 |
use crate::hook;
|
| 18 |
use crate::log_line;
|
|
@@ -63,7 +63,7 @@ unsafe fn queue_sound(
|
|
| 63 |
// __thiscall: ECX = CTask* this, 4 stack params, RET 0x10
|
| 64 |
|
| 65 |
unsafe extern "thiscall" fn hook_play_sound_global(
|
| 66 |
-
this:
|
| 67 |
sound_id: u32,
|
| 68 |
flags: u32,
|
| 69 |
volume: u32,
|
|
@@ -74,13 +74,12 @@ unsafe extern "thiscall" fn hook_play_sound_global(
|
|
| 74 |
.map(|s| format!("{s:?}"))
|
| 75 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 76 |
let _ = log_line(&format!(
|
| 77 |
-
"[Sound] Global: task=0x{this:08X} id={sound_id}({sound_name}) \
|
| 78 |
p3={flags} p4={volume} p5={pitch}"
|
| 79 |
));
|
| 80 |
}
|
| 81 |
|
| 82 |
-
|
| 83 |
-
queue_sound(task.base.ddgame, sound_id, flags, volume, pitch).is_some() as u32
|
| 84 |
}
|
| 85 |
|
| 86 |
// ============================================================
|
|
@@ -95,7 +94,7 @@ hook::usercall_trampoline!(fn trampoline_play_sound_local; impl_fn = play_sound_
|
|
| 95 |
unsafe extern "cdecl" fn play_sound_local_impl(
|
| 96 |
pitch: u32,
|
| 97 |
volume: u32,
|
| 98 |
-
task:
|
| 99 |
sound_id: u32,
|
| 100 |
flags: u32,
|
| 101 |
) -> u32 {
|
|
@@ -104,12 +103,12 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 104 |
.map(|s| format!("{s:?}"))
|
| 105 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 106 |
let _ = log_line(&format!(
|
| 107 |
-
"[Sound] Local: eax={pitch} ecx=0x{volume:08X} task=0x{task:08X} \
|
| 108 |
id={sound_id}({sound_name}) flags={flags}"
|
| 109 |
));
|
| 110 |
}
|
| 111 |
|
| 112 |
-
let gt = &*
|
| 113 |
let ddgame = gt.base.ddgame;
|
| 114 |
let entry = match queue_sound(ddgame, sound_id, flags, volume, pitch) {
|
| 115 |
Some(e) => e,
|
|
@@ -127,8 +126,7 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 127 |
((*emitter.vtable).get_position)(emitter, &mut (*entry).pos_x, &mut (*entry).pos_y);
|
| 128 |
|
| 129 |
// Increment local sound count
|
| 130 |
-
|
| 131 |
-
gt_mut.sound_emitter.local_sound_count += 1;
|
| 132 |
|
| 133 |
1
|
| 134 |
}
|
|
@@ -139,50 +137,37 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 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
|
| 148 |
-
|
| 149 |
-
if
|
| 150 |
return 1;
|
| 151 |
}
|
| 152 |
-
|
| 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=
|
| 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 |
-
|
| 166 |
sound_id: u32,
|
| 167 |
flags: u32,
|
| 168 |
volume: u32,
|
| 169 |
pitch: u32,
|
| 170 |
) -> u32 {
|
| 171 |
-
let
|
| 172 |
-
let gi =
|
| 173 |
|
| 174 |
-
// Suppression check
|
| 175 |
-
if
|
| 176 |
-
return 0xFFFF_FFFF;
|
| 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 |
-
|
| 185 |
-
let dssound = (*ddgame).sound;
|
| 186 |
if dssound.is_null() {
|
| 187 |
return 0;
|
| 188 |
}
|
|
@@ -198,32 +183,25 @@ unsafe extern "fastcall" fn hook_dispatch_global_sound(
|
|
| 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:
|
| 202 |
param1: u32,
|
| 203 |
param2: u32,
|
| 204 |
param3: u32,
|
| 205 |
) -> u32 {
|
| 206 |
-
let
|
| 207 |
-
let gi =
|
| 208 |
|
| 209 |
// Suppression check
|
| 210 |
-
if
|
| 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 |
-
|
| 221 |
-
if ff_active != 0 {
|
| 222 |
return 0xFFFF_FFFF;
|
| 223 |
}
|
| 224 |
|
| 225 |
-
|
| 226 |
-
let dssound = (*ddgame).sound;
|
| 227 |
if dssound.is_null() {
|
| 228 |
return 0;
|
| 229 |
}
|
|
|
|
| 10 |
use std::sync::atomic::Ordering;
|
| 11 |
|
| 12 |
use openwa_core::address::va;
|
| 13 |
+
use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
|
| 14 |
use openwa_core::audio::SoundId;
|
| 15 |
+
use openwa_core::task::{CGameTask, CTask};
|
| 16 |
|
| 17 |
use crate::hook;
|
| 18 |
use crate::log_line;
|
|
|
|
| 63 |
// __thiscall: ECX = CTask* this, 4 stack params, RET 0x10
|
| 64 |
|
| 65 |
unsafe extern "thiscall" fn hook_play_sound_global(
|
| 66 |
+
this: *const CGameTask,
|
| 67 |
sound_id: u32,
|
| 68 |
flags: u32,
|
| 69 |
volume: u32,
|
|
|
|
| 74 |
.map(|s| format!("{s:?}"))
|
| 75 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 76 |
let _ = log_line(&format!(
|
| 77 |
+
"[Sound] Global: task=0x{this:08X?} id={sound_id}({sound_name}) \
|
| 78 |
p3={flags} p4={volume} p5={pitch}"
|
| 79 |
));
|
| 80 |
}
|
| 81 |
|
| 82 |
+
queue_sound((*this).base.ddgame, sound_id, flags, volume, pitch).is_some() as u32
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
// ============================================================
|
|
|
|
| 94 |
unsafe extern "cdecl" fn play_sound_local_impl(
|
| 95 |
pitch: u32,
|
| 96 |
volume: u32,
|
| 97 |
+
task: *mut CGameTask,
|
| 98 |
sound_id: u32,
|
| 99 |
flags: u32,
|
| 100 |
) -> u32 {
|
|
|
|
| 103 |
.map(|s| format!("{s:?}"))
|
| 104 |
.unwrap_or_else(|v| format!("#{v}"));
|
| 105 |
let _ = log_line(&format!(
|
| 106 |
+
"[Sound] Local: eax={pitch} ecx=0x{volume:08X} task=0x{task:08X?} \
|
| 107 |
id={sound_id}({sound_name}) flags={flags}"
|
| 108 |
));
|
| 109 |
}
|
| 110 |
|
| 111 |
+
let gt = &*task;
|
| 112 |
let ddgame = gt.base.ddgame;
|
| 113 |
let entry = match queue_sound(ddgame, sound_id, flags, volume, pitch) {
|
| 114 |
Some(e) => e,
|
|
|
|
| 126 |
((*emitter.vtable).get_position)(emitter, &mut (*entry).pos_x, &mut (*entry).pos_y);
|
| 127 |
|
| 128 |
// Increment local sound count
|
| 129 |
+
(*task).sound_emitter.local_sound_count += 1;
|
|
|
|
| 130 |
|
| 131 |
1
|
| 132 |
}
|
|
|
|
| 137 |
|
| 138 |
/// IsSoundSuppressed (0x5261E0) — thiscall(ECX=DDGame*), plain RET.
|
| 139 |
/// Returns 0 if sound playback is allowed, 1 if suppressed.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
unsafe extern "thiscall" fn hook_is_sound_suppressed(ddgame: *mut DDGame) -> u32 {
|
| 141 |
+
let g = &*ddgame;
|
| 142 |
+
let gi = &*g.game_info;
|
| 143 |
+
if gi.sound_mute != 0 {
|
| 144 |
return 1;
|
| 145 |
}
|
| 146 |
+
if g.frame_counter < gi.sound_start_frame {
|
|
|
|
|
|
|
|
|
|
| 147 |
return 1;
|
| 148 |
}
|
| 149 |
0
|
| 150 |
}
|
| 151 |
|
| 152 |
+
/// DispatchGlobalSound (0x526270) — fastcall(ECX=unused, EDX=ddgame_wrapper) + 4 stack, RET 0x10.
|
| 153 |
/// Checks sound suppression, then calls DSSound vtable slot 3 (play_sound).
|
| 154 |
unsafe extern "fastcall" fn hook_dispatch_global_sound(
|
| 155 |
_ecx: u32,
|
| 156 |
+
ddgame_wrapper: *const DDGameWrapper,
|
| 157 |
sound_id: u32,
|
| 158 |
flags: u32,
|
| 159 |
volume: u32,
|
| 160 |
pitch: u32,
|
| 161 |
) -> u32 {
|
| 162 |
+
let g = &*(*ddgame_wrapper).ddgame;
|
| 163 |
+
let gi = &*g.game_info;
|
| 164 |
|
| 165 |
+
// Suppression check — returns -1 if muted or before sound start frame
|
| 166 |
+
if gi.sound_mute != 0 || g.frame_counter < gi.sound_start_frame {
|
| 167 |
+
return 0xFFFF_FFFF;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
|
| 170 |
+
let dssound = g.sound;
|
|
|
|
| 171 |
if dssound.is_null() {
|
| 172 |
return 0;
|
| 173 |
}
|
|
|
|
| 183 |
/// Bypasses queue, checks suppression + fast-forward, calls DSSound vtable slot 4 directly.
|
| 184 |
unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
|
| 185 |
_ecx: u32,
|
| 186 |
+
task: *const CTask,
|
| 187 |
param1: u32,
|
| 188 |
param2: u32,
|
| 189 |
param3: u32,
|
| 190 |
) -> u32 {
|
| 191 |
+
let g = &*(*task).ddgame;
|
| 192 |
+
let gi = &*g.game_info;
|
| 193 |
|
| 194 |
// Suppression check
|
| 195 |
+
if gi.sound_mute != 0 || g.frame_counter < gi.sound_start_frame {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
return 0xFFFF_FFFF;
|
| 197 |
}
|
| 198 |
|
| 199 |
// Fast-forward check (unique to this function)
|
| 200 |
+
if g.fast_forward_active != 0 {
|
|
|
|
| 201 |
return 0xFFFF_FFFF;
|
| 202 |
}
|
| 203 |
|
| 204 |
+
let dssound = g.sound;
|
|
|
|
| 205 |
if dssound.is_null() {
|
| 206 |
return 0;
|
| 207 |
}
|