paavohtl Claude Opus 4.6 commited on
Commit
8b7ff92
·
1 Parent(s): b036aa9

feat: replace LoadSpeechBank and LoadAllSpeechBanks with full Rust implementations

Browse files

Convert DSSound_LoadSpeechBank from passthrough to full replacement that
iterates the speech line table, builds WAV paths, calls LoadSpeechWAV,
and handles fallback to default speech directory. Add new
DSSound_LoadAllSpeechBanks hook that clears the speech slot table and
iterates teams. Introduce DDGameWrapperHandle typed wrapper in openwa-lib
and add speech_team_count field to GameInfo struct.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

crates/openwa-lib/src/wa/ddgame.rs ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Typed handle for DDGameWrapper — the main game engine wrapper object.
2
+
3
+ use openwa_types::ddgame::DDGame;
4
+ use openwa_types::game_info::GameInfo;
5
+
6
+ /// Zero-cost handle to a DDGameWrapper instance (raw pointer as u32).
7
+ ///
8
+ /// DDGameWrapper is the top-level game engine object (~28KB). It holds
9
+ /// the DDGame pointer, graphics handlers, landscape, and display state.
10
+ ///
11
+ /// Pointer chain: DDGameWrapper → DDGame (+0x488) → GameInfo (+0x24).
12
+ #[derive(Clone, Copy)]
13
+ #[repr(transparent)]
14
+ pub struct DDGameWrapperHandle(pub u32);
15
+
16
+ impl DDGameWrapperHandle {
17
+ /// Wrap a raw DDGameWrapper pointer.
18
+ pub unsafe fn from_raw(ptr: u32) -> Self {
19
+ Self(ptr)
20
+ }
21
+
22
+ /// Read the DDGame pointer from DDGameWrapper+0x488.
23
+ pub unsafe fn ddgame(self) -> *mut DDGame {
24
+ *((self.0 + 0x488) as *const *mut DDGame)
25
+ }
26
+
27
+ /// Follow DDGameWrapper → DDGame(+0x488) → GameInfo(+0x24).
28
+ ///
29
+ /// GameInfo holds team configuration, speech paths, replay state, etc.
30
+ /// Some fields are mapped in the struct; others require offset constants
31
+ /// from `game_info_offsets`.
32
+ pub unsafe fn game_info(self) -> *mut GameInfo {
33
+ let ddgame = self.ddgame() as u32;
34
+ *((ddgame + 0x24) as *const *mut GameInfo)
35
+ }
36
+
37
+ /// Pointer to the speech slot table (DDGame+0x77E4, 0x5A0 bytes).
38
+ ///
39
+ /// Maps (team_index, speech_line_id) → DSSound buffer index.
40
+ pub unsafe fn speech_slot_table(self) -> *mut u8 {
41
+ (self.ddgame() as u32 + 0x77E4) as *mut u8
42
+ }
43
+ }
crates/openwa-lib/src/wa/mod.rs CHANGED
@@ -1,5 +1,6 @@
1
  //! Typed wrappers for calling WA.exe and library functions.
2
 
 
3
  pub mod frontend;
4
  pub mod mfc;
5
  pub mod registry;
 
1
  //! Typed wrappers for calling WA.exe and library functions.
2
 
3
+ pub mod ddgame;
4
  pub mod frontend;
5
  pub mod mfc;
6
  pub mod registry;
