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

feat: replace DDGameWrapper__Constructor with full Rust reimplementation

Browse files

Port DDGameWrapper__Constructor (0x56DEF0) from passthrough hook to complete
Rust replacement. The original uses an implicit EDI=game_info register param
alongside 7 stdcall stack params; a naked trampoline captures EDI before any
Rust code can clobber it.

Key implementation details:
- Naked entry trampoline saves EDI→GAME_INFO, pops caller ret addr, calls cdecl
ctor_impl, then does stdcall callee-cleanup (addl $0x1C; jmpl *SAVED_RET)
- call_init_replay bridge: naked stdcall that loads EAX/ESI and calls the
usercall DDGameWrapper__InitReplay (EAX=game_info, ESI=this, plain RET)
- ddgame_constructor_call bridge: loads implicit ECX=network from static then
tail-jumps to DDGame__Constructor; its RET 0x24 returns directly to ctor_impl
- ctor_impl uses typed fields: DDGameWrapper, DDDisplay, DSSound, Palette,
GameSession (for timer_obj/net_game by name instead of raw offset arithmetic)

Also adds DDGAMEWRAPPER_INIT_REPLAY and DDGAME_INIT_GAME_STATE to address.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

crates/openwa-core/src/address.rs CHANGED
@@ -253,6 +253,12 @@ pub mod va {
253
 
254
  pub const CONSTRUCT_DD_GAME: u32 = 0x0056_E220;
255
  pub const CONSTRUCT_DD_GAME_WRAPPER: u32 = 0x0056_DEF0;
 
 
 
 
 
 
256
  pub const CONSTRUCT_DD_DISPLAY: u32 = 0x0056_9D00;
257
  pub const CONSTRUCT_FRAME_BUFFER: u32 = 0x005A_2430;
258
  pub const BLIT_SCREEN: u32 = 0x005A_2020;
 
253
 
254
  pub const CONSTRUCT_DD_GAME: u32 = 0x0056_E220;
255
  pub const CONSTRUCT_DD_GAME_WRAPPER: u32 = 0x0056_DEF0;
256
+ /// DDGameWrapper::InitReplay β€” usercall(EAX=game_info, ESI=this), plain RET.
257
+ /// Opens replay/recording files based on GameInfo+0xDB08/0xDB09 flags.
258
+ pub const DDGAMEWRAPPER_INIT_REPLAY: u32 = 0x0056_F860;
259
+ /// DDGame::InitGameState β€” stdcall(this=DDGameWrapper*), RET 0x4.
260
+ /// Initializes game state fields in DDGame after DDGame__Constructor.
261
+ pub const DDGAME_INIT_GAME_STATE: u32 = 0x0052_6500;
262
  pub const CONSTRUCT_DD_DISPLAY: u32 = 0x0056_9D00;
263
  pub const CONSTRUCT_FRAME_BUFFER: u32 = 0x005A_2430;
264
  pub const BLIT_SCREEN: u32 = 0x005A_2020;
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -1,105 +1,224 @@
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
+ //! Full Rust replacement for `DDGameWrapper__Constructor` (0x56DEF0).
2
  //!
3
+ //! ## Original calling convention
 
4
  //!
5
+ //! `DDGameWrapper__Constructor` is `__stdcall` with 7 explicit stack params, plus
6
+ //! an **implicit EDI** register param (`game_info` / `unaff_EDI` in Ghidra).
7
+ //! EDI is passed as the 9th argument to `DDGame__Constructor`.
8
  //!
9
+ //! ## Implementation strategy
 
 
 
 
10
  //!
11
+ //! A naked trampoline handles the unconventional calling convention:
12
+ //! 1. Saves EDI (game_info) to a static before any Rust code can touch registers.
13
+ //! 2. Pops the caller return address to another static (to simulate stdcall callee
14
+ //! cleanup from outside the cdecl implementation).
15
+ //! 3. Calls the cdecl `ctor_impl` with the 7 stack args already in position.
16
+ //! 4. After `ctor_impl` returns, skips 0x1C bytes (7 Γ— 4) and jumps to the saved
17
+ //! return address β€” exactly what `stdcall RET 0x1C` would do.
18
+ //! EAX = `this` (ctor_impl's return value) is preserved for the caller.
19
+ //!
20
+ //! ## Sub-call conventions
21
+ //!
22
+ //! - `DDGameWrapper__InitReplay` (0x56F860): usercall(EAX=game_info, ESI=this),
23
+ //! plain RET (no stack args). Bridged via `call_init_replay`.
24
+ //! - `DDGame__Constructor` (0x56E220): stdcall 9 params + implicit ECX=network.
25
+ //! Bridged via `ddgame_constructor_call` which sets ECX then **tail-jumps**,
26
+ //! preserving the exact stack so DDGame::ctor's `RET 0x24` returns to ctor_impl.
27
+ //! - `DDGame__InitGameState` (0x526500): plain stdcall(this), called via transmute.
28
 
29
  use openwa_core::address::va;
30
+ use openwa_core::rebase::rb;
31
+ use openwa_core::ddgame_wrapper::DDGameWrapper;
32
+ use openwa_core::dddisplay::DDDisplay;
33
+ use openwa_core::dssound::DSSound;
34
+ use openwa_core::game_session::GameSession;
35
+ use openwa_core::palette::Palette;
36
  use crate::hook;
37
  use crate::log_line;
38
 
39
+ /// Caller's return address, saved by the naked trampoline so we can do the
40
+ /// stdcall callee-cleanup (arg pop + return) after the cdecl impl returns.
 
 
 
 
 
 
41
  static mut SAVED_RET: u32 = 0;
42
 
43
+ /// Implicit EDI = game_info pointer, captured from EDI on entry.
44
+ static mut GAME_INFO: u32 = 0;
45
+
46
+ /// Implicit ECX = network pointer for `DDGame__Constructor`. Set in `ctor_impl`
47
+ /// just before calling `ddgame_constructor_call`.
48
+ static mut DDGAME_CTOR_ECX: u32 = 0;
49
+
50
+ /// Runtime address of `DDGameWrapper__InitReplay` (set at install time).
51
+ static mut INIT_REPLAY_ADDR: u32 = 0;
52
+
53
+ /// Runtime address of `DDGame__Constructor` (set at install time).
54
+ static mut DDGAME_CTOR_ADDR: u32 = 0;
55
+
56
+ // ─── Bridge: DDGameWrapper__InitReplay ───────────────────────────────────────
57
+ //
58
+ // Convention: usercall(EAX=game_info, ESI=this), plain RET (no stack params).
59
+ // Declared as stdcall(game_info, this) so the Rust caller pushes both; the
60
+ // naked body loads EAX/ESI from the stack and calls via INIT_REPLAY_ADDR.
61
+ //
62
+ // Stack on entry (after stdcall push):
63
+ // [esp+0] = ret addr
64
+ // [esp+4] = game_info (arg 1, pushed last by stdcall)
65
+ // [esp+8] = this (arg 2, pushed first by stdcall)
66
+ //
67
+ // After `pushl %esi`:
68
+ // [esp+0] = old_esi, [esp+4] = ret addr, [esp+8] = game_info, [esp+0xC] = this
69
+ #[unsafe(naked)]
70
+ unsafe extern "stdcall" fn call_init_replay(_game_info: *mut u8, _this: *mut DDGameWrapper) {
71
+ core::arch::naked_asm!(
72
+ "pushl %esi",
73
+ "movl 8(%esp), %eax", // EAX = game_info
74
+ "movl 0xC(%esp), %esi", // ESI = this
75
+ "calll *({fn})", // call DDGameWrapper__InitReplay; plain RET from callee
76
+ "popl %esi", // restore ESI
77
+ "retl $8", // stdcall callee-cleanup: 2 Γ— u32 = 8 bytes
78
+ fn = sym INIT_REPLAY_ADDR,
79
+ options(att_syntax),
80
+ );
81
+ }
82
+
83
+ // ─── Bridge: DDGame__Constructor ─────────────────────────────────────────────
84
+ //
85
+ // Convention: stdcall 9 params + implicit ECX=network.
86
+ //
87
+ // Rust calls this as a normal stdcall(9 params). The naked body loads ECX from
88
+ // DDGAME_CTOR_ECX and then **tail-jumps** (jmp, not call) to DDGame::ctor.
89
+ //
90
+ // Tail-jump rationale: the call instruction in ctor_impl already pushed the
91
+ // return address at [esp+0]. After the tail-jump, DDGame::ctor sees exactly:
92
+ // [esp+0] = return address back into ctor_impl
93
+ // [esp+4] = this … [esp+24] = game_info (9 stdcall args)
94
+ // ECX = network (implicit)
95
+ // DDGame::ctor's `RET 0x24` pops 0x24 bytes and returns to ctor_impl. βœ“
96
+ #[unsafe(naked)]
97
+ unsafe extern "stdcall" fn ddgame_constructor_call(
98
+ _this: *mut DDGameWrapper,
99
+ _display: *mut DDDisplay,
100
+ _sound: *mut DSSound,
101
+ _gfx: *mut u8,
102
+ _palette: *mut Palette,
103
+ _music: *mut u8,
104
+ _timer_obj: *mut u8,
105
+ _net_game: *mut u8,
106
+ _game_info: *mut u8,
107
+ ) -> *mut u8 {
108
+ core::arch::naked_asm!(
109
+ "movl {ecx_val}, %ecx", // ECX = network (implicit param for DDGame::ctor)
110
+ "jmpl *({fn})", // tail-jump; DDGame::ctor's RET 0x24 returns to caller
111
+ ecx_val = sym DDGAME_CTOR_ECX,
112
+ fn = sym DDGAME_CTOR_ADDR,
113
+ options(att_syntax),
114
+ );
115
+ }
116
+
117
+ // ─── Implementation ───────────────────────────────────────────────────────────
118
+
119
+ /// Core Rust implementation of `DDGameWrapper__Constructor`.
120
+ ///
121
+ /// Called by the naked trampoline via `calll`. The 7 stdcall args are already
122
+ /// on the stack in cdecl position (the trampoline popped the original return
123
+ /// address into `SAVED_RET`, leaving [this, display, …, network] at [esp+4..]).
124
+ unsafe extern "cdecl" fn ctor_impl(
125
+ this: *mut DDGameWrapper,
126
+ display: *mut DDDisplay,
127
+ sound: *mut DSSound,
128
+ gfx: *mut u8,
129
+ palette: *mut Palette,
130
+ music: *mut u8,
131
+ network: *mut u8,
132
+ ) -> *mut DDGameWrapper {
133
+ let game_info = GAME_INFO as *mut u8;
134
+
135
+ // Initialize DDGameWrapper fields (order matches original decompile).
136
+ (*this).ddgame = core::ptr::null_mut();
137
+ (*this).landscape = core::ptr::null_mut();
138
+ (*this).vtable = rb(va::DDGAME_WRAPPER_VTABLE) as *mut u8;
139
+ (*this).sound = sound;
140
+ (*this).display = display;
141
+
142
+ // Initialize replay subsystem. usercall(EAX=game_info, ESI=this), plain RET.
143
+ call_init_replay(game_info, this);
144
+
145
+ // Read timer_obj and net_game from the live game session struct.
146
+ let session = *(rb(va::G_GAME_SESSION) as *const *mut GameSession);
147
+ let timer_obj = (*session).timer_obj;
148
+ let net_game = (*session).net_game;
149
+
150
+ // Store network as the implicit ECX for DDGame::ctor, then tail-jump-call.
151
+ DDGAME_CTOR_ECX = network as u32;
152
+ ddgame_constructor_call(
153
+ this, display, sound, gfx, palette, music,
154
+ timer_obj, net_game, game_info,
155
+ );
156
+
157
+ // Initialize DDGame's game-state fields.
158
+ let init_state: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
159
+ core::mem::transmute(rb(va::DDGAME_INIT_GAME_STATE) as usize);
160
+ init_state(this);
161
+
162
+ let ddgame = (*this).ddgame;
163
  let _ = log_line(&format!(
164
+ "[GameSession] DDGameWrapper::Constructor: wrapper=0x{:08X} ddgame=0x{:08X}",
165
+ this as u32, ddgame as u32,
166
  ));
167
+
168
+ this
169
  }
170
 
171
+ // ─── Naked entry trampoline ───────────────────────────────────────────────────
172
+ //
173
+ // Stack on entry (stdcall 7 params + implicit EDI=game_info):
174
+ // [esp+0x00] = caller_ret
175
+ // [esp+0x04] = this
176
+ // [esp+0x08] = display
177
+ // [esp+0x0C] = sound
178
+ // [esp+0x10] = gfx
179
+ // [esp+0x14] = palette
180
+ // [esp+0x18] = music
181
+ // [esp+0x1C] = network
182
+ // EDI = game_info (implicit, must not be modified)
183
+ //
184
+ // Steps:
185
+ // 1. Capture EDI β†’ GAME_INFO via EAX scratch (EDI itself is never written).
186
+ // 2. Pop caller_ret β†’ SAVED_RET.
187
+ // 3. calll ctor_impl β€” args are in place; cdecl so callee doesn't clean stack.
188
+ // 4. addl $0x1C, %esp β€” simulate stdcall callee cleanup of 7 args.
189
+ // 5. jmpl *SAVED_RET β€” return to caller; EAX = this (ctor_impl's return value).
190
  #[unsafe(naked)]
191
+ unsafe extern "C" fn ddgamewrapper_constructor() {
192
  core::arch::naked_asm!(
193
+ // Use EAX as scratch to save EDI without touching EDI.
194
  "pushl %eax",
195
+ // [esp+0]=old_eax, [esp+4]=caller_ret, [esp+8]=this, ..., [esp+20]=network
196
+ "movl %edi, %eax",
197
+ "movl %eax, {game_info}", // GAME_INFO = EDI
198
+ "popl %eax", // restore EAX; stack = [caller_ret, this, ..., network]
199
+ "popl %eax", // EAX = caller_ret; stack = [this, display, ..., network]
200
+ "movl %eax, {saved_ret}", // SAVED_RET = caller_ret
201
+ "calll {impl_fn}", // ctor_impl(this, display, sound, gfx, palette, music, network)
202
+ // cdecl: stack unchanged after call; EAX = this.
203
+ "addl $0x1c, %esp", // stdcall callee-cleanup: discard 7 Γ— u32 args
204
+ "jmpl *{saved_ret}", // return to caller; EAX = this
205
+ game_info = sym GAME_INFO,
206
+ saved_ret = sym SAVED_RET,
207
+ impl_fn = sym ctor_impl,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  options(att_syntax),
209
  );
210
  }
211
 
212
  pub fn install() -> Result<(), String> {
213
  unsafe {
214
+ INIT_REPLAY_ADDR = rb(va::DDGAMEWRAPPER_INIT_REPLAY);
215
+ DDGAME_CTOR_ADDR = rb(va::CONSTRUCT_DD_GAME);
216
+ // Full replacement β€” trampoline not needed.
217
+ let _ = hook::install(
218
  "DDGameWrapper__Constructor",
219
  va::CONSTRUCT_DD_GAME_WRAPPER,
220
+ ddgamewrapper_constructor as *const (),
221
  )?;
 
222
  }
223
  Ok(())
224
  }