feat: add GameSession struct and DDGameWrapper__Constructor passthrough hook
Browse filesRenames DDInput__Constructor_Maybe → GameEngine__InitHardware (the function
initializes all hardware subsystems; the DDInput vtable only appears in the
headless/stats-server else-branch that gave it its wrong name).
Adds:
- GameSession struct (0x120 bytes) in openwa-core with all known field offsets
mapped from decompilation of FUN_00572F50 / GameEngine__InitHardware
- G_GAME_SESSION (0x7A0884) and four new address constants
- Passthrough hook on DDGameWrapper__Constructor (0x56DEF0) that logs
wrapper and DDGame pointers once per game session start
The hook uses a naked AT&T asm trampoline to preserve EDI, which
DDGameWrapper__Constructor passes implicitly as the 9th argument to
DDGame__Constructor. A plain extern "stdcall" hook clobbered EDI via
Rust's register allocator, causing a null-pointer crash in
PCLandscape__Constructor. The trampoline tail-calls the original via
jmp (not call), leaving EDI completely untouched.
Validated: 135 PASS, 0 FAIL. OpenWA.log shows:
[GameSession] DDGameWrapper ctor: wrapper=0x03CD0C00 ddgame=0x07A669E0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@@ -223,6 +223,32 @@ pub mod va {
|
|
| 223 |
/// usercall(EAX=base, EDX=team_idx, ESI=worm_index) → void, plain RET. 3 xrefs.
|
| 224 |
pub const SET_ACTIVE_WORM_MAYBE: u32 = 0x0052_2500;
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
// === Graphics / rendering ===
|
| 227 |
|
| 228 |
pub const CONSTRUCT_DD_GAME: u32 = 0x0056_E220;
|
|
|
|
| 223 |
/// usercall(EAX=base, EDX=team_idx, ESI=worm_index) → void, plain RET. 3 xrefs.
|
| 224 |
pub const SET_ACTIVE_WORM_MAYBE: u32 = 0x0052_2500;
|
| 225 |
|
| 226 |
+
// === Game session ===
|
| 227 |
+
|
| 228 |
+
/// `GameEngine__InitHardware` — initializes all hardware subsystems: display,
|
| 229 |
+
/// sound, keyboard, palette, DDGameWrapper, DDNetGameWrapper, and stores
|
| 230 |
+
/// the resulting pointers into `G_GAME_SESSION`.
|
| 231 |
+
///
|
| 232 |
+
/// `__thiscall(this=GameInfo, hwnd, param3, param4)` → 1=ok 0=fail, `RET 0xC`.
|
| 233 |
+
///
|
| 234 |
+
/// In headless/stats mode (`GameInfo+0xF914 != 0`) creates a `GameStats` object
|
| 235 |
+
/// with the DDInput vtable instead of a real display+audio stack.
|
| 236 |
+
pub const GAME_ENGINE_INIT_HARDWARE: u32 = 0x0056_D350;
|
| 237 |
+
|
| 238 |
+
/// `GameSession__Run` — allocates the `GameSession` struct (0x120 bytes),
|
| 239 |
+
/// calls `GameEngine__InitHardware`, runs the game main loop, then calls
|
| 240 |
+
/// `GameEngine__Shutdown`. Uses ESI implicitly for the `GameInfo` config.
|
| 241 |
+
pub const GAME_SESSION_RUN: u32 = 0x0057_2F50;
|
| 242 |
+
|
| 243 |
+
/// `GameSession__Constructor` — usercall(`EAX=this`), sets vtable and
|
| 244 |
+
/// zero-inits all fields. Called with a freshly `malloc`'d 0x120-byte buffer.
|
| 245 |
+
pub const GAME_SESSION_CONSTRUCTOR: u32 = 0x0058_BFA0;
|
| 246 |
+
|
| 247 |
+
/// `GameEngine__Shutdown` — destroys all subsystems in reverse creation order
|
| 248 |
+
/// (streaming audio, DDGameWrapper, DisplayGfx, DDKeyboard, Palette).
|
| 249 |
+
/// `stdcall(param_1)` → void.
|
| 250 |
+
pub const GAME_ENGINE_SHUTDOWN: u32 = 0x0056_DCD0;
|
| 251 |
+
|
| 252 |
// === Graphics / rendering ===
|
| 253 |
|
| 254 |
pub const CONSTRUCT_DD_GAME: u32 = 0x0056_E220;
|
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// Top-level game session context, allocated once per game run.
|
| 2 |
+
///
|
| 3 |
+
/// `G_GAME_SESSION` (0x7A0884) stores a pointer to this struct. Created by
|
| 4 |
+
/// `GameSession__Constructor` (0x58BFA0, usercall EAX=this), populated by
|
| 5 |
+
/// `GameEngine__InitHardware` (0x56D350), and destroyed by
|
| 6 |
+
/// `GameEngine__Shutdown` (0x56DCD0).
|
| 7 |
+
///
|
| 8 |
+
/// Lifecycle:
|
| 9 |
+
/// ```text
|
| 10 |
+
/// GameSession__Run (0x572F50, ESI=GameInfo)
|
| 11 |
+
/// ├─ alloc 0x120 bytes → GameSession__Constructor (0x58BFA0)
|
| 12 |
+
/// ├─ G_GAME_SESSION ← &this
|
| 13 |
+
/// ├─ GameEngine__InitHardware → fills subsystem pointers (0xA0–0xC0)
|
| 14 |
+
/// ├─ game main loop (exit when exit_flag != 0)
|
| 15 |
+
/// └─ GameEngine__Shutdown → destroys all subsystems
|
| 16 |
+
/// ```
|
| 17 |
+
///
|
| 18 |
+
/// In headless/stats mode (`GameInfo+0xF914 != 0`), `display_gfx` holds a
|
| 19 |
+
/// `GameStats` object with a DDInput vtable instead of a real `DisplayGfx`.
|
| 20 |
+
#[repr(C)]
|
| 21 |
+
pub struct GameSession {
|
| 22 |
+
/// 0x000: vtable pointer (class `GameSession`, `PTR_FUN_0066b3f8`)
|
| 23 |
+
pub vtable: *mut u8,
|
| 24 |
+
pub _unknown_004: [u8; 4],
|
| 25 |
+
/// 0x008: main window HWND, stored from `hWnd` in `GameSession__Run`
|
| 26 |
+
pub hwnd: u32,
|
| 27 |
+
pub _unknown_00c: [u8; 0x20],
|
| 28 |
+
/// 0x02C: set to 1 on init (role TBD)
|
| 29 |
+
pub flag_2c: u32,
|
| 30 |
+
pub _unknown_030: [u8; 8],
|
| 31 |
+
/// 0x038: back-pointer to the `GameInfo` config struct (ESI from `GameSession__Run`)
|
| 32 |
+
pub config_ptr: *mut u8,
|
| 33 |
+
/// 0x03C: nonzero = exit the game main loop (checked each frame in `GameSession__Run`)
|
| 34 |
+
pub exit_flag: u32,
|
| 35 |
+
/// 0x040: game-end status/result code
|
| 36 |
+
pub flag_40: u32,
|
| 37 |
+
pub _unknown_044: [u8; 0x10],
|
| 38 |
+
/// 0x054: display center X = `display_width / 2`.
|
| 39 |
+
/// Initialized to `0x80000000` by `GameSession__Constructor`, then overwritten.
|
| 40 |
+
pub screen_center_x: i32,
|
| 41 |
+
/// 0x058: display center Y = `display_height / 2`
|
| 42 |
+
pub screen_center_y: i32,
|
| 43 |
+
pub _unknown_05c: [u8; 0x14],
|
| 44 |
+
/// 0x070: cursor X at session start (from `GetCursorPos`)
|
| 45 |
+
pub cursor_initial_x: i32,
|
| 46 |
+
/// 0x074: cursor Y at session start
|
| 47 |
+
pub cursor_initial_y: i32,
|
| 48 |
+
pub _unknown_078: [u8; 8],
|
| 49 |
+
/// 0x080: cursor center X — set to `screen_center_x`, used for `SetCursorPos`
|
| 50 |
+
pub cursor_x: i32,
|
| 51 |
+
/// 0x084: cursor center Y
|
| 52 |
+
pub cursor_y: i32,
|
| 53 |
+
pub _unknown_088: [u8; 8],
|
| 54 |
+
/// 0x090: `QueryPerformanceFrequency` result low DWORD (0 if QPC unavailable)
|
| 55 |
+
pub timer_freq_lo: u32,
|
| 56 |
+
/// 0x094: high DWORD
|
| 57 |
+
pub timer_freq_hi: u32,
|
| 58 |
+
/// 0x098: QPC counter accumulator low DWORD
|
| 59 |
+
pub timer_counter_lo: u32,
|
| 60 |
+
/// 0x09C: high DWORD
|
| 61 |
+
pub timer_counter_hi: u32,
|
| 62 |
+
/// 0x0A0: `DDGameWrapper*` — the main game object wrapper (→ `DDGame` at `+0x488`)
|
| 63 |
+
pub ddgame_wrapper: *mut u8,
|
| 64 |
+
/// 0x0A4: `DDKeyboard*` — 0x33C bytes, vtable `DDKeyboard_vtable` (0x66AEC8)
|
| 65 |
+
pub keyboard: *mut u8,
|
| 66 |
+
/// 0x0A8: `DSSound*` — 0xBE0 bytes, vtable `DSSound_vtable` (0x66AF20)
|
| 67 |
+
pub sound: *mut u8,
|
| 68 |
+
/// 0x0AC: `DisplayGfx*` — 0x24E28 bytes (normal), or `GameStats*` in headless mode
|
| 69 |
+
pub display_gfx: *mut u8,
|
| 70 |
+
/// 0x0B0: `Palette*` — 0x28 bytes, vtable `Palette_vtable_Maybe`
|
| 71 |
+
pub palette: *mut u8,
|
| 72 |
+
/// 0x0B4: streaming audio object — 0x354 bytes (`FUN_0058bc10`)
|
| 73 |
+
pub streaming_audio: *mut u8,
|
| 74 |
+
/// 0x0B8: input controller — 0x1800 bytes; null if `param_4 == 0` at init
|
| 75 |
+
pub input_ctrl: *mut u8,
|
| 76 |
+
/// 0x0BC: timing object — 0x30 bytes (`FUN_0053e950`)
|
| 77 |
+
pub timer_obj: *mut u8,
|
| 78 |
+
/// 0x0C0: `DDNetGameWrapper*` — 0x2C bytes
|
| 79 |
+
pub net_game: *mut u8,
|
| 80 |
+
pub _unknown_0c4: [u8; 0x5C],
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const _: () = assert!(core::mem::size_of::<GameSession>() == 0x120);
|
|
@@ -23,6 +23,7 @@ pub mod game_info;
|
|
| 23 |
pub mod scheme;
|
| 24 |
pub mod speech;
|
| 25 |
pub mod turn_order;
|
|
|
|
| 26 |
pub mod address;
|
| 27 |
pub mod rebase;
|
| 28 |
pub mod wa;
|
|
|
|
| 23 |
pub mod scheme;
|
| 24 |
pub mod speech;
|
| 25 |
pub mod turn_order;
|
| 26 |
+
pub mod game_session;
|
| 27 |
pub mod address;
|
| 28 |
pub mod rebase;
|
| 29 |
pub mod wa;
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Game session lifecycle hook.
|
| 2 |
+
//!
|
| 3 |
+
//! Passthrough hook on `DDGameWrapper__Constructor` (0x56DEF0) that fires once
|
| 4 |
+
//! per game session start, logging the `DDGameWrapper` and `DDGame` addresses.
|
| 5 |
+
//!
|
| 6 |
+
//! ## Calling convention notes
|
| 7 |
+
//!
|
| 8 |
+
//! `DDGameWrapper__Constructor` is `__stdcall` with 7 stack params, BUT it also
|
| 9 |
+
//! uses `EDI` as an **implicit register param** (Ghidra: `unaff_EDI`), which it
|
| 10 |
+
//! passes as the 9th argument to `DDGame__Constructor`. A regular `extern "stdcall"`
|
| 11 |
+
//! hook function would clobber EDI before calling the original (the Rust compiler
|
| 12 |
+
//! uses it as a scratch register), causing a null-pointer crash deep in DDGame init.
|
| 13 |
+
//!
|
| 14 |
+
//! The naked trampoline below avoids touching EDI entirely:
|
| 15 |
+
//! 1. Swaps the caller's return address with our continuation label.
|
| 16 |
+
//! 2. Tail-calls the original via `jmp` (not `call`) — EDI is unchanged.
|
| 17 |
+
//! 3. After the original returns (via our continuation), calls a cdecl logger.
|
| 18 |
+
//! 4. Jumps back to the real caller.
|
| 19 |
+
|
| 20 |
+
use openwa_core::address::va;
|
| 21 |
+
use crate::hook;
|
| 22 |
+
use crate::log_line;
|
| 23 |
+
|
| 24 |
+
/// Trampoline address of the original DDGameWrapper__Constructor.
|
| 25 |
+
/// Set by `install()` via MinHook, read by the naked trampoline.
|
| 26 |
+
static mut ORIG_VAL: u32 = 0;
|
| 27 |
+
|
| 28 |
+
/// Caller's return address, saved by the naked trampoline before the tail-call
|
| 29 |
+
/// to orig so we can return there after the logger runs.
|
| 30 |
+
/// Non-reentrant (single-threaded game init; DDGameWrapper::Constructor is
|
| 31 |
+
/// called at most once per game session).
|
| 32 |
+
static mut SAVED_RET: u32 = 0;
|
| 33 |
+
|
| 34 |
+
/// Called from the naked trampoline after the original constructor returns.
|
| 35 |
+
/// `wrapper` = the DDGameWrapper pointer (orig's return value = `this`).
|
| 36 |
+
unsafe extern "cdecl" fn on_ddgame_wrapper_created(wrapper: u32) {
|
| 37 |
+
// DDGame* lives at DDGameWrapper+0x488
|
| 38 |
+
let ddgame = *(wrapper as *const u32).add(0x488 / 4);
|
| 39 |
+
let _ = log_line(&format!(
|
| 40 |
+
"[GameSession] DDGameWrapper ctor: wrapper=0x{wrapper:08X} ddgame=0x{ddgame:08X}"
|
| 41 |
+
));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/// Naked trampoline for `DDGameWrapper__Constructor`.
|
| 45 |
+
///
|
| 46 |
+
/// Stack layout on entry (stdcall 7 params):
|
| 47 |
+
/// ```text
|
| 48 |
+
/// [esp+00] = caller_ret
|
| 49 |
+
/// [esp+04] = this (*mut DDGameWrapper)
|
| 50 |
+
/// [esp+08] = display
|
| 51 |
+
/// [esp+0C] = sound
|
| 52 |
+
/// [esp+10] = gfx
|
| 53 |
+
/// [esp+14] = palette
|
| 54 |
+
/// [esp+18] = music
|
| 55 |
+
/// [esp+1C] = network
|
| 56 |
+
/// EDI = implicit param (passed down to DDGame__Constructor as 9th arg)
|
| 57 |
+
/// ```
|
| 58 |
+
#[unsafe(naked)]
|
| 59 |
+
unsafe extern "C" fn hook_ddgame_wrapper_ctor() {
|
| 60 |
+
core::arch::naked_asm!(
|
| 61 |
+
// --- Save EAX and swap return address ---
|
| 62 |
+
"pushl %eax",
|
| 63 |
+
// Stack: [esp+0]=old_eax, [esp+4]=caller_ret, [esp+8]=this, ..., [esp+20]=network
|
| 64 |
+
"movl 4(%esp), %eax", // eax = caller_ret
|
| 65 |
+
"movl %eax, {ret}", // save caller_ret → SAVED_RET
|
| 66 |
+
"movl $1f, %eax", // eax = address of our continuation (AT&T: $ = immediate)
|
| 67 |
+
"movl %eax, 4(%esp)", // replace caller_ret with continuation address
|
| 68 |
+
"popl %eax", // restore eax; stack: [cont, this, ..., network]
|
| 69 |
+
// Stack is now exactly: [esp+0]=continuation, [esp+4]=this, ..., [esp+1C]=network
|
| 70 |
+
// EDI is unchanged — the original function will read it directly.
|
| 71 |
+
|
| 72 |
+
// --- Tail-call to original (jmp, not call, to avoid pushing a return address) ---
|
| 73 |
+
"movl {orig}, %eax", // eax = trampoline address (value at ORIG_VAL)
|
| 74 |
+
"jmpl *%eax",
|
| 75 |
+
|
| 76 |
+
// --- Continuation: entered when orig does its `ret 0x1C` ---
|
| 77 |
+
// At this point:
|
| 78 |
+
// EAX = wrapper pointer (orig's return value)
|
| 79 |
+
// ESP = caller's stack (orig's ret 0x1C popped 7 params + return addr)
|
| 80 |
+
"1:",
|
| 81 |
+
"pushl %eax", // save wrapper (return value)
|
| 82 |
+
"pushl %eax", // arg: wrapper ptr → on_ddgame_wrapper_created
|
| 83 |
+
"calll {log_fn}",
|
| 84 |
+
"addl $4, %esp", // clean cdecl arg
|
| 85 |
+
"popl %eax", // restore wrapper → EAX (caller's expected return value)
|
| 86 |
+
"jmpl *{ret}", // jump to saved caller_ret
|
| 87 |
+
|
| 88 |
+
orig = sym ORIG_VAL,
|
| 89 |
+
ret = sym SAVED_RET,
|
| 90 |
+
log_fn = sym on_ddgame_wrapper_created,
|
| 91 |
+
options(att_syntax),
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
pub fn install() -> Result<(), String> {
|
| 96 |
+
unsafe {
|
| 97 |
+
let trampoline = hook::install(
|
| 98 |
+
"DDGameWrapper__Constructor",
|
| 99 |
+
va::CONSTRUCT_DD_GAME_WRAPPER,
|
| 100 |
+
hook_ddgame_wrapper_ctor as *const (),
|
| 101 |
+
)?;
|
| 102 |
+
ORIG_VAL = trampoline as u32;
|
| 103 |
+
}
|
| 104 |
+
Ok(())
|
| 105 |
+
}
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
mod config;
|
| 2 |
mod frontend;
|
|
|
|
| 3 |
pub(crate) mod input;
|
| 4 |
mod scheme;
|
| 5 |
mod render;
|
|
@@ -9,6 +10,7 @@ mod team;
|
|
| 9 |
mod weapon;
|
| 10 |
|
| 11 |
pub fn install_all() -> Result<(), String> {
|
|
|
|
| 12 |
frontend::install()?;
|
| 13 |
scheme::install()?;
|
| 14 |
config::install()?;
|
|
|
|
| 1 |
mod config;
|
| 2 |
mod frontend;
|
| 3 |
+
mod game_session;
|
| 4 |
pub(crate) mod input;
|
| 5 |
mod scheme;
|
| 6 |
mod render;
|
|
|
|
| 10 |
mod weapon;
|
| 11 |
|
| 12 |
pub fn install_all() -> Result<(), String> {
|
| 13 |
+
game_session::install()?;
|
| 14 |
frontend::install()?;
|
| 15 |
scheme::install()?;
|
| 16 |
config::install()?;
|