paavohtl Claude Opus 4.6 (1M context) commited on
Commit
5ce5ac6
Β·
1 Parent(s): cf29bed

fix: SpeechSlotTable off-by-one, add DSSound channel dump hotkey

Browse files

Fix SpeechSlotTable::get/set to use line_id directly as array index
(1-based, slot 0 per team unused) matching WA's assembly. Previous
code subtracted 1, causing all voice lines to map to wrong slots.

Add F7 hotkey for runtime DSSound channel descriptor dumps to verify
field layouts. Runtime dumps confirmed ds_buffer at descriptor +0x14.

Add slot 14 (sub_destructor) to Rust. Slots 1, 9, 10, 11 remain
disabled β€” slot 9's return value semantics need caller analysis
(always returning 1 breaks sound scheduling).

13/24 DSSound vtable slots enabled in Rust.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/audio/dssound.rs CHANGED
@@ -241,8 +241,10 @@ pub unsafe extern "thiscall" fn sub_destructor(
241
  this
242
  }
243
 
244
- /// Slot 9: is_channel_playing β€” checks if a buffer pool entry is still playing.
245
- /// `pool_id` is 1-based (1..64). Returns 1 if playing or invalid, 0 if stopped.
 
 
246
  pub unsafe extern "thiscall" fn is_channel_playing(
247
  this: *mut DSSound, pool_id: i32,
248
  ) -> u8 {
@@ -250,21 +252,8 @@ pub unsafe extern "thiscall" fn is_channel_playing(
250
  if idx >= 64 {
251
  return 0;
252
  }
253
- let snd = &*this;
254
- let desc_idx = snd.buffer_pool_shadow[idx];
255
- if desc_idx < 0 {
256
- return 0;
257
- }
258
- let desc = &snd.channel_descs[desc_idx as usize];
259
- let Some(buf) = desc.buffer() else { return 0 };
260
- let status = buf.GetStatus();
261
- if let Ok(s) = status {
262
- if (s & 1) != 0 {
263
- // DSBSTATUS_PLAYING = 1
264
- return 1;
265
- }
266
- }
267
- 1 // original returns 1 even on error
268
  }
269
 
270
  /// Slot 10: stop_channel β€” stops a buffer, releases it, and returns the pool entry.
@@ -278,7 +267,7 @@ pub unsafe extern "thiscall" fn stop_channel(
278
  }
279
  let snd = &mut *this;
280
  let desc_idx = snd.buffer_pool_shadow[idx];
281
- if desc_idx < 0 {
282
  return 0;
283
  }
284
  let di = desc_idx as usize;
 
241
  this
242
  }
243
 
244
+ /// Slot 9: is_channel_playing β€” checks if a buffer pool entry's GetStatus succeeds.
245
+ /// `pool_id` is 1-based (1..64). Returns 0 only for out-of-range pool_id.
246
+ /// Original always returns 1 unless pool_id is invalid β€” it checks whether
247
+ /// GetStatus succeeds (S_OK), not the actual playing status bits.
248
  pub unsafe extern "thiscall" fn is_channel_playing(
249
  this: *mut DSSound, pool_id: i32,
250
  ) -> u8 {
 
252
  if idx >= 64 {
253
  return 0;
254
  }
255
+ // Original returns 1 in all other cases (found or not, playing or not).
256
+ 1
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  }
258
 
259
  /// Slot 10: stop_channel β€” stops a buffer, releases it, and returns the pool entry.
 
267
  }
268
  let snd = &mut *this;
269
  let desc_idx = snd.buffer_pool_shadow[idx];
270
+ if desc_idx < 0 || desc_idx as usize >= 8 {
271
  return 0;
272
  }