crates/openwa-types/src/address.rs CHANGED
@@ -294,16 +294,18 @@ pub mod va {
294
  /// 61 entries + null terminator. Maps speech line IDs to WAV filenames.
295
  pub const SPEECH_LINE_TABLE: u32 = 0x006A_F770;
296
 
297
- /// Iterates teams, calls LoadSpeechBank for each. Clears DDGame+0x77E4
298
- /// speech slot table. usercall(ESI=DDGame), plain RET.
299
  pub const DSSOUND_LOAD_ALL_SPEECH_BANKS: u32 = 0x0057_1A70;
300
 
301
  /// Iterates speech line table, calls LoadSpeechWAV per entry.
302
- /// usercall(EAX=DDGame) + 3 stack params, RET 0xC.
303
  pub const DSSOUND_LOAD_SPEECH_BANK: u32 = 0x0057_1660;
304
 
305
  /// Loads a single speech WAV into DSSound buffer pool.
306
- /// usercall(ESI=DDGame, EBX=DDGame) + 4 stack params, RET 0x10.
 
 
307
  pub const DSSOUND_LOAD_SPEECH_WAV: u32 = 0x0057_1530;
308
 
309
  /// Loads all 126 SFX WAVs from data\wav\Effects\.
@@ -657,6 +659,25 @@ pub mod va {
657
  // "game_state" in some code). Access pattern: *(DDGame+0x24) + offset.
658
 
659
  pub mod game_info_offsets {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  /// Replay state flag A — checked by TurnGame_HurryHandler and FrameFinish.
661
  /// Both DB08 and DB0A must be non-zero for replay-mode hurry path.
662
  pub const REPLAY_STATE_FLAG_A: u32 = 0xDB08;
 
294
  /// 61 entries + null terminator. Maps speech line IDs to WAV filenames.
295
  pub const SPEECH_LINE_TABLE: u32 = 0x006A_F770;
296
 
297
+ /// Iterates teams, calls LoadSpeechBank for each. Clears DDGameWrapper+0x488→DDGame+0x77E4
298
+ /// speech slot table. usercall(ESI=DDGameWrapper), plain RET.
299
  pub const DSSOUND_LOAD_ALL_SPEECH_BANKS: u32 = 0x0057_1A70;
300
 
301
  /// Iterates speech line table, calls LoadSpeechWAV per entry.
302
+ /// usercall(EAX=DDGameWrapper) + 3 stack(team_index, speech_base_path, speech_dir), RET 0xC.
303
  pub const DSSOUND_LOAD_SPEECH_BANK: u32 = 0x0057_1660;
304
 
305
  /// Loads a single speech WAV into DSSound buffer pool.
306
+ /// Searches/allocates buffer slot, calls DSSound vtable to load WAV file.
307
+ /// usercall(ESI=DDGameWrapper) + 4 stack(team_index, line_id, wav_path, full_path), RET 0x10.
308
+ /// Returns 1 on success, 0 on failure.
309
  pub const DSSOUND_LOAD_SPEECH_WAV: u32 = 0x0057_1530;
310
 
311
  /// Loads all 126 SFX WAVs from data\wav\Effects\.
 
659
  // "game_state" in some code). Access pattern: *(DDGame+0x24) + offset.
660
 
661
  pub mod game_info_offsets {
662
+ // === Speech configuration ===
663
+
664
+ /// Number of teams with speech enabled (byte). Used by LoadAllSpeechBanks.
665
+ pub const SPEECH_TEAM_COUNT: u32 = 0x44C;
666
+ /// Per-team speech config stride (0xC2 = 0x81 base path + 0x41 dir name).
667
+ pub const SPEECH_TEAM_STRIDE: u32 = 0xC2;
668
+ /// Offset to per-team speech base path (char[0x81]).
669
+ /// Access: GameInfo + SPEECH_BASE_PATH + team_index * SPEECH_TEAM_STRIDE.
670
+ pub const SPEECH_BASE_PATH: u32 = 0xF486;
671
+ /// Offset to per-team speech directory name (char[0x41]).
672
+ /// Access: GameInfo + SPEECH_DIR + team_index * SPEECH_TEAM_STRIDE.
673
+ pub const SPEECH_DIR: u32 = 0xF507;
674
+ /// Default speech base path (for fallback when team-specific WAV not found).
675
+ pub const DEFAULT_SPEECH_BASE_PATH: u32 = 0xF3C4;
676
+ /// Default speech directory name (for fallback).
677
+ pub const DEFAULT_SPEECH_DIR: u32 = 0xF445;
678
+
679
+ // === Replay configuration ===
680
+
681
  /// Replay state flag A — checked by TurnGame_HurryHandler and FrameFinish.
682
  /// Both DB08 and DB0A must be non-zero for replay-mode hurry path.
683
  pub const REPLAY_STATE_FLAG_A: u32 = 0xDB08;
crates/openwa-types/src/game_info.rs CHANGED
@@ -8,8 +8,17 @@
8
  /// The actual struct is likely larger. Conservative size 0xF500.
9
  #[repr(C)]
10
  pub struct GameInfo {
11
- /// 0x0000-0xDAE7: Unknown
12
- pub _unknown_0000: [u8; 0xDAE8],
 
 
 
 
 
 
 
 
 
13
 
14
  // --- Cluster 1: data paths ---
15
 
 
8
  /// The actual struct is likely larger. Conservative size 0xF500.
9
  #[repr(C)]
10
  pub struct GameInfo {
11
+ /// 0x0000-0x044B: Unknown
12
+ pub _unknown_0000: [u8; 0x44C],
13
+
14
+ // --- Speech configuration ---
15
+
16
+ /// 0x044C: Number of teams with speech banks loaded (byte).
17
+ /// Used by DSSound_LoadAllSpeechBanks to iterate teams.
18
+ pub speech_team_count: u8,
19
+
20
+ /// 0x044D-0xDAE7: Unknown
21
+ pub _unknown_044d: [u8; 0xDAE8 - 0x44D],
22
 
23
  // --- Cluster 1: data paths ---
24
 
crates/openwa-wormkit/src/replacements/speech.rs CHANGED
@@ -4,7 +4,8 @@
4
  //! - PlayFeSfx (0x4D7960): stdcall(sfx_name), RET 0x4 — REPLACED
5
  //! - PlayFanfare_Default (0x4D7500): stdcall(team_type), RET 0x4 — REPLACED
6
  //! - PlayFanfare_CurrentTeam (0x4D78E0): usercall(EAX=index), plain RET — passthrough
7
- //! - DSSound_LoadSpeechBank (0x571660): usercall(EAX=DDGame) + 3 stack, RET 0xC — passthrough
 
8
 
9
  use std::ffi::{c_char, CStr};
10
  use std::sync::atomic::{AtomicU32, Ordering};
@@ -12,7 +13,9 @@ use std::sync::atomic::{AtomicU32, Ordering};
12
  use heapless::CString;
13
 
14
  use openwa_lib::rebase::rb;
15
- use openwa_types::address::va;
 
 
16
 
17
  use crate::hook::{self, usercall_trampoline};
18
  use crate::log_line;
@@ -235,55 +238,210 @@ unsafe extern "cdecl" fn play_fanfare_current_team_impl(index: u32) -> u32 {
235
  }
236
 
237
  // ============================================================
238
- // DSSound_LoadSpeechBank passthrough (0x571660)
239
  // ============================================================
240
- // usercall(EAX=DDGame) + 3 stack params (team_index, speech_path, speech_dir)
241
- // RET 0xC
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- static ORIG_LOAD_SPEECH_BANK: AtomicU32 = AtomicU32::new(0);
 
 
 
 
 
 
 
 
244
 
245
  usercall_trampoline!(fn trampoline_load_speech_bank; impl_fn = load_speech_bank_impl;
246
  reg = eax; stack_params = 3; ret_bytes = "0xC");
247
 
248
  unsafe extern "cdecl" fn load_speech_bank_impl(
249
- ddgame: u32,
250
  team_index: u32,
251
- speech_path: *const u8,
252
  speech_dir: *const u8,
253
  ) {
254
- let path_str = if !speech_path.is_null() {
255
- CStr::from_ptr(speech_path as *const i8)
256
- .to_str()
257
- .unwrap_or("?")
258
- } else {
259
- "(null)"
260
- };
261
- let dir_str = if !speech_dir.is_null() {
262
- CStr::from_ptr(speech_dir as *const i8)
263
- .to_str()
264
- .unwrap_or("?")
265
- } else {
266
- "(null)"
267
- };
268
  let _ = log_line(&format!(
269
- "[Speech] LoadSpeechBank: team={} path=\"{}\" dir=\"{}\" ddgame=0x{:08X}",
270
- team_index, path_str, dir_str, ddgame
271
  ));
272
 
273
- // Call original via trampoline — must restore EAX and push stack params
274
- let orig = ORIG_LOAD_SPEECH_BANK.load(Ordering::Relaxed);
275
- core::arch::asm!(
276
- "push {dir}",
277
- "push {path}",
278
- "push {team}",
279
- "call {orig}",
280
- team = in(reg) team_index,
281
- path = in(reg) speech_path,
282
- dir = in(reg) speech_dir,
283
- orig = in(reg) orig,
284
- in("eax") ddgame,
285
- clobber_abi("C"),
286
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  }
288
 
289
  // ============================================================
@@ -311,12 +469,19 @@ pub fn install() -> Result<(), String> {
311
  )?;
312
  ORIG_PLAY_FANFARE_CURRENT_TEAM.store(trampoline as u32, Ordering::Relaxed);
313
 
314
- let trampoline = hook::install(
 
315
  "DSSound_LoadSpeechBank",
316
  va::DSSOUND_LOAD_SPEECH_BANK,
317
  trampoline_load_speech_bank as *const (),
318
  )?;
319
- ORIG_LOAD_SPEECH_BANK.store(trampoline as u32, Ordering::Relaxed);
 
 
 
 
 
 
320
  }
321
 
322
  Ok(())
 
4
  //! - PlayFeSfx (0x4D7960): stdcall(sfx_name), RET 0x4 — REPLACED
5
  //! - PlayFanfare_Default (0x4D7500): stdcall(team_type), RET 0x4 — REPLACED
6
  //! - PlayFanfare_CurrentTeam (0x4D78E0): usercall(EAX=index), plain RET — passthrough
7
+ //! - DSSound_LoadSpeechBank (0x571660): usercall(EAX=DDGameWrapper) + 3 stack, RET 0xC — REPLACED
8
+ //! - DSSound_LoadAllSpeechBanks (0x571A70): usercall(ESI=DDGameWrapper), plain RET — REPLACED
9
 
10
  use std::ffi::{c_char, CStr};
11
  use std::sync::atomic::{AtomicU32, Ordering};
 
13
  use heapless::CString;
14
 
15
  use openwa_lib::rebase::rb;
16
+ use openwa_lib::wa::ddgame::DDGameWrapperHandle;
17
+ use openwa_types::address::va::{self, game_info_offsets};
18
+ use openwa_types::speech::SpeechLineTableEntry;
19
 
20
  use crate::hook::{self, usercall_trampoline};
21
  use crate::log_line;
 
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,
277
+ line_id: u32,
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
+ // ============================================================
292
+ // Helpers
293
+ // ============================================================
294
+
295
+ /// Extract bytes from a null-terminated C string pointer.
296
+ unsafe fn cstr_bytes<'a>(ptr: *const u8) -> &'a [u8] {
297
+ if ptr.is_null() {
298
+ return &[];
299
+ }
300
+ CStr::from_ptr(ptr as *const i8).to_bytes()
301
+ }
302
 
303
+ // ============================================================
304
+ // DSSound_LoadSpeechBank replacement (0x571660)
305
+ // ============================================================
306
+ // usercall(EAX=DDGameWrapper) + 3 stack params (team_index, speech_base_path, speech_dir)
307
+ // RET 0xC
308
+ //
309
+ // Iterates the speech line table at 0x6AF770. For each entry, builds WAV paths
310
+ // and calls LoadSpeechWAV. Falls back to default speech dir on failure.
311
+ // Skips remaining filename alternates when one succeeds.
312
 
313
  usercall_trampoline!(fn trampoline_load_speech_bank; impl_fn = load_speech_bank_impl;
314
  reg = eax; stack_params = 3; ret_bytes = "0xC");
315
 
316
  unsafe extern "cdecl" fn load_speech_bank_impl(
317
+ ddgw: DDGameWrapperHandle,
318
  team_index: u32,
319
+ speech_base_path: *const u8,
320
  speech_dir: *const u8,
321
  ) {
322
+ let path_str = CStr::from_ptr(speech_base_path as *const i8)
323
+ .to_str()
324
+ .unwrap_or("?");
325
+ let dir_str = CStr::from_ptr(speech_dir as *const i8)
326
+ .to_str()
327
+ .unwrap_or("?");
 
 
 
 
 
 
 
 
328
  let _ = log_line(&format!(
329
+ "[Speech] LoadSpeechBank: team={} path=\"{}\" dir=\"{}\" ddgw=0x{:08X}",
330
+ team_index, path_str, dir_str, ddgw.0
331
  ));
332
 
333
+ let table_base = rb(va::SPEECH_LINE_TABLE) as *const SpeechLineTableEntry;
334
+ let mut i: usize = 0;
335
+
336
+ loop {
337
+ let entry = &*table_base.add(i);
338
+
339
+ // Null name_ptr = end of table
340
+ if entry.name_ptr.is_null() {
341
+ return;
342
+ }
343
+
344
+ let name_bytes = cstr_bytes(entry.name_ptr);
345
+
346
+ // Build wav_path: "<speech_dir>\<name>.wav"
347
+ let mut wav_path = CString::<MAX_PATH>::new();
348
+ let _ = wav_path.extend_from_bytes(cstr_bytes(speech_dir));
349
+ let _ = wav_path.extend_from_bytes(b"\\");
350
+ let _ = wav_path.extend_from_bytes(name_bytes);
351
+ let _ = wav_path.extend_from_bytes(b".wav");
352
+
353
+ // Build full_path: "<speech_base_path>\<wav_path>"
354
+ let mut full_path = CString::<MAX_PATH>::new();
355
+ let _ = full_path.extend_from_bytes(cstr_bytes(speech_base_path));
356
+ let _ = full_path.extend_from_bytes(b"\\");
357
+ let _ = full_path.extend_from_bytes(wav_path.as_bytes());
358
+
359
+ let result = call_load_speech_wav(
360
+ ddgw.0,
361
+ team_index,
362
+ entry.id,
363
+ wav_path.as_ptr(),
364
+ full_path.as_ptr(),
365
+ );
366
+
367
+ if result == 0 {
368
+ // Failed — try fallback with default speech dir if no more alternates
369
+ let next_entry = &*table_base.add(i + 1);
370
+ if next_entry.name_ptr.is_null() || next_entry.id != entry.id {
371
+ let game_info = ddgw.game_info() as *const u8;
372
+ let default_dir =
373
+ game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize);
374
+ let default_base =
375
+ game_info.add(game_info_offsets::DEFAULT_SPEECH_BASE_PATH as usize);
376
+
377
+ let mut wav_path2 = CString::<MAX_PATH>::new();
378
+ let _ = wav_path2.extend_from_bytes(cstr_bytes(default_dir));
379
+ let _ = wav_path2.extend_from_bytes(b"\\");
380
+ let _ = wav_path2.extend_from_bytes(name_bytes);
381
+ let _ = wav_path2.extend_from_bytes(b".wav");
382
+
383
+ let mut full_path2 = CString::<MAX_PATH>::new();
384
+ let _ = full_path2.extend_from_bytes(cstr_bytes(default_base));
385
+ let _ = full_path2.extend_from_bytes(b"\\");
386
+ let _ = full_path2.extend_from_bytes(wav_path2.as_bytes());
387
+
388
+ call_load_speech_wav(
389
+ ddgw.0,
390
+ team_index,
391
+ entry.id,
392
+ wav_path2.as_ptr(),
393
+ full_path2.as_ptr(),
394
+ );
395
+ }
396
+ } else {
397
+ // Success — skip remaining filename alternates for this ID
398
+ let current_id = entry.id;
399
+ while {
400
+ let next = &*table_base.add(i + 1);
401
+ !next.name_ptr.is_null() && next.id == current_id
402
+ } {
403
+ i += 1;
404
+ }
405
+ }
406
+
407
+ i += 1;
408
+ }
409
+ }
410
+
411
+ // ============================================================
412
+ // DSSound_LoadAllSpeechBanks replacement (0x571A70)
413
+ // ============================================================
414
+ // usercall(ESI=DDGameWrapper), plain RET.
415
+ // Clears the speech slot table, then iterates teams and calls LoadSpeechBank.
416
+
417
+ usercall_trampoline!(fn trampoline_load_all_speech_banks;
418
+ impl_fn = load_all_speech_banks_impl; reg = esi);
419
+
420
+ unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: DDGameWrapperHandle) {
421
+ // Clear speech slot table: DDGame+0x77E4, 0x5A0 bytes
422
+ core::ptr::write_bytes(ddgw.speech_slot_table(), 0, 0x5A0);
423
+
424
+ // Read team count from GameInfo
425
+ let game_info = ddgw.game_info();
426
+ let team_count = (*game_info).speech_team_count as u32;
427
+
428
+ let _ = log_line(&format!(
429
+ "[Speech] LoadAllSpeechBanks: ddgw=0x{:08X} teams={}",
430
+ ddgw.0, team_count
431
+ ));
432
+
433
+ // Load speech bank for each team
434
+ let gi = game_info as *const u8;
435
+ for i in 0..team_count {
436
+ let team_offset = (i * game_info_offsets::SPEECH_TEAM_STRIDE) as usize;
437
+ let base_path =
438
+ gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset);
439
+ let dir =
440
+ gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset);
441
+
442
+ // Call our Rust impl directly (bypasses the hook trampoline)
443
+ load_speech_bank_impl(ddgw, i, base_path, dir);
444
+ }
445
  }
446
 
447
  // ============================================================
 
469
  )?;
470
  ORIG_PLAY_FANFARE_CURRENT_TEAM.store(trampoline as u32, Ordering::Relaxed);
471
 
472
+ // Full replacement — no trampoline needed
473
+ hook::install(
474
  "DSSound_LoadSpeechBank",
475
  va::DSSOUND_LOAD_SPEECH_BANK,
476
  trampoline_load_speech_bank as *const (),
477
  )?;
478
+
479
+ // Full replacement — no trampoline needed
480
+ hook::install(
481
+ "DSSound_LoadAllSpeechBanks",
482
+ va::DSSOUND_LOAD_ALL_SPEECH_BANKS,
483
+ trampoline_load_all_speech_banks as *const (),
484
+ )?;
485
  }
486
 
487
  Ok(())
deploy.ps1 CHANGED
@@ -1,10 +1,9 @@
1
  $dest = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon"
2
 
3
- cargo build --release -p openwa-wormkit -p openwa-validator
4
  if ($LASTEXITCODE -ne 0) { exit 1 }
5
 
6
  $src = "target\i686-pc-windows-msvc\release"
7
  Copy-Item "$src\openwa_wormkit.dll" "$dest\wkOpenWA.dll"
8
- Copy-Item "$src\openwa_validator.dll" "$dest\wkOpenWAValidator.dll"
9
 
10
  Write-Host "Deployed to $dest"
 
1
  $dest = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon"
2
 
3
+ cargo build --release -p openwa-wormkit
4
  if ($LASTEXITCODE -ne 0) { exit 1 }
5
 
6
  $src = "target\i686-pc-windows-msvc\release"
7
  Copy-Item "$src\openwa_wormkit.dll" "$dest\wkOpenWA.dll"
 
8
 
9
  Write-Host "Deployed to $dest"