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