273
  let di = desc_idx as usize;
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -179,12 +179,18 @@ unsafe fn patch_dssound_vtable() -> Result<(), String> {
179
  // Slot 12: load_wav β€” WAV file β†’ DirectSound secondary buffer (hound + windows crate)
180
  *vt.add(12) = load_wav as *const () as u32;
181
 
182
- // Slots 1, 9, 10, 11 disabled β€” need descriptor field layout
183
- // verified with runtime memory dumps before enabling.
 
184
  // *vt.add(1) = update_channels as *const () as u32;
 
 
 
 
185
  // *vt.add(9) = is_channel_playing as *const () as u32;
 
 
186
  // *vt.add(10) = stop_channel as *const () as u32;
187
- // *vt.add(11) = release_finished as *const () as u32;
188
 
189
  // Slot 13: is_slot_loaded β€” channel_slots check
190
  *vt.add(13) = is_slot_loaded as *const () as u32;
 
179
  // Slot 12: load_wav β€” WAV file β†’ DirectSound secondary buffer (hound + windows crate)
180
  *vt.add(12) = load_wav as *const () as u32;
181
 
182
+ // Slots 1, 11 disabled β€” update_channels/release_finished crash
183
+ // when iterating descriptors. Layout is confirmed correct via
184
+ // runtime dumps; issue is likely COM refcount handling in buffer().
185
  // *vt.add(1) = update_channels as *const () as u32;
186
+ // *vt.add(11) = release_finished as *const () as u32;
187
+
188
+ // Slots 9, 10 disabled β€” slot 9's return value semantics are unclear
189
+ // from decompile; wrong values break sound scheduling entirely.
190
  // *vt.add(9) = is_channel_playing as *const () as u32;
191
+
192
+ // Slot 10: disabled pending slot 9 fix
193
  // *vt.add(10) = stop_channel as *const () as u32;
 
194
 
195
  // Slot 13: is_slot_loaded β€” channel_slots check
196
  *vt.add(13) = is_slot_loaded as *const () as u32;
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -1438,6 +1438,57 @@ pub fn run() -> Result<(), String> {
1438
  Ok(())
1439
  }
1440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1441
  /// Start the debug hotkey listener thread.
1442
  ///
1443
  /// F8 = CTaskMissile dump (fire a weapon first, then press F8 while missile is in flight).
@@ -1451,11 +1502,13 @@ pub fn start_hotkeys() {
1451
 
1452
  std::thread::spawn(|| {
1453
  use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
 
1454
  const VK_F8: i32 = 0x77;
1455
- let _ = log_validation(" Hotkeys: F8=missile dump (fire weapon, then press F8 while missile is in flight)");
1456
  loop {
1457
  std::thread::sleep(std::time::Duration::from_millis(100));
1458
  unsafe {
 
1459
  if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_missile_tasks(); }
1460
  }
1461
  }
 
1438
  Ok(())
1439
  }
1440
 
1441
+ /// Dump DSSound channel descriptor raw bytes for field layout verification.
1442
+ unsafe fn dump_dssound_channels() {
1443
+ use openwa_core::address::va;
1444
+ use openwa_core::rebase::rb;
1445
+ use openwa_core::engine::game_session::GameSession;
1446
+ use openwa_core::audio::DSSound;
1447
+
1448
+ let session_ptr = rb(va::G_GAME_SESSION) as *const *const GameSession;
1449
+ let session = *session_ptr;
1450
+ if session.is_null() {
1451
+ let _ = log_validation("[DSSound] No session");
1452
+ return;
1453
+ }
1454
+ let sound = (*session).sound;
1455
+ if sound.is_null() {
1456
+ let _ = log_validation("[DSSound] No sound");
1457
+ return;
1458
+ }
1459
+ let snd = &*sound;
1460
+
1461
+ let _ = log_validation("=== DSSound Channel Descriptor Dump ===");
1462
+ let _ = log_validation(&format!(
1463
+ " DSSound at 0x{:08X}, vtable=0x{:08X}, volume={:?}",
1464
+ sound as u32, snd.vtable as u32, snd.volume
1465
+ ));
1466
+
1467
+ // Dump each descriptor as raw u32s
1468
+ let base = sound as *const u8;
1469
+ for i in 0..8 {
1470
+ let desc_offset = 0x14 + i * 0x18;
1471
+ let desc_ptr = base.add(desc_offset) as *const u32;
1472
+ let words: [u32; 6] = [
1473
+ *desc_ptr, *desc_ptr.add(1), *desc_ptr.add(2),
1474
+ *desc_ptr.add(3), *desc_ptr.add(4), *desc_ptr.add(5),
1475
+ ];
1476
+ let has_buffer = words[5] != 0; // ds_buffer at +0x14 = word[5]
1477
+ let _ = log_validation(&format!(
1478
+ " desc[{}] @+0x{:03X}: [{:08X} {:08X} {:08X} {:08X} {:08X} {:08X}]{}",
1479
+ i, desc_offset,
1480
+ words[0], words[1], words[2], words[3], words[4], words[5],
1481
+ if has_buffer { " <-- has buffer" } else { "" }
1482
+ ));
1483
+ }
1484
+
1485
+ // Also dump buffer pool state
1486
+ let _ = log_validation(&format!(
1487
+ " pool: free={}, used={}",
1488
+ snd.buffer_pool_free_count, snd.buffer_pool_used_count
1489
+ ));
1490
+ }
1491
+
1492
  /// Start the debug hotkey listener thread.
1493
  ///
1494
  /// F8 = CTaskMissile dump (fire a weapon first, then press F8 while missile is in flight).
 
1502
 
1503
  std::thread::spawn(|| {
1504
  use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
1505
+ const VK_F7: i32 = 0x76;
1506
  const VK_F8: i32 = 0x77;
1507
+ let _ = log_validation(" Hotkeys: F7=DSSound channel dump, F8=missile dump");
1508
  loop {
1509
  std::thread::sleep(std::time::Duration::from_millis(100));
1510
  unsafe {
1511
+ if GetAsyncKeyState(VK_F7) & 1 != 0 { dump_dssound_channels(); }
1512
  if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_missile_tasks(); }
1513
  }
1514
  }