fix: correct SpeechSlotTable off-by-one, add sub_destructor (slot 14)
Browse filesSpeechSlotTable::get/set subtracted 1 from line_id, but WA uses the
raw ID directly as the array index (1-based, slot 0 unused per team) β
same pattern as channel_slots. This caused voice lines to be stored and
looked up at the wrong index, playing wrong speech for each event.
Add DSSound vtable slot 14 (sub_destructor) in Rust β sets secondary
vtable and optionally frees.
Add ChannelDescriptor _field_00 at +0x00 and shift ds_buffer to +0x14
to match original code's piVar4 indexing. Slots 1, 9, 10, 11 remain
disabled pending runtime verification of the full descriptor layout.
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
|
@@ -21,19 +21,19 @@ type Ptr32 = u32;
|
|
| 21 |
#[repr(C)]
|
| 22 |
#[derive(Clone, Copy)]
|
| 23 |
pub struct ChannelDescriptor {
|
| 24 |
-
/// Flags
|
|
|
|
|
|
|
| 25 |
pub flags_a: i32,
|
| 26 |
-
/// Flags B (init -1,
|
| 27 |
pub flags_b: i32,
|
| 28 |
-
/// Volume
|
| 29 |
-
pub
|
| 30 |
-
/// Volume
|
| 31 |
-
pub
|
| 32 |
-
/// IDirectSoundBuffer* for the active buffer
|
| 33 |
-
/// Use `
|
| 34 |
pub ds_buffer: Ptr32,
|
| 35 |
-
/// Pool index tracking which buffer pool entry this channel uses
|
| 36 |
-
pub pool_index: u32,
|
| 37 |
}
|
| 38 |
|
| 39 |
const _: () = assert!(core::mem::size_of::<ChannelDescriptor>() == 0x18);
|
|
@@ -193,11 +193,11 @@ pub unsafe extern "thiscall" fn update_channels(this: *mut DSSound) {
|
|
| 193 |
if let Some(buf) = desc.buffer() {
|
| 194 |
if let Ok(status) = buf.GetStatus() {
|
| 195 |
// If not playing (bit 0 clear) and flags_b < 0, release.
|
| 196 |
-
if (status & 1) == 0 && desc.
|
| 197 |
// Take and drop to release COM ref.
|
| 198 |
desc.take_buffer();
|
| 199 |
-
desc.
|
| 200 |
-
desc.
|
| 201 |
desc.flags_b = -1;
|
| 202 |
desc.flags_a = -1;
|
| 203 |
}
|
|
@@ -213,10 +213,10 @@ pub unsafe extern "thiscall" fn release_finished(this: *mut DSSound) -> i32 {
|
|
| 213 |
for desc in &mut snd.channel_descs {
|
| 214 |
if let Some(buf) = desc.buffer() {
|
| 215 |
if let Ok(status) = buf.GetStatus() {
|
| 216 |
-
if (status & 1) == 0 && desc.
|
| 217 |
desc.take_buffer();
|
| 218 |
-
desc.
|
| 219 |
-
desc.
|
| 220 |
desc.flags_b = -1;
|
| 221 |
desc.flags_a = -1;
|
| 222 |
count += 1;
|
|
@@ -291,8 +291,8 @@ pub unsafe extern "thiscall" fn stop_channel(
|
|
| 291 |
}
|
| 292 |
|
| 293 |
// Reset descriptor to free state.
|
| 294 |
-
desc.
|
| 295 |
-
desc.
|
| 296 |
desc.flags_b = -1;
|
| 297 |
desc.flags_a = -1;
|
| 298 |
|
|
|
|
| 21 |
#[repr(C)]
|
| 22 |
#[derive(Clone, Copy)]
|
| 23 |
pub struct ChannelDescriptor {
|
| 24 |
+
/// +0x00: Flags/state field (not set by constructor init helper)
|
| 25 |
+
pub _field_00: u32,
|
| 26 |
+
/// +0x04: Flags A (init -1, -1 = unused/free, checked for < 0)
|
| 27 |
pub flags_a: i32,
|
| 28 |
+
/// +0x08: Flags B (init -1, set to -1 on release)
|
| 29 |
pub flags_b: i32,
|
| 30 |
+
/// +0x0C: Volume/state field (cleared to 0 on release)
|
| 31 |
+
pub _field_0c: i32,
|
| 32 |
+
/// +0x10: Volume/state field (cleared to 0 on release)
|
| 33 |
+
pub _field_10: i32,
|
| 34 |
+
/// +0x14: IDirectSoundBuffer* for the active buffer (0 = empty).
|
| 35 |
+
/// At absolute offset this+0x28 for desc[0]. Use `buffer()` to get a typed ref.
|
| 36 |
pub ds_buffer: Ptr32,
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
const _: () = assert!(core::mem::size_of::<ChannelDescriptor>() == 0x18);
|
|
|
|
| 193 |
if let Some(buf) = desc.buffer() {
|
| 194 |
if let Ok(status) = buf.GetStatus() {
|
| 195 |
// If not playing (bit 0 clear) and flags_b < 0, release.
|
| 196 |
+
if (status & 1) == 0 && desc.flags_a < 0 {
|
| 197 |
// Take and drop to release COM ref.
|
| 198 |
desc.take_buffer();
|
| 199 |
+
desc._field_0c = 0;
|
| 200 |
+
desc._field_10 = 0;
|
| 201 |
desc.flags_b = -1;
|
| 202 |
desc.flags_a = -1;
|
| 203 |
}
|
|
|
|
| 213 |
for desc in &mut snd.channel_descs {
|
| 214 |
if let Some(buf) = desc.buffer() {
|
| 215 |
if let Ok(status) = buf.GetStatus() {
|
| 216 |
+
if (status & 1) == 0 && desc.flags_a < 0 {
|
| 217 |
desc.take_buffer();
|
| 218 |
+
desc._field_0c = 0;
|
| 219 |
+
desc._field_10 = 0;
|
| 220 |
desc.flags_b = -1;
|
| 221 |
desc.flags_a = -1;
|
| 222 |
count += 1;
|
|
|
|
| 291 |
}
|
| 292 |
|
| 293 |
// Reset descriptor to free state.
|
| 294 |
+
desc._field_0c = 0;
|
| 295 |
+
desc._field_10 = 0;
|
| 296 |
desc.flags_b = -1;
|
| 297 |
desc.flags_a = -1;
|
| 298 |
|
crates/openwa-core/src/audio/speech.rs
CHANGED
|
@@ -295,12 +295,10 @@ impl SpeechSlotTable {
|
|
| 295 |
/// Get the DSSound buffer index for a team's speech line.
|
| 296 |
///
|
| 297 |
/// `team` is 0-indexed (0..6), `line_id` is the raw speech line ID (1-56).
|
|
|
|
| 298 |
/// Returns 0 if the slot is empty (no WAV loaded for this line).
|
| 299 |
pub fn get(&self, team: usize, line_id: u32) -> u32 {
|
| 300 |
-
|
| 301 |
-
return 0;
|
| 302 |
-
}
|
| 303 |
-
let idx = team * Self::LINES_PER_TEAM + (line_id as usize - 1);
|
| 304 |
if idx < self.0.len() {
|
| 305 |
self.0[idx]
|
| 306 |
} else {
|
|
@@ -309,11 +307,9 @@ impl SpeechSlotTable {
|
|
| 309 |
}
|
| 310 |
|
| 311 |
/// Set the DSSound buffer index for a team's speech line.
|
|
|
|
| 312 |
pub fn set(&mut self, team: usize, line_id: u32, slot: u32) {
|
| 313 |
-
|
| 314 |
-
return;
|
| 315 |
-
}
|
| 316 |
-
let idx = team * Self::LINES_PER_TEAM + (line_id as usize - 1);
|
| 317 |
if idx < self.0.len() {
|
| 318 |
self.0[idx] = slot;
|
| 319 |
}
|
|
|
|
| 295 |
/// Get the DSSound buffer index for a team's speech line.
|
| 296 |
///
|
| 297 |
/// `team` is 0-indexed (0..6), `line_id` is the raw speech line ID (1-56).
|
| 298 |
+
/// Index is used directly (1-based, slot 0 per team unused).
|
| 299 |
/// Returns 0 if the slot is empty (no WAV loaded for this line).
|
| 300 |
pub fn get(&self, team: usize, line_id: u32) -> u32 {
|
| 301 |
+
let idx = team * Self::LINES_PER_TEAM + line_id as usize;
|
|
|
|
|
|
|
|
|
|
| 302 |
if idx < self.0.len() {
|
| 303 |
self.0[idx]
|
| 304 |
} else {
|
|
|
|
| 307 |
}
|
| 308 |
|
| 309 |
/// Set the DSSound buffer index for a team's speech line.
|
| 310 |
+
/// Index is used directly (1-based, slot 0 per team unused).
|
| 311 |
pub fn set(&mut self, team: usize, line_id: u32, slot: u32) {
|
| 312 |
+
let idx = team * Self::LINES_PER_TEAM + line_id as usize;
|
|
|
|
|
|
|
|
|
|
| 313 |
if idx < self.0.len() {
|
| 314 |
self.0[idx] = slot;
|
| 315 |
}
|
crates/openwa-wormkit/src/replacements/sound.rs
CHANGED
|
@@ -179,16 +179,11 @@ 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 |
-
//
|
|
|
|
| 183 |
// *vt.add(1) = update_channels as *const () as u32;
|
| 184 |
-
|
| 185 |
-
//
|
| 186 |
-
*vt.add(9) = is_channel_playing as *const () as u32;
|
| 187 |
-
|
| 188 |
-
// Slot 10: stop_channel β Stop, Release, return to pool
|
| 189 |
-
*vt.add(10) = stop_channel as *const () as u32;
|
| 190 |
-
|
| 191 |
-
// Slot 11: release_finished β TODO: same issue as slot 1
|
| 192 |
// *vt.add(11) = release_finished as *const () as u32;
|
| 193 |
|
| 194 |
// Slot 13: is_slot_loaded β channel_slots check
|
|
@@ -209,6 +204,6 @@ unsafe fn patch_dssound_vtable() -> Result<(), String> {
|
|
| 209 |
// Trivial returns-1 (slot 23)
|
| 210 |
*vt.add(23) = dssound_returns_1 as *const () as u32;
|
| 211 |
|
| 212 |
-
let _ = log_line("[Sound] DSSound vtable: patched
|
| 213 |
}).map_err(|e| e.to_string())
|
| 214 |
}
|
|
|
|
| 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
|
|
|
|
| 204 |
// Trivial returns-1 (slot 23)
|
| 205 |
*vt.add(23) = dssound_returns_1 as *const () as u32;
|
| 206 |
|
| 207 |
+
let _ = log_line("[Sound] DSSound vtable: patched 13/24 slots with Rust");
|
| 208 |
}).map_err(|e| e.to_string())
|
| 209 |
}
|