fix: SpeechSlotTable off-by-one, add DSSound channel dump hotkey
Browse filesFix 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
|
| 245 |
-
/// `pool_id` is 1-based (1..64). Returns
|
|
|
|
|
|
|
| 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 |
-
|
| 254 |
-
|
| 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,
|
| 183 |
-
//
|
|
|
|
| 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:
|
| 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 |
}
|