paavohtl Claude Opus 4.6 (1M context) commited on
Commit
085ae61
·
1 Parent(s): 8d85984

feat: pure Rust DDGameWrapper__LoadSpeechWAV, typed struct fields

Browse files

Replace 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
- /// Loads a single speech WAV into DSSound buffer pool.
406
- /// Searches/allocates buffer slot, calls DSSound vtable to load WAV file.
 
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 DSSOUND_LOAD_SPEECH_WAV: u32 = 0x0057_1530;
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 DSSound_LoadSpeechWAV (0x571530).
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
- /// The constructor accesses at least up to DWORD index 0x1BBA (byte offset 0x6EE8),
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-0x6EEB: Unknown fields (extends to at least DWORD 0x1BBA)
52
- pub _unknown_4e4: [u8; 0x6A08],
 
 
 
 
 
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
- // DSSound_LoadSpeechWAV wrapper (0x571530)
242
  // ============================================================
243
- // usercall(ESI=DDGameWrapper) + 4 stack(team_index, line_id, wav_path, full_path), RET 0x10.
244
- // Returns 1 on success, 0 on failure.
 
245
 
246
- /// cdecl(ddgw, team_index, line_id, wav_path, full_path, func_addr) -> u32
247
- /// Sets ESI=ddgw, pushes 4 stack params in order, calls func (RET 0x10 cleans them).
248
- #[unsafe(naked)]
249
- unsafe extern "cdecl" fn load_speech_wav_raw(
250
- _ddgw: u32,
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
- load_speech_wav_raw(
282
- ddgw,
283
- team_index,
284
- line_id,
285
- wav_path as *const u8,
286
- full_path as *const u8,
287
- rb(va::DSSOUND_LOAD_SPEECH_WAV),
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
  // ============================================================