paavohtl Claude Opus 4.6 (1M context) commited on
Commit
cbd02f2
·
1 Parent(s): 8c437b9

feat: convert sound dispatch helpers and fix ActiveSoundEntry fields

Browse files

Replace 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: Number of active entries (max 64).
19
- pub count: u32,
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: DSSound channel/slot index (1-based).
43
- pub channel_index: u32,
44
- /// 0x14: Flags (0x40 observed on actively emitting entry).
45
- pub flags: u32,
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
- ("DSSound vtable", va::DS_SOUND_VTABLE),
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),