feat: pure Rust DDGameWrapper__LoadSpeechWAV, typed struct fields
Browse filesReplace the naked asm bridge to DDGameWrapper__LoadSpeechWAV (0x571530)
with a pure Rust implementation that uses typed struct field access.
Add speech_name_table (360 x 0x40-byte entries at +0x14E8) and
speech_name_count (+0x6EE8) fields to DDGameWrapper, eliminating all
pointer arithmetic from the speech WAV loading path.
Rename address constant DSSOUND_LOAD_SPEECH_WAV to
DDGAMEWRAPPER_LOAD_SPEECH_WAV and update Ghidra label to match
(function operates on DDGameWrapper, not DSSound).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/address.rs
CHANGED
|
@@ -402,11 +402,12 @@ pub mod va {
|
|
| 402 |
/// usercall(EAX=DDGameWrapper) + 3 stack(team_index, speech_base_path, speech_dir), RET 0xC.
|
| 403 |
pub const DSSOUND_LOAD_SPEECH_BANK: u32 = 0x0057_1660;
|
| 404 |
|
| 405 |
-
///
|
| 406 |
-
/// Searches
|
|
|
|
| 407 |
/// usercall(ESI=DDGameWrapper) + 4 stack(team_index, line_id, wav_path, full_path), RET 0x10.
|
| 408 |
/// Returns 1 on success, 0 on failure.
|
| 409 |
-
pub const
|
| 410 |
|
| 411 |
/// Loads all 126 SFX WAVs from data\wav\Effects\.
|
| 412 |
/// stdcall(DDGame), RET 0x4.
|
|
|
|
| 402 |
/// usercall(EAX=DDGameWrapper) + 3 stack(team_index, speech_base_path, speech_dir), RET 0xC.
|
| 403 |
pub const DSSOUND_LOAD_SPEECH_BANK: u32 = 0x0057_1660;
|
| 404 |
|
| 405 |
+
/// DDGameWrapper__LoadSpeechWAV — loads a single speech WAV.
|
| 406 |
+
/// Searches DDGameWrapper's name table for existing buffer, reuses if found.
|
| 407 |
+
/// Otherwise calls DSSound vtable slot 12 (load_wav) to create new buffer.
|
| 408 |
/// usercall(ESI=DDGameWrapper) + 4 stack(team_index, line_id, wav_path, full_path), RET 0x10.
|
| 409 |
/// Returns 1 on success, 0 on failure.
|
| 410 |
+
pub const DDGAMEWRAPPER_LOAD_SPEECH_WAV: u32 = 0x0057_1530;
|
| 411 |
|
| 412 |
/// Loads all 126 SFX WAVs from data\wav\Effects\.
|
| 413 |
/// stdcall(DDGame), RET 0x4.
|
crates/openwa-core/src/engine/ddgame.rs
CHANGED
|
@@ -251,7 +251,7 @@ pub struct DDGame {
|
|
| 251 |
pub _unknown_77cc: [u8; 0x77E4 - 0x77CC],
|
| 252 |
|
| 253 |
/// 0x77E4: Speech slot table. Maps (team, speech_line_id) → DSSound buffer index.
|
| 254 |
-
/// Cleared by DSSound_LoadAllSpeechBanks (0x571A70), filled by
|
| 255 |
pub speech_slot_table: SpeechSlotTable,
|
| 256 |
|
| 257 |
/// 0x7D84-0x7E9F: Unknown
|
|
|
|
| 251 |
pub _unknown_77cc: [u8; 0x77E4 - 0x77CC],
|
| 252 |
|
| 253 |
/// 0x77E4: Speech slot table. Maps (team, speech_line_id) → DSSound buffer index.
|
| 254 |
+
/// Cleared by DSSound_LoadAllSpeechBanks (0x571A70), filled by DDGameWrapper__LoadSpeechWAV (0x571530).
|
| 255 |
pub speech_slot_table: SpeechSlotTable,
|
| 256 |
|
| 257 |
/// 0x7D84-0x7E9F: Unknown
|
crates/openwa-core/src/engine/ddgame_wrapper.rs
CHANGED
|
@@ -3,6 +3,11 @@ use crate::engine::ddgame::DDGame;
|
|
| 3 |
use crate::audio::dssound::DSSound;
|
| 4 |
use crate::render::landscape::PCLandscape;
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
/// DDGameWrapper — large wrapper around DDGame.
|
| 7 |
///
|
| 8 |
/// Created by DDGameWrapper__Constructor (0x56DEF0).
|
|
@@ -13,11 +18,7 @@ use crate::render::landscape::PCLandscape;
|
|
| 13 |
/// Note: Ghidra shows DWORD-indexed offsets (param_2[0x122] etc.).
|
| 14 |
/// Byte offset = dword_index * 4.
|
| 15 |
///
|
| 16 |
-
///
|
| 17 |
-
/// making this object at least ~28KB. Total size is not yet determined.
|
| 18 |
-
///
|
| 19 |
-
/// PARTIAL: Only confirmed fields are defined. The repr(C) struct uses
|
| 20 |
-
/// a conservative size that covers known fields. The actual object is larger.
|
| 21 |
#[repr(C)]
|
| 22 |
pub struct DDGameWrapper {
|
| 23 |
/// 0x000: Vtable pointer (0x66A30C)
|
|
@@ -48,13 +49,17 @@ pub struct DDGameWrapper {
|
|
| 48 |
pub _field_4dc: u32,
|
| 49 |
/// 0x4E0: Init -100 / 0xFFFFFF9C (DWORD index 0x138)
|
| 50 |
pub _field_4e0: u32,
|
| 51 |
-
/// 0x4E4-
|
| 52 |
-
pub _unknown_4e4: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
/// 0x6EEC: Init 0 (DWORD index 0x1BBA)
|
| 54 |
pub _field_6eec: u32,
|
| 55 |
/// 0x6EF0-end: Unknown trailing fields
|
| 56 |
pub _unknown_6ef0: [u8; 0x10],
|
| 57 |
}
|
| 58 |
|
| 59 |
-
// This is a minimum estimate. The actual object is likely larger.
|
| 60 |
const _: () = assert!(core::mem::size_of::<DDGameWrapper>() == 0x6F00);
|
|
|
|
| 3 |
use crate::audio::dssound::DSSound;
|
| 4 |
use crate::render::landscape::PCLandscape;
|
| 5 |
|
| 6 |
+
/// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
|
| 7 |
+
pub const SPEECH_NAME_ENTRY_SIZE: usize = 0x40;
|
| 8 |
+
/// Maximum number of speech name entries.
|
| 9 |
+
pub const SPEECH_NAME_TABLE_LEN: usize = 360;
|
| 10 |
+
|
| 11 |
/// DDGameWrapper — large wrapper around DDGame.
|
| 12 |
///
|
| 13 |
/// Created by DDGameWrapper__Constructor (0x56DEF0).
|
|
|
|
| 18 |
/// Note: Ghidra shows DWORD-indexed offsets (param_2[0x122] etc.).
|
| 19 |
/// Byte offset = dword_index * 4.
|
| 20 |
///
|
| 21 |
+
/// PARTIAL: Only confirmed fields are defined.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
#[repr(C)]
|
| 23 |
pub struct DDGameWrapper {
|
| 24 |
/// 0x000: Vtable pointer (0x66A30C)
|
|
|
|
| 49 |
pub _field_4dc: u32,
|
| 50 |
/// 0x4E0: Init -100 / 0xFFFFFF9C (DWORD index 0x138)
|
| 51 |
pub _field_4e0: u32,
|
| 52 |
+
/// 0x4E4-0x14E7: Unknown fields
|
| 53 |
+
pub _unknown_4e4: [u8; 0x14E8 - 0x4E4],
|
| 54 |
+
/// 0x14E8: Speech name table — 360 entries of 0x40-byte C strings.
|
| 55 |
+
/// Used by DDGameWrapper__LoadSpeechWAV to deduplicate loaded WAVs.
|
| 56 |
+
pub speech_name_table: [[u8; SPEECH_NAME_ENTRY_SIZE]; SPEECH_NAME_TABLE_LEN],
|
| 57 |
+
/// 0x6EE8: Number of entries used in speech_name_table.
|
| 58 |
+
pub speech_name_count: u32,
|
| 59 |
/// 0x6EEC: Init 0 (DWORD index 0x1BBA)
|
| 60 |
pub _field_6eec: u32,
|
| 61 |
/// 0x6EF0-end: Unknown trailing fields
|
| 62 |
pub _unknown_6ef0: [u8; 0x10],
|
| 63 |
}
|
| 64 |
|
|
|
|
| 65 |
const _: () = assert!(core::mem::size_of::<DDGameWrapper>() == 0x6F00);
|
crates/openwa-wormkit/src/replacements/speech.rs
CHANGED
|
@@ -238,39 +238,17 @@ unsafe extern "cdecl" fn play_fanfare_current_team_impl(index: u32) -> u32 {
|
|
| 238 |
}
|
| 239 |
|
| 240 |
// ============================================================
|
| 241 |
-
//
|
| 242 |
// ============================================================
|
| 243 |
-
//
|
| 244 |
-
//
|
|
|
|
| 245 |
|
| 246 |
-
///
|
| 247 |
-
///
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
_team_index: u32,
|
| 252 |
-
_line_id: u32,
|
| 253 |
-
_wav_path: *const u8,
|
| 254 |
-
_full_path: *const u8,
|
| 255 |
-
_func: u32,
|
| 256 |
-
) -> u32 {
|
| 257 |
-
// After push esi: ESP+0=saved_esi, +4=retaddr, +8=ddgw, +12=team,
|
| 258 |
-
// +16=line_id, +20=wav_path, +24=full_path, +28=func
|
| 259 |
-
core::arch::naked_asm!(
|
| 260 |
-
"push esi",
|
| 261 |
-
"mov esi, [esp + 8]", // ESI = ddgw
|
| 262 |
-
"push dword ptr [esp + 24]", // full_path (+4 shift each push)
|
| 263 |
-
"push dword ptr [esp + 24]", // wav_path
|
| 264 |
-
"push dword ptr [esp + 24]", // line_id
|
| 265 |
-
"push dword ptr [esp + 24]", // team_index
|
| 266 |
-
"call dword ptr [esp + 44]", // func (offset = 28 + 16 from 4 pushes)
|
| 267 |
-
// RET 0x10 cleans the 4 params; ESP back to after push esi
|
| 268 |
-
"pop esi",
|
| 269 |
-
"ret",
|
| 270 |
-
);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
/// Call DSSound_LoadSpeechWAV with the given parameters.
|
| 274 |
unsafe fn call_load_speech_wav(
|
| 275 |
ddgw: u32,
|
| 276 |
team_index: u32,
|
|
@@ -278,14 +256,55 @@ unsafe fn call_load_speech_wav(
|
|
| 278 |
wav_path: *const c_char,
|
| 279 |
full_path: *const c_char,
|
| 280 |
) -> u32 {
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
}
|
| 290 |
|
| 291 |
// ============================================================
|
|
|
|
| 238 |
}
|
| 239 |
|
| 240 |
// ============================================================
|
| 241 |
+
// DDGameWrapper__LoadSpeechWAV — pure Rust (replaces 0x571530)
|
| 242 |
// ============================================================
|
| 243 |
+
// Searches DDGameWrapper's speech name table for existing WAV, reuses slot if found.
|
| 244 |
+
// Otherwise calls DSSound vtable load_wav to load new WAV into a buffer.
|
| 245 |
+
// Stores the buffer slot index in the SpeechSlotTable.
|
| 246 |
|
| 247 |
+
/// Pure Rust replacement for DDGameWrapper__LoadSpeechWAV (0x571530).
|
| 248 |
+
///
|
| 249 |
+
/// Searches the name table for `wav_path`. If found, reuses the existing
|
| 250 |
+
/// buffer slot. Otherwise calls DSSound::load_wav to create a new buffer.
|
| 251 |
+
/// Returns 1 on success, 0 on failure.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
unsafe fn call_load_speech_wav(
|
| 253 |
ddgw: u32,
|
| 254 |
team_index: u32,
|
|
|
|
| 256 |
wav_path: *const c_char,
|
| 257 |
full_path: *const c_char,
|
| 258 |
) -> u32 {
|
| 259 |
+
use openwa_core::audio::SpeechSlotTable;
|
| 260 |
+
use openwa_core::engine::DDGameWrapper;
|
| 261 |
+
use openwa_core::engine::ddgame_wrapper::SPEECH_NAME_ENTRY_SIZE;
|
| 262 |
+
|
| 263 |
+
let wrapper = &mut *(ddgw as *mut DDGameWrapper);
|
| 264 |
+
let count = wrapper.speech_name_count as usize;
|
| 265 |
+
let search_name = core::ffi::CStr::from_ptr(wav_path).to_bytes();
|
| 266 |
+
|
| 267 |
+
// Search name table for existing entry matching wav_path.
|
| 268 |
+
let mut found_idx: Option<usize> = None;
|
| 269 |
+
for i in 0..count {
|
| 270 |
+
let entry = &wrapper.speech_name_table[i];
|
| 271 |
+
// Compare up to the null terminator.
|
| 272 |
+
let entry_len = entry.iter().position(|&b| b == 0).unwrap_or(entry.len());
|
| 273 |
+
if entry[..entry_len] == *search_name {
|
| 274 |
+
found_idx = Some(i);
|
| 275 |
+
break;
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
let ddgame = &mut *wrapper.ddgame;
|
| 280 |
+
let slot_table = &mut ddgame.speech_slot_table;
|
| 281 |
+
|
| 282 |
+
if let Some(idx) = found_idx {
|
| 283 |
+
// Found existing — reuse the buffer slot.
|
| 284 |
+
slot_table.set(team_index as usize, line_id, idx as u32 + SpeechSlotTable::BUFFER_OFFSET);
|
| 285 |
+
return 1;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// Not found — load new WAV via DSSound vtable slot 12 (load_wav).
|
| 289 |
+
let slot_idx = count as u32 + SpeechSlotTable::BUFFER_OFFSET;
|
| 290 |
+
let vtable = (*wrapper.sound).vtable;
|
| 291 |
+
let result = ((*vtable).load_wav)(wrapper.sound, slot_idx as i32, full_path as *const u8);
|
| 292 |
+
|
| 293 |
+
if result != 0 {
|
| 294 |
+
// Success — store slot, copy name into table, increment count.
|
| 295 |
+
slot_table.set(team_index as usize, line_id, slot_idx);
|
| 296 |
+
|
| 297 |
+
// Copy wav_path into name table entry.
|
| 298 |
+
let dest = &mut wrapper.speech_name_table[count];
|
| 299 |
+
let copy_len = search_name.len().min(SPEECH_NAME_ENTRY_SIZE - 1);
|
| 300 |
+
dest[..copy_len].copy_from_slice(&search_name[..copy_len]);
|
| 301 |
+
dest[copy_len] = 0;
|
| 302 |
+
|
| 303 |
+
wrapper.speech_name_count = (count + 1) as u32;
|
| 304 |
+
return 1;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
0 // failure
|
| 308 |
}
|
| 309 |
|
| 310 |
// ============================================================
|