feat: replace PlayFeSfx and PlayFanfare_Default with full Rust implementations
Browse filesConvert 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 +32 -0
- crates/openwa-types/src/address.rs +17 -3
- crates/openwa-wormkit/Cargo.toml +1 -0
- crates/openwa-wormkit/src/replacements/speech.rs +189 -16
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) +
|
| 317 |
pub const WAV_PLAYER_LOAD_AND_PLAY: u32 = 0x0059_9B40;
|
| 318 |
|
| 319 |
/// Calls IDirectSoundBuffer::Play on loaded buffer.
|
| 320 |
-
/// usercall(EDI=result_out) +
|
| 321 |
pub const WAV_PLAYER_PLAY: u32 = 0x0059_96E0;
|
| 322 |
|
| 323 |
/// Stops and releases current DirectSound buffer.
|
| 324 |
-
/// usercall(ESI=
|
| 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
|
| 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 |
-
//! -
|
| 9 |
-
//! -
|
|
|
|
| 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 |
-
//
|
| 21 |
// ============================================================
|
| 22 |
-
// stdcall(sfx_name: *const u8), RET 0x4
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 128 |
"PlayFeSfx",
|
| 129 |
va::PLAY_FE_SFX,
|
| 130 |
hook_play_fe_sfx as *const (),
|
| 131 |
)?;
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|