paavohtl Claude Opus 4.6 (1M context) commited on
Commit
410a66e
·
1 Parent(s): d2ae910

feat: port WavPlayer and speech/fanfare functions to pure Rust

Browse files

Replace all 3 WavPlayer asm bridges (Stop, Play, LoadAndPlay) with Rust
implementations using hound for WAV parsing and the windows crate for
DirectSound. Hook them globally to avoid CRT heap mismatch between WA's
msvcr80 and our ucrt.

Port PlayFanfare_CurrentTeam from passthrough to full replacement
(team index lookup + fanfare path building). Eliminates all remaining
WA.exe function calls from the speech/fanfare system.

Move game logic from wormkit speech.rs into openwa-core:
- audio/wav_player.rs: WavPlayer struct, stop/play/load_and_play
- audio/speech_ops.rs: PlayFeSfx, PlayFanfare, speech bank loading,
team config name lookup table

Wormkit speech.rs now contains only hook trampolines, logging shims,
and install().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/audio/mod.rs CHANGED
@@ -5,7 +5,9 @@ pub mod music;
5
  pub mod sound_id;
6
  pub mod sound_ops;
7
  pub mod speech;
 
8
  pub mod streaming;
 
9
 
10
  pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
11
  pub use dssound::{
 
5
  pub mod sound_id;
6
  pub mod sound_ops;
7
  pub mod speech;
8
+ pub mod speech_ops;
9
  pub mod streaming;
10
+ pub mod wav_player;
11
 
12
  pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
13
  pub use dssound::{
crates/openwa-core/src/audio/speech_ops.rs ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Speech / fanfare / FE SFX operations.
2
+ //!
3
+ //! Pure Rust reimplementations of WA.exe speech and fanfare functions.
4
+ //! Called from wormkit hook trampolines.
5
+ //!
6
+ //! Original WA functions:
7
+ //! - PlayFeSfx (0x4D7960)
8
+ //! - PlayFanfare_Default (0x4D7500)
9
+ //! - PlayFanfare (0x4D7630)
10
+ //! - PlayFanfare_CurrentTeam (0x4D78E0)
11
+ //! - FindCurrentTeamIndex (0x46A7D0)
12
+ //! - GetTeamConfigName (0x4A62A0)
13
+ //! - DDGameWrapper__LoadSpeechWAV (0x571530)
14
+ //! - DSSound_LoadSpeechBank (0x571660)
15
+ //! - DSSound_LoadAllSpeechBanks (0x571A70)
16
+
17
+ use std::ffi::{c_char, CStr};
18
+
19
+ use heapless::CString;
20
+
21
+ use crate::address::va::{self, game_info_offsets};
22
+ use crate::audio::wav_player::{self, WavPlayer};
23
+ use crate::audio::{SpeechLineTableEntry, SpeechSlotTable};
24
+ use crate::engine::ddgame_wrapper::SPEECH_NAME_ENTRY_SIZE;
25
+ use crate::engine::DDGameWrapper;
26
+ use crate::rebase::rb;
27
+
28
+ /// Windows MAX_PATH (260 bytes including nul terminator).
29
+ const MAX_PATH: usize = 260;
30
+
31
+ // ============================================================
32
+ // Team config name lookup (port of GetTeamConfigName 0x4A62A0)
33
+ // ============================================================
34
+
35
+ /// Team config names indexed 0-48, matching WA.exe's jump table at 0x4A62A0.
36
+ /// Used for fanfare WAV file paths (e.g. "user\Fanfare\Finland.wav").
37
+ /// Note: "Leichtenstein" is WA's original misspelling — must match for file lookup.
38
+ const TEAM_CONFIG_NAMES: [&str; 49] = [
39
+ "UK", // 0
40
+ "Argentina", // 1
41
+ "Australia", // 2
42
+ "Austria", // 3
43
+ "Belgium", // 4
44
+ "Brazil", // 5
45
+ "Canada", // 6
46
+ "Croatia", // 7
47
+ "Simple", // 8
48
+ "Cyprus", // 9
49
+ "Simple", // 10
50
+ "Denmark", // 11
51
+ "Finland", // 12
52
+ "France", // 13
53
+ "Simple", // 14
54
+ "Germany", // 15
55
+ "Greece", // 16
56
+ "Simple", // 17
57
+ "Hungary", // 18
58
+ "Iceland", // 19
59
+ "India", // 20
60
+ "Simple", // 21
61
+ "Psycho Laugh", // 22
62
+ "Psycho Laugh", // 23
63
+ "Ireland", // 24
64
+ "Israel", // 25
65
+ "Italy", // 26
66
+ "Japan", // 27
67
+ "Leichtenstein", // 28
68
+ "Luxembourg", // 29
69
+ "Simple", // 30
70
+ "Malta", // 31
71
+ "Mexico", // 32
72
+ "Morocco", // 33
73
+ "Netherlands", // 34
74
+ "New Zealand", // 35
75
+ "Norway", // 36
76
+ "Poland", // 37
77
+ "Portugal", // 38
78
+ "Simple", // 39
79
+ "Romania", // 40
80
+ "Russia", // 41
81
+ "Singapore", // 42
82
+ "South Africa", // 43
83
+ "Spain", // 44
84
+ "Sweden", // 45
85
+ "Switzerland", // 46
86
+ "Turkey", // 47
87
+ "USA", // 48
88
+ ];
89
+
90
+ /// Look up a team config name by team_type (1-49). Returns "Simple" for out-of-range.
91
+ pub fn team_config_name(team_type: u32) -> &'static str {
92
+ if (1..=49).contains(&team_type) {
93
+ TEAM_CONFIG_NAMES[(team_type - 1) as usize]
94
+ } else {
95
+ "Simple"
96
+ }
97
+ }
98
+
99
+ // ============================================================
100
+ // WavPlayer access helper
101
+ // ============================================================
102
+
103
+ /// Get a pointer to the WavPlayer struct at a global address.
104
+ #[inline]
105
+ unsafe fn wav_player_at(ghidra_addr: u32) -> *mut WavPlayer {
106
+ rb(ghidra_addr) as *mut WavPlayer
107
+ }
108
+
109
+ // ============================================================
110
+ // PlayFeSfx — port of 0x4D7960
111
+ // ============================================================
112
+
113
+ /// Build "fesfx\<name>.wav" path and play on the FESFX WavPlayer.
114
+ pub unsafe fn play_fe_sfx(sfx_name: &str) {
115
+ let mut path = CString::<MAX_PATH>::new();
116
+ let _ = path.extend_from_bytes(b"fesfx\\");
117
+ let _ = path.extend_from_bytes(sfx_name.as_bytes());
118
+ let _ = path.extend_from_bytes(b".wav");
119
+
120
+ let player = wav_player_at(va::FESFX_WAV_PLAYER);
121
+ wav_player::wav_player_stop(player);
122
+ wav_player::wav_player_load_and_play(player, path.as_ptr(), 0);
123
+ wav_player::wav_player_play(player, 0);
124
+ }
125
+
126
+ // ============================================================
127
+ // PlayFanfare_Default — port of 0x4D7500
128
+ // ============================================================
129
+
130
+ /// Look up team config name, build fanfare path, play on FANFARE WavPlayer.
131
+ pub unsafe fn play_fanfare_default(team_type: u32) {
132
+ let name = team_config_name(team_type);
133
+
134
+ let wa_path = CStr::from_ptr(rb(va::WA_DATA_PATH) as *const i8)
135
+ .to_str()
136
+ .unwrap_or(".");
137
+
138
+ let mut path = CString::<MAX_PATH>::new();
139
+ let _ = path.extend_from_bytes(wa_path.as_bytes());
140
+ let _ = path.extend_from_bytes(b"\\user\\Fanfare\\");
141
+ let _ = path.extend_from_bytes(name.as_bytes());
142
+ let _ = path.extend_from_bytes(b".wav");
143
+
144
+ let player = wav_player_at(va::FANFARE_WAV_PLAYER);
145
+ wav_player::wav_player_stop(player);
146
+ wav_player::wav_player_load_and_play(player, path.as_ptr(), 0);
147
+ wav_player::wav_player_play(player, 0);
148
+ }
149
+
150
+ // ============================================================
151
+ // PlayFanfare — port of 0x4D7630
152
+ // ============================================================
153
+
154
+ /// Build the fanfare WAV path and play it on the FANFARE player.
155
+ /// Falls back to `play_fanfare_default` if loading fails.
156
+ pub unsafe fn play_fanfare(team_type: u32, has_custom_path: bool) {
157
+ let name = team_config_name(team_type);
158
+
159
+ let mut path = CString::<MAX_PATH>::new();
160
+ if has_custom_path {
161
+ let custom_path = CStr::from_ptr(rb(0x0088_E282) as *const i8)
162
+ .to_str()
163
+ .unwrap_or(".");
164
+ let _ = path.extend_from_bytes(custom_path.as_bytes());
165
+ let _ = path.extend_from_bytes(b"\\user\\Fanfare\\");
166
+ let _ = path.extend_from_bytes(name.as_bytes());
167
+ let _ = path.extend_from_bytes(b".wav");
168
+ } else {
169
+ let _ = path.extend_from_bytes(b"user\\Fanfare\\");
170
+ let _ = path.extend_from_bytes(name.as_bytes());
171
+ let _ = path.extend_from_bytes(b".wav");
172
+ }
173
+
174
+ let player = wav_player_at(va::FANFARE_WAV_PLAYER);
175
+ wav_player::wav_player_stop(player);
176
+ let loaded = wav_player::wav_player_load_and_play(player, path.as_ptr(), 0);
177
+ if loaded {
178
+ wav_player::wav_player_play(player, 0);
179
+ } else {
180
+ play_fanfare_default(team_type);
181
+ }
182
+ }
183
+
184
+ // ============================================================
185
+ // PlayFanfare_CurrentTeam — port of 0x4D78E0
186
+ // ============================================================
187
+
188
+ /// Find current team index by comparing team names.
189
+ ///
190
+ /// Port of FindCurrentTeamIndex (0x46A7D0). Iterates up to 6 team entries
191
+ /// at stride 0xD7B, comparing name at offset +0x627 against the search name.
192
+ unsafe fn find_current_team_index(name_ptr: u32) -> u32 {
193
+ let name = name_ptr as *const u8;
194
+ let base = rb(0x0087_7FFC) as *const u8;
195
+
196
+ for i in 0u32..6 {
197
+ let entry = base.add(i as usize * 0xD7B);
198
+ if *entry.add(0x121) == 0 {
199
+ continue;
200
+ }
201
+ let team_name = entry.add(0x627);
202
+ if c_str_eq(team_name, name) {
203
+ return i;
204
+ }
205
+ }
206
+ -1i32 as u32
207
+ }
208
+
209
+ /// Compare two null-terminated C strings for equality.
210
+ unsafe fn c_str_eq(a: *const u8, b: *const u8) -> bool {
211
+ let mut i = 0;
212
+ loop {
213
+ let ca = *a.add(i);
214
+ let cb = *b.add(i);
215
+ if ca != cb {
216
+ return false;
217
+ }
218
+ if ca == 0 {
219
+ return true;
220
+ }
221
+ i += 1;
222
+ }
223
+ }
224
+
225
+ /// Read the default team type, find current team, play the fanfare.
226
+ ///
227
+ /// Returns 1 on success, 0 if team not found.
228
+ pub unsafe fn play_fanfare_current_team(eax_index: u32) -> u32 {
229
+ let default_team_type = *(rb(0x0088_DFAC) as *const u32);
230
+
231
+ let team_index = find_current_team_index(eax_index);
232
+ if team_index == -1i32 as u32 {
233
+ return 0;
234
+ }
235
+
236
+ let custom_types_active = *(rb(0x0087_D0DE) as *const u8);
237
+ let team_type = if custom_types_active != 0 {
238
+ let team_data = rb(0x0087_7FFC) as *const u8;
239
+ let team_type_byte = *team_data.add(team_index as usize * 0xD7B) as i8;
240
+ let team_types_array = rb(0x0087_7A54) as *const u32;
241
+ *team_types_array.add(team_type_byte as usize * 0x1E)
242
+ } else {
243
+ default_team_type
244
+ };
245
+
246
+ let team_data_2 = rb(0x0087_8093) as *const u8;
247
+ let has_custom_path = *team_data_2.add(team_index as usize * 0xD7B) != 0;
248
+
249
+ play_fanfare(team_type, has_custom_path);
250
+
251
+ 1
252
+ }
253
+
254
+ // ============================================================
255
+ // DDGameWrapper__LoadSpeechWAV — port of 0x571530
256
+ // ============================================================
257
+
258
+ /// Search speech name table for existing WAV, reuse slot if found.
259
+ /// Otherwise load new WAV via DSSound vtable. Returns 1 on success, 0 on failure.
260
+ pub unsafe fn load_speech_wav(
261
+ ddgw: *mut DDGameWrapper,
262
+ team_index: u32,
263
+ line_id: u32,
264
+ wav_path: *const c_char,
265
+ full_path: *const c_char,
266
+ ) -> u32 {
267
+ let wrapper = &mut *ddgw;
268
+ let count = wrapper.speech_name_count as usize;
269
+ let search_name = core::ffi::CStr::from_ptr(wav_path).to_bytes();
270
+
271
+ let mut found_idx: Option<usize> = None;
272
+ for i in 0..count {
273
+ let entry = &wrapper.speech_name_table[i];
274
+ let entry_len = entry.iter().position(|&b| b == 0).unwrap_or(entry.len());
275
+ if entry[..entry_len] == *search_name {
276
+ found_idx = Some(i);
277
+ break;
278
+ }
279
+ }
280
+
281
+ let ddgame = &mut *wrapper.ddgame;
282
+ let slot_table = &mut ddgame.speech_slot_table;
283
+
284
+ if let Some(idx) = found_idx {
285
+ slot_table.set(
286
+ team_index as usize,
287
+ line_id,
288
+ idx as u32 + SpeechSlotTable::BUFFER_OFFSET,
289
+ );
290
+ return 1;
291
+ }
292
+
293
+ let slot_idx = count as u32 + SpeechSlotTable::BUFFER_OFFSET;
294
+ let vtable = (*wrapper.sound).vtable;
295
+ let result = ((*vtable).load_wav)(wrapper.sound, slot_idx as i32, full_path as *const u8);
296
+
297
+ if result != 0 {
298
+ slot_table.set(team_index as usize, line_id, slot_idx);
299
+
300
+ let dest = &mut wrapper.speech_name_table[count];
301
+ let copy_len = search_name.len().min(SPEECH_NAME_ENTRY_SIZE - 1);
302
+ dest[..copy_len].copy_from_slice(&search_name[..copy_len]);
303
+ dest[copy_len] = 0;
304
+
305
+ wrapper.speech_name_count = (count + 1) as u32;
306
+ return 1;
307
+ }
308
+
309
+ 0
310
+ }
311
+
312
+ // ============================================================
313
+ // Helpers
314
+ // ============================================================
315
+
316
+ /// Extract bytes from a null-terminated C string pointer.
317
+ unsafe fn cstr_bytes<'a>(ptr: *const u8) -> &'a [u8] {
318
+ if ptr.is_null() {
319
+ return &[];
320
+ }
321
+ CStr::from_ptr(ptr as *const i8).to_bytes()
322
+ }
323
+
324
+ // ============================================================
325
+ // DSSound_LoadSpeechBank — port of 0x571660
326
+ // ============================================================
327
+
328
+ /// Iterate speech line table, build WAV paths, load each into DSSound.
329
+ /// Falls back to default speech dir on failure.
330
+ pub unsafe fn load_speech_bank(
331
+ ddgw: *const DDGameWrapper,
332
+ team_index: u32,
333
+ speech_base_path: *const u8,
334
+ speech_dir: *const u8,
335
+ ) {
336
+ let table_base = rb(va::SPEECH_LINE_TABLE) as *const SpeechLineTableEntry;
337
+ let mut i: usize = 0;
338
+
339
+ loop {
340
+ let entry = &*table_base.add(i);
341
+
342
+ if entry.name_ptr.is_null() {
343
+ return;
344
+ }
345
+
346
+ let name_bytes = cstr_bytes(entry.name_ptr);
347
+
348
+ let mut wav_path = CString::<MAX_PATH>::new();
349
+ let _ = wav_path.extend_from_bytes(cstr_bytes(speech_dir));
350
+ let _ = wav_path.extend_from_bytes(b"\\");
351
+ let _ = wav_path.extend_from_bytes(name_bytes);
352
+ let _ = wav_path.extend_from_bytes(b".wav");
353
+
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 = load_speech_wav(
360
+ ddgw as *mut DDGameWrapper,
361
+ team_index,
362
+ entry.id,
363
+ wav_path.as_ptr(),
364
+ full_path.as_ptr(),
365
+ );
366
+
367
+ if result == 0 {
368
+ let next_entry = &*table_base.add(i + 1);
369
+ if next_entry.name_ptr.is_null() || next_entry.id != entry.id {
370
+ let game_info = (*(*ddgw).ddgame).game_info as *const u8;
371
+ let default_dir = game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize);
372
+ let default_base =
373
+ game_info.add(game_info_offsets::DEFAULT_SPEECH_BASE_PATH as usize);
374
+
375
+ let mut wav_path2 = CString::<MAX_PATH>::new();
376
+ let _ = wav_path2.extend_from_bytes(cstr_bytes(default_dir));
377
+ let _ = wav_path2.extend_from_bytes(b"\\");
378
+ let _ = wav_path2.extend_from_bytes(name_bytes);
379
+ let _ = wav_path2.extend_from_bytes(b".wav");
380
+
381
+ let mut full_path2 = CString::<MAX_PATH>::new();
382
+ let _ = full_path2.extend_from_bytes(cstr_bytes(default_base));
383
+ let _ = full_path2.extend_from_bytes(b"\\");
384
+ let _ = full_path2.extend_from_bytes(wav_path2.as_bytes());
385
+
386
+ load_speech_wav(
387
+ ddgw as *mut DDGameWrapper,
388
+ team_index,
389
+ entry.id,
390
+ wav_path2.as_ptr(),
391
+ full_path2.as_ptr(),
392
+ );
393
+ }
394
+ } else {
395
+ let current_id = entry.id;
396
+ while {
397
+ let next = &*table_base.add(i + 1);
398
+ !next.name_ptr.is_null() && next.id == current_id
399
+ } {
400
+ i += 1;
401
+ }
402
+ }
403
+
404
+ i += 1;
405
+ }
406
+ }
407
+
408
+ // ============================================================
409
+ // DSSound_LoadAllSpeechBanks — port of 0x571A70
410
+ // ============================================================
411
+
412
+ /// Clear speech slot table, then load speech bank for each team.
413
+ pub unsafe fn load_all_speech_banks(ddgw: *const DDGameWrapper) {
414
+ let ddgame = &mut *(*ddgw).ddgame;
415
+
416
+ ddgame.speech_slot_table.clear();
417
+
418
+ let game_info = &*ddgame.game_info;
419
+ let team_count = game_info.speech_team_count as u32;
420
+
421
+ let gi = ddgame.game_info as *const u8;
422
+ for i in 0..team_count {
423
+ let team_offset = (i * game_info_offsets::SPEECH_TEAM_STRIDE) as usize;
424
+ let base_path = gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset);
425
+ let dir = gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset);
426
+
427
+ load_speech_bank(ddgw, i, base_path, dir);
428
+ }
429
+ }
crates/openwa-core/src/audio/wav_player.rs ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! WavPlayer — standalone WAV playback via DirectSound.
2
+ //!
3
+ //! WA.exe uses WavPlayer instances for frontend SFX, fanfares, and music
4
+ //! playback. Each instance is an 8-byte struct at a fixed global address.
5
+ //! This module provides pure Rust replacements for the three core operations:
6
+ //!
7
+ //! - `WavPlayer_Stop` (0x599670): Release the loaded DirectSound buffer
8
+ //! - `WavPlayer_Play` (0x5996E0): Set volume and start playback
9
+ //! - `WavPlayer_LoadAndPlay` (0x599B40): Parse WAV, create DS buffer, store
10
+ //!
11
+ //! The WavPlayer system uses its own IDirectSound instance (global at
12
+ //! 0x79D654), separate from DSSound's instance.
13
+
14
+ use std::ffi::{c_char, c_void, CStr, CString};
15
+
16
+ use windows::Win32::Media::Audio::DirectSound::{
17
+ IDirectSound, IDirectSoundBuffer, DSBLOCK_ENTIREBUFFER, DSBUFFERDESC,
18
+ };
19
+ use windows::Win32::Media::Audio::{WAVEFORMATEX, WAVE_FORMAT_PCM};
20
+
21
+ use crate::rebase::rb;
22
+
23
+ // ============================================================
24
+ // Address constants
25
+ // ============================================================
26
+
27
+ crate::define_addresses! {
28
+ /// IDirectSound* for the WavPlayer subsystem (separate from DSSound).
29
+ /// Null before DirectSound init.
30
+ global G_WAV_DIRECT_SOUND = 0x0079_D654;
31
+ /// Master volume for WavPlayer playback (0-100).
32
+ global G_WAV_MASTER_VOLUME = 0x0069_7704;
33
+ /// Success sentinel: a result pointer equal to this address means "no error".
34
+ global G_WAV_RESULT_SUCCESS = 0x008A_C8A0;
35
+ }
36
+
37
+ /// Volume-to-dB attenuation table (65 entries of i16, indices 0-64).
38
+ /// Copied from WA.exe .rdata at 0x697680.
39
+ /// Index 0 = silence (-10000 cB), index 64 = unity (0 cB).
40
+ /// Used by WavPlayer_Play to convert the 0-100 master volume to DirectSound
41
+ /// hundredths-of-decibels via: `table[(vol * 64) / 100]`.
42
+ const WAV_VOLUME_DB_TABLE: [i16; 65] = [
43
+ -10000, -6000, -5000, -4415, -4000, -3678, -3415, -3000, -2744, -2522, -2326, -2150, -1991,
44
+ -1845, -1712, -1589, -1475, -1368, -1268, -1176, -1088, -1006, -928, -855, -786, -720, -658,
45
+ -599, -543, -490, -439, -390, -344, -299, -256, -216, -177, -139, -104, -70, -38, -7, 24, 54,
46
+ 83, 111, 138, 163, 188, 211, 234, 256, 277, 296, 315, 333, 350, 367, 383, 398, 413, 427, 441,
47
+ 454, 0,
48
+ ];
49
+
50
+ // ============================================================
51
+ // WavPlayer struct layout
52
+ // ============================================================
53
+
54
+ /// WavPlayer instance (8 bytes at a fixed global address).
55
+ ///
56
+ /// Layout matches WA.exe:
57
+ /// - `+0x00`: Pointer to static vtable/type data (0x651CC8)
58
+ /// - `+0x04`: Pointer to WavBuffer (null = nothing loaded)
59
+ #[repr(C)]
60
+ pub struct WavPlayer {
61
+ pub type_ptr: u32,
62
+ pub buffer: *mut WavBuffer,
63
+ }
64
+
65
+ /// Loaded WAV buffer (heap-allocated, owned by the WavPlayer system).
66
+ ///
67
+ /// The first two fields must match WA's layout because unhooked WA code
68
+ /// (FUN_005999c0) reads `filename` directly from buffer+4.
69
+ #[repr(C)]
70
+ pub struct WavBuffer {
71
+ /// Raw IDirectSoundBuffer COM pointer (stored as u32 for FFI).
72
+ pub ds_buffer: u32,
73
+ /// Null-terminated filename (C string, allocated via CString::into_raw).
74
+ pub filename: *mut c_char,
75
+ }
76
+
77
+ // ============================================================
78
+ // Helpers
79
+ // ============================================================
80
+
81
+ /// Read the WavPlayer IDirectSound* global. Returns 0 if not initialized.
82
+ #[inline]
83
+ unsafe fn wav_direct_sound_ptr() -> u32 {
84
+ *(rb(G_WAV_DIRECT_SOUND) as *const u32)
85
+ }
86
+
87
+ /// Read the WavPlayer master volume (0-100).
88
+ #[inline]
89
+ unsafe fn wav_master_volume() -> u32 {
90
+ *(rb(G_WAV_MASTER_VOLUME) as *const u32)
91
+ }
92
+
93
+ /// Read the success sentinel value.
94
+ #[inline]
95
+ pub unsafe fn wav_result_success() -> u32 {
96
+ *(rb(G_WAV_RESULT_SUCCESS) as *const u32)
97
+ }
98
+
99
+ /// Reinterpret a `*const u32` (pointing to a stored raw COM pointer) as
100
+ /// `&IDirectSoundBuffer`. The pointer must point to stable memory (e.g. a
101
+ /// struct field), NOT a stack local — the returned reference borrows it.
102
+ #[inline]
103
+ unsafe fn ds_buffer_from_field(field_ptr: *const u32) -> &'static IDirectSoundBuffer {
104
+ &*(field_ptr as *const IDirectSoundBuffer)
105
+ }
106
+
107
+ // ============================================================
108
+ // WavPlayer_Stop — port of 0x599670
109
+ // ============================================================
110
+
111
+ /// Stop and release the loaded DirectSound buffer.
112
+ ///
113
+ /// If no buffer is loaded (player.buffer == null), this is a no-op.
114
+ /// Otherwise: releases the DirectSound buffer COM object, frees the
115
+ /// filename string, and deallocates the WavBuffer struct.
116
+ pub unsafe fn wav_player_stop(player: *mut WavPlayer) {
117
+ let buf = (*player).buffer;
118
+ if buf.is_null() {
119
+ return;
120
+ }
121
+
122
+ // Free filename string (was allocated via CString::into_raw)
123
+ let filename = (*buf).filename;
124
+ if !filename.is_null() {
125
+ drop(CString::from_raw(filename));
126
+ }
127
+
128
+ // Stop and release DirectSound buffer
129
+ let ds_ptr = (*buf).ds_buffer;
130
+ if wav_direct_sound_ptr() != 0 && ds_ptr != 0 {
131
+ let ds_buf: IDirectSoundBuffer = core::mem::transmute_copy(&ds_ptr);
132
+ let _ = ds_buf.Stop();
133
+ // Drop calls Release()
134
+ }
135
+
136
+ // Free the WavBuffer struct (was allocated via Box::into_raw)
137
+ drop(Box::from_raw(buf));
138
+ (*player).buffer = core::ptr::null_mut();
139
+ }
140
+
141
+ // ============================================================
142
+ // WavPlayer_Play — port of 0x5996E0
143
+ // ============================================================
144
+
145
+ /// Rewind, set volume, and start playback on the loaded buffer.
146
+ ///
147
+ /// The `flags` parameter maps to IDirectSoundBuffer::Play dwFlags
148
+ /// (0 = play once, DSBPLAY_LOOPING = loop). The volume is read from
149
+ /// the global master volume at 0x697704 and converted via the dB table.
150
+ pub unsafe fn wav_player_play(player: *mut WavPlayer, flags: u32) {
151
+ let ds_raw = wav_direct_sound_ptr();
152
+ if ds_raw == 0 {
153
+ return;
154
+ }
155
+
156
+ let buf = (*player).buffer;
157
+ if buf.is_null() {
158
+ return;
159
+ }
160
+
161
+ if (*buf).ds_buffer == 0 {
162
+ return;
163
+ }
164
+
165
+ // Reference the COM pointer from the heap-allocated WavBuffer field
166
+ // (NOT from a stack copy — that would create a dangling reference).
167
+ let ds_buf = ds_buffer_from_field(&(*buf).ds_buffer);
168
+
169
+ // Rewind to start (port of FUN_00599930's SetCurrentPosition call)
170
+ let _ = ds_buf.SetCurrentPosition(0);
171
+
172
+ // Compute volume: (master_vol * 64) / 100, then look up in dB table
173
+ let master_vol = wav_master_volume();
174
+ let idx = ((master_vol << 6) / 100).min(64) as usize;
175
+ let vol_db = WAV_VOLUME_DB_TABLE[idx] as i32;
176
+ let _ = ds_buf.SetVolume(vol_db);
177
+
178
+ // Play
179
+ let _ = ds_buf.Play(0, 0, flags);
180
+ }
181
+
182
+ // ============================================================
183
+ // WavPlayer_LoadAndPlay — port of 0x599B40
184
+ // ============================================================
185
+
186
+ /// Parse a WAV file, create a DirectSound buffer, and store it in the player.
187
+ ///
188
+ /// Uses `hound` for WAV parsing and the `windows` crate for DirectSound.
189
+ /// Only PCM format is supported (WA.exe rejects non-PCM too).
190
+ ///
191
+ /// `param3` controls DSBCAPS_GLOBALFOCUS: if >= 0, the flag is set.
192
+ /// All known callers pass 0.
193
+ ///
194
+ /// Returns true on success, false on failure.
195
+ pub unsafe fn wav_player_load_and_play(
196
+ player: *mut WavPlayer,
197
+ path: *const c_char,
198
+ param3: i32,
199
+ ) -> bool {
200
+ // Need DirectSound
201
+ let ds_raw = wav_direct_sound_ptr();
202
+ if ds_raw == 0 {
203
+ return false;
204
+ }
205
+
206
+ // Stop and free current buffer
207
+ wav_player_stop(player);
208
+
209
+ // Open WAV file
210
+ let path_str = match CStr::from_ptr(path).to_str() {
211
+ Ok(s) => s,
212
+ Err(_) => return false,
213
+ };
214
+ let reader = match hound::WavReader::open(path_str) {
215
+ Ok(r) => r,
216
+ Err(_) => return false,
217
+ };
218
+ let spec = reader.spec();
219
+
220
+ // Only support PCM
221
+ if spec.sample_format != hound::SampleFormat::Int {
222
+ return false;
223
+ }
224
+
225
+ // Compute format parameters
226
+ let block_align = spec.channels * (spec.bits_per_sample / 8);
227
+ let avg_bytes_per_sec = spec.sample_rate * block_align as u32;
228
+ let data_len = reader.duration() * block_align as u32;
229
+
230
+ if data_len == 0 {
231
+ return false;
232
+ }
233
+
234
+ // Build WAVEFORMATEX
235
+ let wfx = WAVEFORMATEX {
236
+ wFormatTag: WAVE_FORMAT_PCM as u16,
237
+ nChannels: spec.channels,
238
+ nSamplesPerSec: spec.sample_rate,
239
+ nAvgBytesPerSec: avg_bytes_per_sec,
240
+ nBlockAlign: block_align,
241
+ wBitsPerSample: spec.bits_per_sample,
242
+ cbSize: 0,
243
+ };
244
+
245
+ // DSBUFFERDESC flags: DSBCAPS_STATIC (0x40) | DSBCAPS_CTRLVOLUME (0x80)
246
+ // Plus DSBCAPS_GLOBALFOCUS (0x8000) if param3 >= 0
247
+ let mut flags: u32 = 0xC0; // STATIC | CTRLVOLUME
248
+ if param3 >= 0 {
249
+ flags |= 0x8000; // GLOBALFOCUS
250
+ }
251
+
252
+ let desc = DSBUFFERDESC {
253
+ dwSize: core::mem::size_of::<DSBUFFERDESC>() as u32,
254
+ dwFlags: flags,
255
+ dwBufferBytes: data_len,
256
+ dwReserved: 0,
257
+ lpwfxFormat: &wfx as *const _ as *mut _,
258
+ ..Default::default()
259
+ };
260
+
261
+ // Create secondary buffer
262
+ let ds: &IDirectSound = &*(&ds_raw as *const u32 as *const IDirectSound);
263
+ let mut buf_opt: Option<IDirectSoundBuffer> = None;
264
+ if ds.CreateSoundBuffer(&desc, &mut buf_opt, None).is_err() {
265
+ return false;
266
+ }
267
+ let ds_buf = match buf_opt {
268
+ Some(b) => b,
269
+ None => return false,
270
+ };
271
+
272
+ // Lock the entire buffer
273
+ let mut audio_ptr: *mut c_void = core::ptr::null_mut();
274
+ let mut audio_len: u32 = 0;
275
+ if ds_buf
276
+ .Lock(
277
+ 0,
278
+ data_len,
279
+ &mut audio_ptr,
280
+ &mut audio_len,
281
+ None,
282
+ None,
283
+ DSBLOCK_ENTIREBUFFER,
284
+ )
285
+ .is_err()
286
+ {
287
+ return false;
288
+ }
289
+
290
+ // Fill with PCM data from the WAV file
291
+ {
292
+ let dest = core::slice::from_raw_parts_mut(audio_ptr as *mut u8, audio_len as usize);
293
+ match spec.bits_per_sample {
294
+ 16 => {
295
+ // Re-open to get fresh sample iterator
296
+ if let Ok(mut reader) = hound::WavReader::open(path_str) {
297
+ let mut offset = 0usize;
298
+ for s in reader.samples::<i16>().flatten() {
299
+ if offset + 2 <= dest.len() {
300
+ dest[offset..offset + 2].copy_from_slice(&s.to_le_bytes());
301
+ offset += 2;
302
+ }
303
+ }
304
+ }
305
+ }
306
+ 8 => {
307
+ if let Ok(mut reader) = hound::WavReader::open(path_str) {
308
+ let mut offset = 0usize;
309
+ for s in reader.samples::<i16>().flatten() {
310
+ if offset < dest.len() {
311
+ dest[offset] = (s + 128) as u8;
312
+ offset += 1;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ _ => {
318
+ dest.fill(0);
319
+ }
320
+ }
321
+ }
322
+
323
+ let _ = ds_buf.Unlock(audio_ptr, audio_len, None, 0);
324
+
325
+ // Store filename (CString for proper deallocation in wav_player_stop)
326
+ let filename = match CString::new(path_str) {
327
+ Ok(cs) => cs.into_raw(),
328
+ Err(_) => core::ptr::null_mut(),
329
+ };
330
+
331
+ // Store DS buffer as raw COM pointer (we take ownership)
332
+ let ds_raw_ptr: u32 = core::mem::transmute_copy(&ds_buf);
333
+ core::mem::forget(ds_buf); // Don't Release — stored in WavBuffer
334
+
335
+ // Allocate WavBuffer and store in player
336
+ let wav_buf = Box::new(WavBuffer {
337
+ ds_buffer: ds_raw_ptr,
338
+ filename,
339
+ });
340
+ (*player).buffer = Box::into_raw(wav_buf);
341
+
342
+ true
343
+ }
crates/openwa-wormkit/src/replacements/speech.rs CHANGED
@@ -1,190 +1,112 @@
1
- //! Speech / fanfare / FE SFX hooks.
 
 
 
2
  //!
3
  //! Hooks:
 
 
 
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};
12
-
13
- use heapless::CString;
14
 
15
- use openwa_core::address::va::{self, game_info_offsets};
16
- use openwa_core::audio::SpeechLineTableEntry;
 
17
  use openwa_core::engine::DDGameWrapper;
18
  use openwa_core::rebase::rb;
19
 
20
  use crate::hook::{self, usercall_trampoline};
21
  use crate::log_line;
22
 
23
- /// Windows MAX_PATH (260 bytes including nul terminator).
24
- const MAX_PATH: usize = 260;
25
-
26
  // ============================================================
27
- // WavPlayerHandle — typed wrapper for WA's WavPlayer instances
28
  // ============================================================
29
 
30
- /// Handle to a WA.exe WavPlayer instance (opaque, unknown size).
31
- ///
32
- /// Known globals:
33
- /// - `va::FESFX_WAV_PLAYER` (0x6AC888) — frontend sound effects
34
- /// - `va::FANFARE_WAV_PLAYER` (0x6AC890) — team fanfares
35
- #[derive(Clone, Copy)]
36
- struct WavPlayerHandle(u32);
37
-
38
- impl WavPlayerHandle {
39
- /// Read a WavPlayerHandle from a Ghidra global address.
40
- unsafe fn from_global(ghidra_addr: u32) -> Self {
41
- Self(rb(ghidra_addr))
42
- }
43
-
44
- /// Stop and release the current DirectSound buffer.
45
- /// WavPlayer_Stop: usercall(ESI=player, EDI=&result), plain RET.
46
- unsafe fn stop(self) {
47
- wav_player_stop_raw(self.0, rb(va::WAV_PLAYER_STOP));
48
- }
49
-
50
- /// Open a WAV file, parse RIFF, create DirectSound buffer.
51
- /// WavPlayer_LoadAndPlay: usercall(ESI=&result) + stack(player, path, 0), RET 0xC.
52
- unsafe fn load(self, path: *const c_char) {
53
- wav_player_load_raw(self.0, path.cast(), rb(va::WAV_PLAYER_LOAD_AND_PLAY));
54
- }
55
-
56
- /// Play the loaded buffer at the given volume.
57
- /// WavPlayer_Play: usercall(EDI=&result) + stack(player, volume), RET 0x8.
58
- unsafe fn play(self, volume: u32) {
59
- wav_player_play_raw(self.0, volume, rb(va::WAV_PLAYER_PLAY));
60
- }
61
- }
62
-
63
- // Naked asm wrappers for WavPlayer usercall functions.
64
- // ESI/EDI are LLVM-reserved, so we must use naked functions.
65
-
66
- /// cdecl(player, func_addr) — sets ESI=player, EDI=&result, calls func.
67
  #[unsafe(naked)]
68
- unsafe extern "cdecl" fn wav_player_stop_raw(_player: u32, _func: u32) {
69
  core::arch::naked_asm!(
70
  "push esi",
71
- "push edi",
72
- "sub esp, 4", // result on stack
73
- "mov esi, [esp + 16]", // player (arg1, past saved esi+edi+result)
74
- "lea edi, [esp]", // &result
75
- "call [esp + 20]", // func (arg2)
76
- "add esp, 4", // drop result
77
- "pop edi",
78
- "pop esi",
79
  "ret",
 
 
80
  );
81
  }
82
 
83
- /// cdecl(player, path, func_addr) — pushes stack params, sets ESI=&result, calls func.
84
- /// Stack layout after push esi + sub esp,4:
85
- /// ESP+0=result, +4=saved_esi, +8=retaddr, +12=player, +16=path, +20=func
86
- /// Callee expects stack params: [ESP+4]=player, [ESP+8]=path, [ESP+12]=0
 
87
  #[unsafe(naked)]
88
- unsafe extern "cdecl" fn wav_player_load_raw(_player: u32, _path: *const u8, _func: u32) {
89
  core::arch::naked_asm!(
90
- "push esi",
91
- "sub esp, 4", // result on stack
92
- "lea esi, [esp]", // ESI = &result
93
- // Push in reverse order: 0, path, player
94
- "push 0", // third param=0 (ESP: +16=player, +20=path, +24=func)
95
- "push dword ptr [esp+20]", // second param=path (ESP: +20=player, +24=path, +28=func)
96
- "push dword ptr [esp+20]", // first param=player (ESP: +28=path, +32=func)
97
- "call dword ptr [esp+32]", // call func; RET 0xC cleans 3 params
98
- "add esp, 4", // drop result
99
- "pop esi",
100
- "ret",
101
  );
102
  }
103
 
104
- /// cdecl(player, volume, func_addr) — pushes stack params, sets EDI=&result, calls func.
105
- /// Stack layout after push edi + sub esp,4:
106
- /// ESP+0=result, +4=saved_edi, +8=retaddr, +12=player, +16=volume, +20=func
107
- /// Callee expects stack params: [ESP+4]=player, [ESP+8]=volume
 
108
  #[unsafe(naked)]
109
- unsafe extern "cdecl" fn wav_player_play_raw(_player: u32, _volume: u32, _func: u32) {
110
  core::arch::naked_asm!(
111
- "push edi",
112
- "sub esp, 4", // result on stack
113
- "lea edi, [esp]", // EDI = &result
114
- // Push in reverse order: volume, player
115
- "push dword ptr [esp+16]", // second param=volume (ESP: +16=player, +24=func)
116
- "push dword ptr [esp+16]", // first param=player (ESP: +28=func)
117
- "call dword ptr [esp+28]", // call func; RET 0x8 cleans 2 params
118
- "add esp, 4", // drop result
119
- "pop edi",
120
- "ret",
 
 
 
 
 
 
 
121
  );
122
  }
123
 
124
- // ============================================================
125
- // Team config name lookup (port of GetTeamConfigName 0x4A62A0)
126
- // ============================================================
 
 
 
 
127
 
128
- /// Team config names indexed 0-48, matching WA.exe's jump table at 0x4A62A0.
129
- /// Used for fanfare WAV file paths (e.g. "user\Fanfare\Finland.wav").
130
- /// Note: "Leichtenstein" is WA's original misspelling — must match for file lookup.
131
- const TEAM_CONFIG_NAMES: [&str; 49] = [
132
- "UK", // 0
133
- "Argentina", // 1
134
- "Australia", // 2
135
- "Austria", // 3
136
- "Belgium", // 4
137
- "Brazil", // 5
138
- "Canada", // 6
139
- "Croatia", // 7
140
- "Simple", // 8
141
- "Cyprus", // 9
142
- "Simple", // 10
143
- "Denmark", // 11
144
- "Finland", // 12
145
- "France", // 13
146
- "Simple", // 14
147
- "Germany", // 15
148
- "Greece", // 16
149
- "Simple", // 17
150
- "Hungary", // 18
151
- "Iceland", // 19
152
- "India", // 20
153
- "Simple", // 21
154
- "Psycho Laugh", // 22
155
- "Psycho Laugh", // 23
156
- "Ireland", // 24
157
- "Israel", // 25
158
- "Italy", // 26
159
- "Japan", // 27
160
- "Leichtenstein", // 28
161
- "Luxembourg", // 29
162
- "Simple", // 30
163
- "Malta", // 31
164
- "Mexico", // 32
165
- "Morocco", // 33
166
- "Netherlands", // 34
167
- "New Zealand", // 35
168
- "Norway", // 36
169
- "Poland", // 37
170
- "Portugal", // 38
171
- "Simple", // 39
172
- "Romania", // 40
173
- "Russia", // 41
174
- "Singapore", // 42
175
- "South Africa", // 43
176
- "Spain", // 44
177
- "Sweden", // 45
178
- "Switzerland", // 46
179
- "Turkey", // 47
180
- "USA", // 48
181
- ];
182
 
183
  // ============================================================
184
- // PlayFeSfx replacement (0x4D7960)
185
  // ============================================================
186
- // stdcall(sfx_name: *const u8), RET 0x4
187
- // Builds "fesfx\<name>.wav", plays on FESFX_WAV_PLAYER.
188
 
189
  unsafe extern "stdcall" fn hook_play_fe_sfx(sfx_name: *const u8) {
190
  let name = if !sfx_name.is_null() {
@@ -195,180 +117,39 @@ unsafe extern "stdcall" fn hook_play_fe_sfx(sfx_name: *const u8) {
195
  "(null)"
196
  };
197
  let _ = log_line(&format!("[Speech] PlayFeSfx: \"{}\"", name));
198
-
199
- // Build null-terminated path: "fesfx\<name>.wav"
200
- let mut path = CString::<MAX_PATH>::new();
201
- let _ = path.extend_from_bytes(b"fesfx\\");
202
- let _ = path.extend_from_bytes(name.as_bytes());
203
- let _ = path.extend_from_bytes(b".wav");
204
-
205
- let player = WavPlayerHandle::from_global(va::FESFX_WAV_PLAYER);
206
- player.stop();
207
- player.load(path.as_ptr());
208
- player.play(0);
209
  }
210
 
211
  // ============================================================
212
- // PlayFanfare_Default replacement (0x4D7500)
213
  // ============================================================
214
- // stdcall(team_type: u32), RET 0x4
215
- // If team_type in 1..=49, looks up config name; else "Simple".
216
- // Builds "<wa_path>\user\Fanfare\<name>.wav", plays on FANFARE_WAV_PLAYER.
217
 
218
  unsafe extern "stdcall" fn hook_play_fanfare_default(team_type: u32) {
219
- // Get the fanfare name from lookup table (port of GetTeamConfigName 0x4A62A0)
220
- let name = if (1..=49).contains(&team_type) {
221
- TEAM_CONFIG_NAMES[(team_type - 1) as usize]
222
- } else {
223
- "Simple"
224
- };
225
-
226
- // Read WA data path
227
- let wa_path = CStr::from_ptr(rb(va::WA_DATA_PATH) as *const i8)
228
- .to_str()
229
- .unwrap_or(".");
230
-
231
  let _ = log_line(&format!(
232
- "[Speech] PlayFanfare_Default: team_type={} name=\"{}\"",
233
- team_type, name
234
  ));
235
-
236
- // Build null-terminated path: "<wa_path>\user\Fanfare\<name>.wav"
237
- let mut path = CString::<MAX_PATH>::new();
238
- let _ = path.extend_from_bytes(wa_path.as_bytes());
239
- let _ = path.extend_from_bytes(b"\\user\\Fanfare\\");
240
- let _ = path.extend_from_bytes(name.as_bytes());
241
- let _ = path.extend_from_bytes(b".wav");
242
-
243
- let player = WavPlayerHandle::from_global(va::FANFARE_WAV_PLAYER);
244
- player.stop();
245
- player.load(path.as_ptr());
246
- player.play(0);
247
  }
248
 
249
  // ============================================================
250
- // PlayFanfare_CurrentTeam passthrough (0x4D78E0)
251
  // ============================================================
252
- // usercall(EAX=index), plain RET, returns EAX = u32
253
- // EAX is used as an implicit input (MOV EDI,EAX at 0x4D78E8).
254
-
255
- static ORIG_PLAY_FANFARE_CURRENT_TEAM: AtomicU32 = AtomicU32::new(0);
256
 
257
  usercall_trampoline!(fn trampoline_play_fanfare_current_team;
258
  impl_fn = play_fanfare_current_team_impl; reg = eax);
259
 
260
- unsafe extern "cdecl" fn play_fanfare_current_team_impl(index: u32) -> u32 {
261
- let _ = log_line(&format!("[Speech] PlayFanfare_CurrentTeam: eax={}", index));
262
-
263
- let orig = ORIG_PLAY_FANFARE_CURRENT_TEAM.load(Ordering::Relaxed);
264
- let result: u32;
265
- core::arch::asm!(
266
- "call {orig}",
267
- orig = in(reg) orig,
268
- in("eax") index,
269
- lateout("eax") result,
270
- clobber_abi("C"),
271
- );
272
-
273
- let _ = log_line(&format!("[Speech] PlayFanfare_CurrentTeam => {}", result));
274
- result
275
- }
276
-
277
- // ============================================================
278
- // DDGameWrapper__LoadSpeechWAV — pure Rust (replaces 0x571530)
279
- // ============================================================
280
- // Searches DDGameWrapper's speech name table for existing WAV, reuses slot if found.
281
- // Otherwise calls DSSound vtable load_wav to load new WAV into a buffer.
282
- // Stores the buffer slot index in the SpeechSlotTable.
283
-
284
- /// Pure Rust replacement for DDGameWrapper__LoadSpeechWAV (0x571530).
285
- ///
286
- /// Searches the name table for `wav_path`. If found, reuses the existing
287
- /// buffer slot. Otherwise calls DSSound::load_wav to create a new buffer.
288
- /// Returns 1 on success, 0 on failure.
289
- unsafe fn call_load_speech_wav(
290
- ddgw: u32,
291
- team_index: u32,
292
- line_id: u32,
293
- wav_path: *const c_char,
294
- full_path: *const c_char,
295
- ) -> u32 {
296
- use openwa_core::audio::SpeechSlotTable;
297
- use openwa_core::engine::ddgame_wrapper::SPEECH_NAME_ENTRY_SIZE;
298
- use openwa_core::engine::DDGameWrapper;
299
-
300
- let wrapper = &mut *(ddgw as *mut DDGameWrapper);
301
- let count = wrapper.speech_name_count as usize;
302
- let search_name = core::ffi::CStr::from_ptr(wav_path).to_bytes();
303
-
304
- // Search name table for existing entry matching wav_path.
305
- let mut found_idx: Option<usize> = None;
306
- for i in 0..count {
307
- let entry = &wrapper.speech_name_table[i];
308
- // Compare up to the null terminator.
309
- let entry_len = entry.iter().position(|&b| b == 0).unwrap_or(entry.len());
310
- if entry[..entry_len] == *search_name {
311
- found_idx = Some(i);
312
- break;
313
- }
314
- }
315
-
316
- let ddgame = &mut *wrapper.ddgame;
317
- let slot_table = &mut ddgame.speech_slot_table;
318
-
319
- if let Some(idx) = found_idx {
320
- // Found existing — reuse the buffer slot.
321
- slot_table.set(
322
- team_index as usize,
323
- line_id,
324
- idx as u32 + SpeechSlotTable::BUFFER_OFFSET,
325
- );
326
- return 1;
327
- }
328
-
329
- // Not found — load new WAV via DSSound vtable slot 12 (load_wav).
330
- let slot_idx = count as u32 + SpeechSlotTable::BUFFER_OFFSET;
331
- let vtable = (*wrapper.sound).vtable;
332
- let result = ((*vtable).load_wav)(wrapper.sound, slot_idx as i32, full_path as *const u8);
333
-
334
- if result != 0 {
335
- // Success — store slot, copy name into table, increment count.
336
- slot_table.set(team_index as usize, line_id, slot_idx);
337
-
338
- // Copy wav_path into name table entry.
339
- let dest = &mut wrapper.speech_name_table[count];
340
- let copy_len = search_name.len().min(SPEECH_NAME_ENTRY_SIZE - 1);
341
- dest[..copy_len].copy_from_slice(&search_name[..copy_len]);
342
- dest[copy_len] = 0;
343
-
344
- wrapper.speech_name_count = (count + 1) as u32;
345
- return 1;
346
- }
347
-
348
- 0 // failure
349
- }
350
-
351
- // ============================================================
352
- // Helpers
353
- // ============================================================
354
-
355
- /// Extract bytes from a null-terminated C string pointer.
356
- unsafe fn cstr_bytes<'a>(ptr: *const u8) -> &'a [u8] {
357
- if ptr.is_null() {
358
- return &[];
359
- }
360
- CStr::from_ptr(ptr as *const i8).to_bytes()
361
  }
362
 
363
  // ============================================================
364
- // DSSound_LoadSpeechBank replacement (0x571660)
365
  // ============================================================
366
- // usercall(EAX=DDGameWrapper) + 3 stack params (team_index, speech_base_path, speech_dir)
367
- // RET 0xC
368
- //
369
- // Iterates the speech line table at 0x6AF770. For each entry, builds WAV paths
370
- // and calls LoadSpeechWAV. Falls back to default speech dir on failure.
371
- // Skips remaining filename alternates when one succeeds.
372
 
373
  usercall_trampoline!(fn trampoline_load_speech_bank; impl_fn = load_speech_bank_impl;
374
  reg = eax; stack_params = 3; ret_bytes = "0xC");
@@ -386,121 +167,25 @@ unsafe extern "cdecl" fn load_speech_bank_impl(
386
  .to_str()
387
  .unwrap_or("?");
388
  let _ = log_line(&format!(
389
- "[Speech] LoadSpeechBank: team={} path=\"{}\" dir=\"{}\" ddgw=0x{:08X}",
390
- team_index, path_str, dir_str, ddgw as u32
391
  ));
392
-
393
- let table_base = rb(va::SPEECH_LINE_TABLE) as *const SpeechLineTableEntry;
394
- let mut i: usize = 0;
395
-
396
- loop {
397
- let entry = &*table_base.add(i);
398
-
399
- // Null name_ptr = end of table
400
- if entry.name_ptr.is_null() {
401
- return;
402
- }
403
-
404
- let name_bytes = cstr_bytes(entry.name_ptr);
405
-
406
- // Build wav_path: "<speech_dir>\<name>.wav"
407
- let mut wav_path = CString::<MAX_PATH>::new();
408
- let _ = wav_path.extend_from_bytes(cstr_bytes(speech_dir));
409
- let _ = wav_path.extend_from_bytes(b"\\");
410
- let _ = wav_path.extend_from_bytes(name_bytes);
411
- let _ = wav_path.extend_from_bytes(b".wav");
412
-
413
- // Build full_path: "<speech_base_path>\<wav_path>"
414
- let mut full_path = CString::<MAX_PATH>::new();
415
- let _ = full_path.extend_from_bytes(cstr_bytes(speech_base_path));
416
- let _ = full_path.extend_from_bytes(b"\\");
417
- let _ = full_path.extend_from_bytes(wav_path.as_bytes());
418
-
419
- let result = call_load_speech_wav(
420
- ddgw as u32,
421
- team_index,
422
- entry.id,
423
- wav_path.as_ptr(),
424
- full_path.as_ptr(),
425
- );
426
-
427
- if result == 0 {
428
- // Failed — try fallback with default speech dir if no more alternates
429
- let next_entry = &*table_base.add(i + 1);
430
- if next_entry.name_ptr.is_null() || next_entry.id != entry.id {
431
- let game_info = (*(*ddgw).ddgame).game_info as *const u8;
432
- let default_dir = game_info.add(game_info_offsets::DEFAULT_SPEECH_DIR as usize);
433
- let default_base =
434
- game_info.add(game_info_offsets::DEFAULT_SPEECH_BASE_PATH as usize);
435
-
436
- let mut wav_path2 = CString::<MAX_PATH>::new();
437
- let _ = wav_path2.extend_from_bytes(cstr_bytes(default_dir));
438
- let _ = wav_path2.extend_from_bytes(b"\\");
439
- let _ = wav_path2.extend_from_bytes(name_bytes);
440
- let _ = wav_path2.extend_from_bytes(b".wav");
441
-
442
- let mut full_path2 = CString::<MAX_PATH>::new();
443
- let _ = full_path2.extend_from_bytes(cstr_bytes(default_base));
444
- let _ = full_path2.extend_from_bytes(b"\\");
445
- let _ = full_path2.extend_from_bytes(wav_path2.as_bytes());
446
-
447
- call_load_speech_wav(
448
- ddgw as u32,
449
- team_index,
450
- entry.id,
451
- wav_path2.as_ptr(),
452
- full_path2.as_ptr(),
453
- );
454
- }
455
- } else {
456
- // Success — skip remaining filename alternates for this ID
457
- let current_id = entry.id;
458
- while {
459
- let next = &*table_base.add(i + 1);
460
- !next.name_ptr.is_null() && next.id == current_id
461
- } {
462
- i += 1;
463
- }
464
- }
465
-
466
- i += 1;
467
- }
468
  }
469
 
470
  // ============================================================
471
- // DSSound_LoadAllSpeechBanks replacement (0x571A70)
472
  // ============================================================
473
- // usercall(ESI=DDGameWrapper), plain RET.
474
- // Clears the speech slot table, then iterates teams and calls LoadSpeechBank.
475
 
476
  usercall_trampoline!(fn trampoline_load_all_speech_banks;
477
  impl_fn = load_all_speech_banks_impl; reg = esi);
478
 
479
  unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: *const DDGameWrapper) {
480
- let ddgame = &mut *(*ddgw).ddgame;
481
-
482
- // Clear speech slot table
483
- ddgame.speech_slot_table.clear();
484
-
485
- // Read team count from GameInfo
486
- let game_info = &*ddgame.game_info;
487
- let team_count = game_info.speech_team_count as u32;
488
-
489
  let _ = log_line(&format!(
490
- "[Speech] LoadAllSpeechBanks: ddgw=0x{:08X} teams={}",
491
- ddgw as u32, team_count
492
  ));
493
-
494
- // Load speech bank for each team
495
- let gi = ddgame.game_info as *const u8;
496
- for i in 0..team_count {
497
- let team_offset = (i * game_info_offsets::SPEECH_TEAM_STRIDE) as usize;
498
- let base_path = gi.add(game_info_offsets::SPEECH_BASE_PATH as usize + team_offset);
499
- let dir = gi.add(game_info_offsets::SPEECH_DIR as usize + team_offset);
500
-
501
- // Call our Rust impl directly (bypasses the hook trampoline)
502
- load_speech_bank_impl(ddgw, i, base_path, dir);
503
- }
504
  }
505
 
506
  // ============================================================
@@ -509,29 +194,43 @@ unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: *const DDGameWrapper)
509
 
510
  pub fn install() -> Result<(), String> {
511
  unsafe {
512
- hook::install("PlayFeSfx", va::PLAY_FE_SFX, hook_play_fe_sfx as *const ())?;
 
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  hook::install(
515
  "PlayFanfare_Default",
516
  va::PLAY_FANFARE_DEFAULT,
517
  hook_play_fanfare_default as *const (),
518
  )?;
519
-
520
- let trampoline = hook::install(
521
  "PlayFanfare_CurrentTeam",
522
  va::PLAY_FANFARE_CURRENT_TEAM,
523
  trampoline_play_fanfare_current_team as *const (),
524
  )?;
525
- ORIG_PLAY_FANFARE_CURRENT_TEAM.store(trampoline as u32, Ordering::Relaxed);
526
-
527
- // Full replacement — no trampoline needed
528
  hook::install(
529
  "DSSound_LoadSpeechBank",
530
  va::DSSOUND_LOAD_SPEECH_BANK,
531
  trampoline_load_speech_bank as *const (),
532
  )?;
533
-
534
- // Full replacement — no trampoline needed
535
  hook::install(
536
  "DSSound_LoadAllSpeechBanks",
537
  va::DSSOUND_LOAD_ALL_SPEECH_BANKS,
 
1
+ //! Speech / fanfare / FE SFX / WavPlayer hooks.
2
+ //!
3
+ //! Thin hook shims — game logic lives in `openwa_core::audio::speech_ops`
4
+ //! and `openwa_core::audio::wav_player`.
5
  //!
6
  //! Hooks:
7
+ //! - WavPlayer_Stop (0x599670): usercall(ESI=player, EDI=&result), plain RET — REPLACED
8
+ //! - WavPlayer_Play (0x5996E0): usercall(EDI=&result) + stack(player, flags), RET 0x8 — REPLACED
9
+ //! - WavPlayer_LoadAndPlay (0x599B40): usercall(ESI=&result) + stack(player, path, param3), RET 0xC — REPLACED
10
  //! - PlayFeSfx (0x4D7960): stdcall(sfx_name), RET 0x4 — REPLACED
11
  //! - PlayFanfare_Default (0x4D7500): stdcall(team_type), RET 0x4 — REPLACED
12
+ //! - PlayFanfare_CurrentTeam (0x4D78E0): usercall(EAX=index), plain RET — REPLACED
13
  //! - DSSound_LoadSpeechBank (0x571660): usercall(EAX=DDGameWrapper) + 3 stack, RET 0xC — REPLACED
14
  //! - DSSound_LoadAllSpeechBanks (0x571A70): usercall(ESI=DDGameWrapper), plain RET — REPLACED
15
 
16
  use std::ffi::{c_char, CStr};
 
 
 
17
 
18
+ use openwa_core::address::va;
19
+ use openwa_core::audio::{speech_ops, wav_player};
20
+ use openwa_core::audio::wav_player::WavPlayer;
21
  use openwa_core::engine::DDGameWrapper;
22
  use openwa_core::rebase::rb;
23
 
24
  use crate::hook::{self, usercall_trampoline};
25
  use crate::log_line;
26
 
 
 
 
27
  // ============================================================
28
+ // WavPlayer hook trampolines (usercall → cdecl shims)
29
  // ============================================================
30
 
31
+ // WavPlayer_Stop: usercall(ESI=player, EDI=&result), plain RET.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  #[unsafe(naked)]
33
+ unsafe extern "C" fn hook_wav_player_stop() {
34
  core::arch::naked_asm!(
35
  "push esi",
36
+ "call {impl_fn}",
37
+ "add esp, 4",
38
+ "mov eax, [{success_val}]",
39
+ "mov [edi], eax",
40
+ "mov eax, edi",
 
 
 
41
  "ret",
42
+ impl_fn = sym wav_player_stop_impl,
43
+ success_val = sym WAV_RESULT_SUCCESS_VALUE,
44
  );
45
  }
46
 
47
+ unsafe extern "cdecl" fn wav_player_stop_impl(player: *mut WavPlayer) {
48
+ wav_player::wav_player_stop(player);
49
+ }
50
+
51
+ // WavPlayer_Play: usercall(EDI=&result) + stack(player, flags), RET 0x8.
52
  #[unsafe(naked)]
53
+ unsafe extern "C" fn hook_wav_player_play() {
54
  core::arch::naked_asm!(
55
+ "push dword ptr [esp+8]",
56
+ "push dword ptr [esp+8]",
57
+ "call {impl_fn}",
58
+ "add esp, 8",
59
+ "mov eax, [{success_val}]",
60
+ "mov [edi], eax",
61
+ "mov eax, edi",
62
+ "ret 0x8",
63
+ impl_fn = sym wav_player_play_impl,
64
+ success_val = sym WAV_RESULT_SUCCESS_VALUE,
 
65
  );
66
  }
67
 
68
+ unsafe extern "cdecl" fn wav_player_play_impl(player: *mut WavPlayer, flags: u32) {
69
+ wav_player::wav_player_play(player, flags);
70
+ }
71
+
72
+ // WavPlayer_LoadAndPlay: usercall(ESI=&result) + stack(player, path, param3), RET 0xC.
73
  #[unsafe(naked)]
74
+ unsafe extern "C" fn hook_wav_player_load_and_play() {
75
  core::arch::naked_asm!(
76
+ "push dword ptr [esp+12]",
77
+ "push dword ptr [esp+12]",
78
+ "push dword ptr [esp+12]",
79
+ "call {impl_fn}",
80
+ "add esp, 12",
81
+ "test eax, eax",
82
+ "jz 2f",
83
+ "mov eax, [{success_val}]",
84
+ "mov [esi], eax",
85
+ "mov eax, esi",
86
+ "ret 0xC",
87
+ "2:",
88
+ "mov dword ptr [esi], 0xFFFFFFFF",
89
+ "mov eax, esi",
90
+ "ret 0xC",
91
+ impl_fn = sym wav_player_load_and_play_impl,
92
+ success_val = sym WAV_RESULT_SUCCESS_VALUE,
93
  );
94
  }
95
 
96
+ unsafe extern "cdecl" fn wav_player_load_and_play_impl(
97
+ player: *mut WavPlayer,
98
+ path: *const c_char,
99
+ param3: i32,
100
+ ) -> u32 {
101
+ wav_player::wav_player_load_and_play(player, path, param3) as u32
102
+ }
103
 
104
+ /// The success sentinel VALUE (read from *(0x8AC8A0+delta) at install time).
105
+ static mut WAV_RESULT_SUCCESS_VALUE: u32 = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  // ============================================================
108
+ // PlayFeSfx hook (0x4D7960)
109
  // ============================================================
 
 
110
 
111
  unsafe extern "stdcall" fn hook_play_fe_sfx(sfx_name: *const u8) {
112
  let name = if !sfx_name.is_null() {
 
117
  "(null)"
118
  };
119
  let _ = log_line(&format!("[Speech] PlayFeSfx: \"{}\"", name));
120
+ speech_ops::play_fe_sfx(name);
 
 
 
 
 
 
 
 
 
 
121
  }
122
 
123
  // ============================================================
124
+ // PlayFanfare_Default hook (0x4D7500)
125
  // ============================================================
 
 
 
126
 
127
  unsafe extern "stdcall" fn hook_play_fanfare_default(team_type: u32) {
 
 
 
 
 
 
 
 
 
 
 
 
128
  let _ = log_line(&format!(
129
+ "[Speech] PlayFanfare_Default: team_type={}",
130
+ team_type
131
  ));
132
+ speech_ops::play_fanfare_default(team_type);
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
 
135
  // ============================================================
136
+ // PlayFanfare_CurrentTeam hook (0x4D78E0)
137
  // ============================================================
 
 
 
 
138
 
139
  usercall_trampoline!(fn trampoline_play_fanfare_current_team;
140
  impl_fn = play_fanfare_current_team_impl; reg = eax);
141
 
142
+ unsafe extern "cdecl" fn play_fanfare_current_team_impl(eax_index: u32) -> u32 {
143
+ let _ = log_line(&format!(
144
+ "[Speech] PlayFanfare_CurrentTeam: eax={}",
145
+ eax_index
146
+ ));
147
+ speech_ops::play_fanfare_current_team(eax_index)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
  // ============================================================
151
+ // DSSound_LoadSpeechBank hook (0x571660)
152
  // ============================================================
 
 
 
 
 
 
153
 
154
  usercall_trampoline!(fn trampoline_load_speech_bank; impl_fn = load_speech_bank_impl;
155
  reg = eax; stack_params = 3; ret_bytes = "0xC");
 
167
  .to_str()
168
  .unwrap_or("?");
169
  let _ = log_line(&format!(
170
+ "[Speech] LoadSpeechBank: team={} path=\"{}\" dir=\"{}\"",
171
+ team_index, path_str, dir_str
172
  ));
173
+ speech_ops::load_speech_bank(ddgw, team_index, speech_base_path, speech_dir);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  }
175
 
176
  // ============================================================
177
+ // DSSound_LoadAllSpeechBanks hook (0x571A70)
178
  // ============================================================
 
 
179
 
180
  usercall_trampoline!(fn trampoline_load_all_speech_banks;
181
  impl_fn = load_all_speech_banks_impl; reg = esi);
182
 
183
  unsafe extern "cdecl" fn load_all_speech_banks_impl(ddgw: *const DDGameWrapper) {
 
 
 
 
 
 
 
 
 
184
  let _ = log_line(&format!(
185
+ "[Speech] LoadAllSpeechBanks: ddgw=0x{:08X}",
186
+ ddgw as u32
187
  ));
188
+ speech_ops::load_all_speech_banks(ddgw);
 
 
 
 
 
 
 
 
 
 
189
  }
190
 
191
  // ============================================================
 
194
 
195
  pub fn install() -> Result<(), String> {
196
  unsafe {
197
+ // Read the success sentinel VALUE for hook trampolines.
198
+ WAV_RESULT_SUCCESS_VALUE = *(rb(wav_player::G_WAV_RESULT_SUCCESS) as *const u32);
199
 
200
+ // Global WavPlayer hooks — all callers (our hooks + unhooked WA code)
201
+ // go through Rust. Avoids CRT heap mismatch.
202
+ hook::install(
203
+ "WavPlayer_Stop",
204
+ va::WAV_PLAYER_STOP,
205
+ hook_wav_player_stop as *const (),
206
+ )?;
207
+ hook::install(
208
+ "WavPlayer_Play",
209
+ va::WAV_PLAYER_PLAY,
210
+ hook_wav_player_play as *const (),
211
+ )?;
212
+ hook::install(
213
+ "WavPlayer_LoadAndPlay",
214
+ va::WAV_PLAYER_LOAD_AND_PLAY,
215
+ hook_wav_player_load_and_play as *const (),
216
+ )?;
217
+
218
+ hook::install("PlayFeSfx", va::PLAY_FE_SFX, hook_play_fe_sfx as *const ())?;
219
  hook::install(
220
  "PlayFanfare_Default",
221
  va::PLAY_FANFARE_DEFAULT,
222
  hook_play_fanfare_default as *const (),
223
  )?;
224
+ hook::install(
 
225
  "PlayFanfare_CurrentTeam",
226
  va::PLAY_FANFARE_CURRENT_TEAM,
227
  trampoline_play_fanfare_current_team as *const (),
228
  )?;
 
 
 
229
  hook::install(
230
  "DSSound_LoadSpeechBank",
231
  va::DSSOUND_LOAD_SPEECH_BANK,
232
  trampoline_load_speech_bank as *const (),
233
  )?;
 
 
234
  hook::install(
235
  "DSSound_LoadAllSpeechBanks",
236
  va::DSSOUND_LOAD_ALL_SPEECH_BANKS,