paavohtl Claude Sonnet 4.6 commited on
Commit
79e6e26
·
1 Parent(s): 80c20a7

feat: add GameSession struct and DDGameWrapper__Constructor passthrough hook

Browse files

Renames 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>

crates/openwa-core/src/address.rs CHANGED
@@ -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;
crates/openwa-core/src/game_session.rs ADDED
@@ -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);
crates/openwa-core/src/lib.rs CHANGED
@@ -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;
crates/openwa-wormkit/src/replacements/game_session.rs ADDED
@@ -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
+ }
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -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()?;