feat: port WavPlayer and speech/fanfare functions to pure Rust
Browse filesReplace 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>
|
@@ -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::{
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 —
|
| 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
|
| 16 |
-
use openwa_core::audio::
|
|
|
|
| 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 |
-
//
|
| 28 |
// ============================================================
|
| 29 |
|
| 30 |
-
//
|
| 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 "
|
| 69 |
core::arch::naked_asm!(
|
| 70 |
"push esi",
|
| 71 |
-
"
|
| 72 |
-
"
|
| 73 |
-
"mov
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"add esp, 4", // drop result
|
| 77 |
-
"pop edi",
|
| 78 |
-
"pop esi",
|
| 79 |
"ret",
|
|
|
|
|
|
|
| 80 |
);
|
| 81 |
}
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
| 87 |
#[unsafe(naked)]
|
| 88 |
-
unsafe extern "
|
| 89 |
core::arch::naked_asm!(
|
| 90 |
-
"push
|
| 91 |
-
"
|
| 92 |
-
"
|
| 93 |
-
|
| 94 |
-
"
|
| 95 |
-
"
|
| 96 |
-
"
|
| 97 |
-
"
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
"ret",
|
| 101 |
);
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
| 108 |
#[unsafe(naked)]
|
| 109 |
-
unsafe extern "
|
| 110 |
core::arch::naked_asm!(
|
| 111 |
-
"push
|
| 112 |
-
"
|
| 113 |
-
"
|
| 114 |
-
|
| 115 |
-
"
|
| 116 |
-
"
|
| 117 |
-
"
|
| 118 |
-
"
|
| 119 |
-
"
|
| 120 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
);
|
| 122 |
}
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
///
|
| 129 |
-
|
| 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
|
| 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
|
| 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={}
|
| 233 |
-
team_type
|
| 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
|
| 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(
|
| 261 |
-
let _ = log_line(&format!(
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 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
|
| 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=\"{}\"
|
| 390 |
-
team_index, path_str, dir_str
|
| 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
|
| 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}
|
| 491 |
-
ddgw as u32
|
| 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 |
-
|
|
|
|
| 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,
|