paavohtl Claude Opus 4.6 commited on
Commit
c09eb2f
·
1 Parent(s): 5f6c289

feat: replace PlayFeSfx and PlayFanfare_Default with full Rust implementations

Browse files

Convert two speech passthrough hooks into full replacements that build
file paths in Rust via heapless::CString and delegate WAV playback to
WA's WavPlayer functions through naked asm wrappers (WavPlayerHandle).

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

Cargo.lock CHANGED
@@ -2,6 +2,12 @@
2
  # It is not intended for manual editing.
3
  version = 4
4
 
 
 
 
 
 
 
5
  [[package]]
6
  name = "cc"
7
  version = "1.2.56"
@@ -18,6 +24,25 @@ version = "0.1.9"
18
  source = "registry+https://github.com/rust-lang/crates.io-index"
19
  checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  [[package]]
22
  name = "libc"
23
  version = "0.2.182"
@@ -71,6 +96,7 @@ version = "0.1.0"
71
  name = "openwa-wormkit"
72
  version = "0.1.0"
73
  dependencies = [
 
74
  "minhook",
75
  "openwa-lib",
76
  "openwa-types",
@@ -107,6 +133,12 @@ version = "1.3.0"
107
  source = "registry+https://github.com/rust-lang/crates.io-index"
108
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
109
 
 
 
 
 
 
 
110
  [[package]]
111
  name = "syn"
112
  version = "2.0.117"
 
2
  # It is not intended for manual editing.
3
  version = 4
4
 
5
+ [[package]]
6
+ name = "byteorder"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
10
+
11
  [[package]]
12
  name = "cc"
13
  version = "1.2.56"
 
24
  source = "registry+https://github.com/rust-lang/crates.io-index"
25
  checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
26
 
27
+ [[package]]
28
+ name = "hash32"
29
+ version = "0.3.1"
30
+ source = "registry+https://github.com/rust-lang/crates.io-index"
31
+ checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
32
+ dependencies = [
33
+ "byteorder",
34
+ ]
35
+
36
+ [[package]]
37
+ name = "heapless"
38
+ version = "0.9.2"
39
+ source = "registry+https://github.com/rust-lang/crates.io-index"
40
+ checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
41
+ dependencies = [
42
+ "hash32",
43
+ "stable_deref_trait",
44
+ ]
45
+
46
  [[package]]
47
  name = "libc"
48
  version = "0.2.182"
 
96
  name = "openwa-wormkit"
97
  version = "0.1.0"
98
  dependencies = [
99
+ "heapless",
100
  "minhook",
101
  "openwa-lib",
102
  "openwa-types",
 
133
  source = "registry+https://github.com/rust-lang/crates.io-index"
134
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
135
 
136
+ [[package]]
137
+ name = "stable_deref_trait"
138
+ version = "1.2.1"
139
+ source = "registry+https://github.com/rust-lang/crates.io-index"
140
+ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
141
+
142
  [[package]]
143
  name = "syn"
144
  version = "2.0.117"
crates/openwa-types/src/address.rs CHANGED
@@ -313,19 +313,33 @@ pub mod va {
313
  // === WAV Player ===
314
 
315
  /// Opens WAV via mmioOpenA, parses RIFF chunks, creates IDirectSoundBuffer.
316
- /// usercall(ESI=result_out) + 3 stack params, RET 0xC.
317
  pub const WAV_PLAYER_LOAD_AND_PLAY: u32 = 0x0059_9B40;
318
 
319
  /// Calls IDirectSoundBuffer::Play on loaded buffer.
320
- /// usercall(EDI=result_out) + 2 stack params, RET 0x8.
321
  pub const WAV_PLAYER_PLAY: u32 = 0x0059_96E0;
322
 
323
  /// Stops and releases current DirectSound buffer.
324
- /// usercall(ESI=ctx, EDI=result_out), plain RET.
325
  pub const WAV_PLAYER_STOP: u32 = 0x0059_9670;
326
 
327
  // === Fanfare / FE SFX ===
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  /// Builds `\user\Fanfare\<name>.wav`, plays via WavPlayer.
330
  /// stdcall(team_config_index), RET 0x4.
331
  pub const PLAY_FANFARE_DEFAULT: u32 = 0x004D_7500;
 
313
  // === WAV Player ===
314
 
315
  /// Opens WAV via mmioOpenA, parses RIFF chunks, creates IDirectSoundBuffer.
316
+ /// usercall(ESI=result_out) + stack(player, path, 0), RET 0xC.
317
  pub const WAV_PLAYER_LOAD_AND_PLAY: u32 = 0x0059_9B40;
318
 
319
  /// Calls IDirectSoundBuffer::Play on loaded buffer.
320
+ /// usercall(EDI=result_out) + stack(player, volume), RET 0x8.
321
  pub const WAV_PLAYER_PLAY: u32 = 0x0059_96E0;
322
 
323
  /// Stops and releases current DirectSound buffer.
324
+ /// usercall(ESI=player, EDI=result_out), plain RET.
325
  pub const WAV_PLAYER_STOP: u32 = 0x0059_9670;
326
 
327
  // === Fanfare / FE SFX ===
328
 
329
+ /// FeSfx WavPlayer global instance (used by PlayFeSfx).
330
+ pub const FESFX_WAV_PLAYER: u32 = 0x006A_C888;
331
+
332
+ /// Fanfare WavPlayer global instance (used by PlayFanfare_Default).
333
+ pub const FANFARE_WAV_PLAYER: u32 = 0x006A_C890;
334
+
335
+ /// WA data path string buffer (char[0x81]).
336
+ pub const WA_DATA_PATH: u32 = 0x0088_E282;
337
+
338
+ /// Team config fanfare name lookup — jump table with 49 cases (0-48).
339
+ /// usercall(ECX=index_0based, EAX=output_buf), plain RET.
340
+ /// Writes null-terminated country/config name into buffer at EAX.
341
+ pub const GET_TEAM_CONFIG_NAME: u32 = 0x004A_62A0;
342
+
343
  /// Builds `\user\Fanfare\<name>.wav`, plays via WavPlayer.
344
  /// stdcall(team_config_index), RET 0x4.
345
  pub const PLAY_FANFARE_DEFAULT: u32 = 0x004D_7500;
crates/openwa-wormkit/Cargo.toml CHANGED
@@ -10,6 +10,7 @@ crate-type = ["cdylib"]
10
  [dependencies]
11
  openwa-types = { path = "../openwa-types" }
12
  openwa-lib = { path = "../openwa-lib" }
 
13
  minhook = "0.9"
14
 
15
  [target.'cfg(target_os = "windows")'.dependencies]
 
10
  [dependencies]
11
  openwa-types = { path = "../openwa-types" }
12
  openwa-lib = { path = "../openwa-lib" }
13
+ heapless = "0.9"
14
  minhook = "0.9"
15
 
16
  [target.'cfg(target_os = "windows")'.dependencies]
crates/openwa-wormkit/src/replacements/speech.rs CHANGED
@@ -1,27 +1,146 @@
1
- //! Speech / fanfare / FE SFX passthrough hooks.
2
- //!
3
- //! Logs speech-related function calls without replacing behavior.
4
- //! All hooks call the original WA.exe function via MinHook trampoline.
5
  //!
6
  //! Hooks:
7
- //! - PlayFeSfx (0x4D7960): stdcall(sfx_name), RET 0x4
8
- //! - PlayFanfare_CurrentTeam (0x4D78E0): usercall(EAX=index), plain RET
9
- //! - DSSound_LoadSpeechBank (0x571660): usercall(EAX=DDGame) + 3 stack, RET 0xC
 
10
 
11
- use std::ffi::CStr;
12
  use std::sync::atomic::{AtomicU32, Ordering};
13
 
 
 
 
14
  use openwa_types::address::va;
15
 
16
  use crate::hook::{self, usercall_trampoline};
17
  use crate::log_line;
18
 
 
 
 
19
  // ============================================================
20
- // PlayFeSfx passthrough (0x4D7960)
21
  // ============================================================
22
- // stdcall(sfx_name: *const u8), RET 0x4
23
 
24
- static ORIG_PLAY_FE_SFX: AtomicU32 = AtomicU32::new(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  unsafe extern "stdcall" fn hook_play_fe_sfx(sfx_name: *const u8) {
27
  let name = if !sfx_name.is_null() {
@@ -33,9 +152,58 @@ unsafe extern "stdcall" fn hook_play_fe_sfx(sfx_name: *const u8) {
33
  };
34
  let _ = log_line(&format!("[Speech] PlayFeSfx: \"{}\"", name));
35
 
36
- let orig: unsafe extern "stdcall" fn(*const u8) =
37
- core::mem::transmute(ORIG_PLAY_FE_SFX.load(Ordering::Relaxed) as usize);
38
- orig(sfx_name);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
 
41
  // ============================================================
@@ -124,12 +292,17 @@ unsafe extern "cdecl" fn load_speech_bank_impl(
124
 
125
  pub fn install() -> Result<(), String> {
126
  unsafe {
127
- let trampoline = hook::install(
128
  "PlayFeSfx",
129
  va::PLAY_FE_SFX,
130
  hook_play_fe_sfx as *const (),
131
  )?;
132
- ORIG_PLAY_FE_SFX.store(trampoline as u32, Ordering::Relaxed);
 
 
 
 
 
133
 
134
  let trampoline = hook::install(
135
  "PlayFanfare_CurrentTeam",
 
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=DDGame) + 3 stack, RET 0xC — passthrough
8
 
9
+ use std::ffi::{c_char, CStr};
10
  use std::sync::atomic::{AtomicU32, Ordering};
11
 
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;
19
 
20
+ /// Windows MAX_PATH (260 bytes including nul terminator).
21
+ const MAX_PATH: usize = 260;
22
+
23
  // ============================================================
24
+ // WavPlayerHandle — typed wrapper for WA's WavPlayer instances
25
  // ============================================================
 
26
 
27
+ /// Handle to a WA.exe WavPlayer instance (opaque, unknown size).
28
+ ///
29
+ /// Known globals:
30
+ /// - `va::FESFX_WAV_PLAYER` (0x6AC888) — frontend sound effects
31
+ /// - `va::FANFARE_WAV_PLAYER` (0x6AC890) — team fanfares
32
+ #[derive(Clone, Copy)]
33
+ struct WavPlayerHandle(u32);
34
+
35
+ impl WavPlayerHandle {
36
+ /// Read a WavPlayerHandle from a Ghidra global address.
37
+ unsafe fn from_global(ghidra_addr: u32) -> Self {
38
+ Self(rb(ghidra_addr))
39
+ }
40
+
41
+ /// Stop and release the current DirectSound buffer.
42
+ /// WavPlayer_Stop: usercall(ESI=player, EDI=&result), plain RET.
43
+ unsafe fn stop(self) {
44
+ wav_player_stop_raw(self.0, rb(va::WAV_PLAYER_STOP));
45
+ }
46
+
47
+ /// Open a WAV file, parse RIFF, create DirectSound buffer.
48
+ /// WavPlayer_LoadAndPlay: usercall(ESI=&result) + stack(player, path, 0), RET 0xC.
49
+ unsafe fn load(self, path: *const c_char) {
50
+ wav_player_load_raw(self.0, path.cast(), rb(va::WAV_PLAYER_LOAD_AND_PLAY));
51
+ }
52
+
53
+ /// Play the loaded buffer at the given volume.
54
+ /// WavPlayer_Play: usercall(EDI=&result) + stack(player, volume), RET 0x8.
55
+ unsafe fn play(self, volume: u32) {
56
+ wav_player_play_raw(self.0, volume, rb(va::WAV_PLAYER_PLAY));
57
+ }
58
+ }
59
+
60
+ // Naked asm wrappers for WavPlayer usercall functions.
61
+ // ESI/EDI are LLVM-reserved, so we must use naked functions.
62
+
63
+ /// cdecl(player, func_addr) — sets ESI=player, EDI=&result, calls func.
64
+ #[unsafe(naked)]
65
+ unsafe extern "cdecl" fn wav_player_stop_raw(_player: u32, _func: u32) {
66
+ core::arch::naked_asm!(
67
+ "push esi",
68
+ "push edi",
69
+ "sub esp, 4", // result on stack
70
+ "mov esi, [esp + 16]", // player (arg1, past saved esi+edi+result)
71
+ "lea edi, [esp]", // &result
72
+ "call [esp + 20]", // func (arg2)
73
+ "add esp, 4", // drop result
74
+ "pop edi",
75
+ "pop esi",
76
+ "ret",
77
+ );
78
+ }
79
+
80
+ /// cdecl(player, path, func_addr) — pushes stack params, sets ESI=&result, calls func.
81
+ /// Stack layout after push esi + sub esp,4:
82
+ /// ESP+0=result, +4=saved_esi, +8=retaddr, +12=player, +16=path, +20=func
83
+ /// Callee expects stack params: [ESP+4]=player, [ESP+8]=path, [ESP+12]=0
84
+ #[unsafe(naked)]
85
+ unsafe extern "cdecl" fn wav_player_load_raw(_player: u32, _path: *const u8, _func: u32) {
86
+ core::arch::naked_asm!(
87
+ "push esi",
88
+ "sub esp, 4", // result on stack
89
+ "lea esi, [esp]", // ESI = &result
90
+ // Push in reverse order: 0, path, player
91
+ "push 0", // third param=0 (ESP: +16=player, +20=path, +24=func)
92
+ "push dword ptr [esp+20]", // second param=path (ESP: +20=player, +24=path, +28=func)
93
+ "push dword ptr [esp+20]", // first param=player (ESP: +28=path, +32=func)
94
+ "call dword ptr [esp+32]", // call func; RET 0xC cleans 3 params
95
+ "add esp, 4", // drop result
96
+ "pop esi",
97
+ "ret",
98
+ );
99
+ }
100
+
101
+ /// cdecl(player, volume, func_addr) — pushes stack params, sets EDI=&result, calls func.
102
+ /// Stack layout after push edi + sub esp,4:
103
+ /// ESP+0=result, +4=saved_edi, +8=retaddr, +12=player, +16=volume, +20=func
104
+ /// Callee expects stack params: [ESP+4]=player, [ESP+8]=volume
105
+ #[unsafe(naked)]
106
+ unsafe extern "cdecl" fn wav_player_play_raw(_player: u32, _volume: u32, _func: u32) {
107
+ core::arch::naked_asm!(
108
+ "push edi",
109
+ "sub esp, 4", // result on stack
110
+ "lea edi, [esp]", // EDI = &result
111
+ // Push in reverse order: volume, player
112
+ "push dword ptr [esp+16]", // second param=volume (ESP: +16=player, +24=func)
113
+ "push dword ptr [esp+16]", // first param=player (ESP: +28=func)
114
+ "call dword ptr [esp+28]", // call func; RET 0x8 cleans 2 params
115
+ "add esp, 4", // drop result
116
+ "pop edi",
117
+ "ret",
118
+ );
119
+ }
120
+
121
+
122
+ // ============================================================
123
+ // WA function wrappers
124
+ // ============================================================
125
+
126
+ /// GetTeamConfigName: usercall(ECX=index_0based, EAX=output_buf), plain RET.
127
+ /// Jump table with 49 cases (0-48). Writes null-terminated country/config
128
+ /// name (e.g. "Finland", "Simple", "USA") into the buffer at EAX.
129
+ unsafe fn get_team_config_name(index_0based: u32, buf: *mut u8) {
130
+ core::arch::asm!(
131
+ "call {func}",
132
+ func = in(reg) rb(va::GET_TEAM_CONFIG_NAME),
133
+ in("ecx") index_0based,
134
+ in("eax") buf as u32,
135
+ clobber_abi("C"),
136
+ );
137
+ }
138
+
139
+ // ============================================================
140
+ // PlayFeSfx replacement (0x4D7960)
141
+ // ============================================================
142
+ // stdcall(sfx_name: *const u8), RET 0x4
143
+ // Builds "fesfx\<name>.wav", plays on FESFX_WAV_PLAYER.
144
 
145
  unsafe extern "stdcall" fn hook_play_fe_sfx(sfx_name: *const u8) {
146
  let name = if !sfx_name.is_null() {
 
152
  };
153
  let _ = log_line(&format!("[Speech] PlayFeSfx: \"{}\"", name));
154
 
155
+ // Build null-terminated path: "fesfx\<name>.wav"
156
+ let mut path = CString::<MAX_PATH>::new();
157
+ let _ = path.extend_from_bytes(b"fesfx\\");
158
+ let _ = path.extend_from_bytes(name.as_bytes());
159
+ let _ = path.extend_from_bytes(b".wav");
160
+
161
+ let player = WavPlayerHandle::from_global(va::FESFX_WAV_PLAYER);
162
+ player.stop();
163
+ player.load(path.as_ptr());
164
+ player.play(0);
165
+ }
166
+
167
+ // ============================================================
168
+ // PlayFanfare_Default replacement (0x4D7500)
169
+ // ============================================================
170
+ // stdcall(team_type: u32), RET 0x4
171
+ // If team_type in 1..=49, looks up config name; else "Simple".
172
+ // Builds "<wa_path>\user\Fanfare\<name>.wav", plays on FANFARE_WAV_PLAYER.
173
+
174
+ unsafe extern "stdcall" fn hook_play_fanfare_default(team_type: u32) {
175
+ // Get the fanfare name
176
+ let mut name_buf = [0u8; 64];
177
+ if (1..=49).contains(&team_type) {
178
+ get_team_config_name(team_type - 1, name_buf.as_mut_ptr());
179
+ } else {
180
+ name_buf[..7].copy_from_slice(b"Simple\0");
181
+ }
182
+ let name = CStr::from_ptr(name_buf.as_ptr() as *const i8)
183
+ .to_str()
184
+ .unwrap_or("Simple");
185
+
186
+ // Read WA data path
187
+ let wa_path = CStr::from_ptr(rb(va::WA_DATA_PATH) as *const i8)
188
+ .to_str()
189
+ .unwrap_or(".");
190
+
191
+ let _ = log_line(&format!(
192
+ "[Speech] PlayFanfare_Default: team_type={} name=\"{}\"",
193
+ team_type, name
194
+ ));
195
+
196
+ // Build null-terminated path: "<wa_path>\user\Fanfare\<name>.wav"
197
+ let mut path = CString::<MAX_PATH>::new();
198
+ let _ = path.extend_from_bytes(wa_path.as_bytes());
199
+ let _ = path.extend_from_bytes(b"\\user\\Fanfare\\");
200
+ let _ = path.extend_from_bytes(name.as_bytes());
201
+ let _ = path.extend_from_bytes(b".wav");
202
+
203
+ let player = WavPlayerHandle::from_global(va::FANFARE_WAV_PLAYER);
204
+ player.stop();
205
+ player.load(path.as_ptr());
206
+ player.play(0);
207
  }
208
 
209
  // ============================================================
 
292
 
293
  pub fn install() -> Result<(), String> {
294
  unsafe {
295
+ hook::install(
296
  "PlayFeSfx",
297
  va::PLAY_FE_SFX,
298
  hook_play_fe_sfx as *const (),
299
  )?;
300
+
301
+ hook::install(
302
+ "PlayFanfare_Default",
303
+ va::PLAY_FANFARE_DEFAULT,
304
+ hook_play_fanfare_default as *const (),
305
+ )?;
306
 
307
  let trampoline = hook::install(
308
  "PlayFanfare_CurrentTeam",