paavohtl Claude Opus 4.6 commited on
Commit
903360a
·
1 Parent(s): ec26eaa

feat: add openwa-wormkit DLL with FrontendChangeScreen replacement

Browse files

New crate openwa-wormkit (wkOpenWA.dll) that replaces WA.exe functions
with Rust reimplementations, starting with FrontendChangeScreen (0x447A20).

- Naked ASM trampoline captures ESI (implicit dialog this pointer)
- Full reimplementation of both code paths (init + normal EndDialog)
- ScreenId enum expanded to 35 variants with correct semantics:
screen IDs (main-loop dispatch targets) vs result codes (sub-dialog
exit values like offline/online pairs 35/36 and 37/38)
- New globals: g_CurrentScreen (0x6B3504), g_SkipToMainMenu, etc.
- New addresses: Frontend__MainNavigationLoop, DoModal wrapper, etc.
- Validator: removed FrontendChangeScreen hook (conflicts with wormkit),
silenced constructor hooks to reduce log noise

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

Cargo.lock CHANGED
@@ -68,6 +68,15 @@ dependencies = [
68
  "windows-sys",
69
  ]
70
 
 
 
 
 
 
 
 
 
 
71
  [[package]]
72
  name = "pin-project-lite"
73
  version = "0.2.17"
 
68
  "windows-sys",
69
  ]
70
 
71
+ [[package]]
72
+ name = "openwa-wormkit"
73
+ version = "0.1.0"
74
+ dependencies = [
75
+ "minhook",
76
+ "openwa-types",
77
+ "windows-sys",
78
+ ]
79
+
80
  [[package]]
81
  name = "pin-project-lite"
82
  version = "0.2.17"
crates/openwa-types/src/address.rs CHANGED
@@ -165,12 +165,18 @@ pub mod va {
165
 
166
  // === Frontend / menu screens ===
167
 
 
 
168
  pub const FRONTEND_CHANGE_SCREEN: u32 = 0x0044_7A20;
 
 
169
  pub const FRONTEND_FRAME_CONSTRUCTOR: u32 = 0x004E_CCA0;
170
  pub const FRONTEND_DIALOG_CONSTRUCTOR: u32 = 0x0044_6BA0;
171
  pub const FRONTEND_PALETTE_ANIMATION: u32 = 0x0042_2180;
172
  pub const FRONTEND_LOAD_TRANSITION_PAL: u32 = 0x0044_7AA0;
173
  pub const FRONTEND_PRE_TRANSITION_CLEANUP: u32 = 0x004E_4AE0;
 
 
174
  pub const FRONTEND_ON_INITIAL_LOAD: u32 = 0x0042_9830;
175
  pub const FRONTEND_LAUNCH_SINGLE_PLAYER: u32 = 0x0044_1D80;
176
  pub const FRONTEND_ON_MULTIPLAYER: u32 = 0x0044_E850;
@@ -179,6 +185,8 @@ pub mod va {
179
  pub const FRONTEND_ON_OPTIONS_ACCEPT: u32 = 0x0048_DAB0;
180
  pub const FRONTEND_ON_START_GAME: u32 = 0x004F_14A0;
181
  pub const CDIALOG_DO_MODAL_CUSTOM: u32 = 0x0040_FD60;
 
 
182
 
183
  // === Lobby / network ===
184
 
@@ -201,10 +209,18 @@ pub mod va {
201
 
202
  // === Global variables (in .data) ===
203
 
 
 
204
  /// Main frontend frame window (CWnd*)
205
  pub const G_FRONTEND_FRAME: u32 = 0x006B_3908;
206
  /// Main frontend HWND
207
  pub const G_FRONTEND_HWND: u32 = 0x006B_390C;
 
 
 
 
 
 
208
  /// Network mode (0=LAN, nonzero=WormNET)
209
  pub const G_NETWORK_MODE: u32 = 0x007C_0D40;
210
  /// Network sub-type selector
 
165
 
166
  // === Frontend / menu screens ===
167
 
168
+ /// Main navigation loop (CWinApp::Run override) — dispatches on g_CurrentScreen
169
+ pub const FRONTEND_MAIN_NAVIGATION_LOOP: u32 = 0x004E_6440;
170
  pub const FRONTEND_CHANGE_SCREEN: u32 = 0x0044_7A20;
171
+ /// Wraps DoModal: palette transition + custom DoModal
172
+ pub const FRONTEND_DO_MODAL_WRAPPER: u32 = 0x0044_7960;
173
  pub const FRONTEND_FRAME_CONSTRUCTOR: u32 = 0x004E_CCA0;
174
  pub const FRONTEND_DIALOG_CONSTRUCTOR: u32 = 0x0044_6BA0;
175
  pub const FRONTEND_PALETTE_ANIMATION: u32 = 0x0042_2180;
176
  pub const FRONTEND_LOAD_TRANSITION_PAL: u32 = 0x0044_7AA0;
177
  pub const FRONTEND_PRE_TRANSITION_CLEANUP: u32 = 0x004E_4AE0;
178
+ /// Post-screen cleanup: destroys previous dialog
179
+ pub const FRONTEND_POST_SCREEN_CLEANUP: u32 = 0x004E_B450;
180
  pub const FRONTEND_ON_INITIAL_LOAD: u32 = 0x0042_9830;
181
  pub const FRONTEND_LAUNCH_SINGLE_PLAYER: u32 = 0x0044_1D80;
182
  pub const FRONTEND_ON_MULTIPLAYER: u32 = 0x0044_E850;
 
185
  pub const FRONTEND_ON_OPTIONS_ACCEPT: u32 = 0x0048_DAB0;
186
  pub const FRONTEND_ON_START_GAME: u32 = 0x004F_14A0;
187
  pub const CDIALOG_DO_MODAL_CUSTOM: u32 = 0x0040_FD60;
188
+ /// Custom message pump (replaces MFC's RunModalLoop)
189
+ pub const CDIALOG_CUSTOM_MSG_PUMP: u32 = 0x0040_FBE0;
190
 
191
  // === Lobby / network ===
192
 
 
209
 
210
  // === Global variables (in .data) ===
211
 
212
+ /// Current screen ID driving the main navigation loop dispatch
213
+ pub const G_CURRENT_SCREEN: u32 = 0x006B_3504;
214
  /// Main frontend frame window (CWnd*)
215
  pub const G_FRONTEND_FRAME: u32 = 0x006B_3908;
216
  /// Main frontend HWND
217
  pub const G_FRONTEND_HWND: u32 = 0x006B_390C;
218
+ /// Skip-to-main-menu flag (set after intro movie)
219
+ pub const G_SKIP_TO_MAIN_MENU: u32 = 0x007A_083D;
220
+ /// Auto-network mode flag
221
+ pub const G_AUTO_NETWORK_FLAG: u32 = 0x007A_083F;
222
+ /// Main menu active flag (0xFF during screen 18)
223
+ pub const G_MAIN_MENU_ACTIVE: u32 = 0x007C_0A20;
224
  /// Network mode (0=LAN, nonzero=WormNET)
225
  pub const G_NETWORK_MODE: u32 = 0x007C_0D40;
226
  /// Network sub-type selector
crates/openwa-types/src/frontend.rs CHANGED
@@ -1,51 +1,122 @@
1
- /// Frontend screen identifiers.
2
  ///
3
- /// Used by FrontendChangeScreen (0x447A20) to transition between
4
- /// menu screens. The screen ID is passed to CDialog::EndDialog as
5
- /// the result code, then returned from DoModal to drive navigation.
 
6
  ///
7
- /// The frontend uses MFC CDialog modal dialogs, not the CTask hierarchy.
8
- /// The main frame window is stored at global 0x6B3908.
 
 
 
 
 
 
9
  ///
10
- /// Source: Ghidra decompilation of 65 call sites + wkJellyWorm Lobby.cpp
 
 
11
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12
  #[repr(i32)]
13
  pub enum ScreenId {
14
- /// Return to previous screen / cancel action
 
 
15
  Cancel = 0,
16
- /// Main menu screen
17
- MainMenu = 10,
18
- /// Local multiplayer setup
 
 
 
 
 
 
 
 
 
 
 
19
  LocalMultiplayer = 11,
20
- /// Window minimized / inactive state (checked via IsIconic)
21
- Minimized = 15,
22
- /// Initial game loading screen
23
- InitialLoad = 16,
24
- /// Single player game (Deathmatch)
 
 
 
 
 
 
 
 
25
  SinglePlayer = 21,
26
- /// Network options sub-screen
27
- NetworkSubScreen = 26,
28
- /// Start game path A (game setup, param+0x244 == 0)
29
- StartGameA = 35,
30
- /// Start game path B (game setup fallthrough)
31
- StartGameB = 36,
32
- /// Network setup path A (param+0x140 == 0)
33
- NetworkSetupA = 37,
34
- /// Network setup path B (fallthrough)
35
- NetworkSetupB = 38,
36
- /// Options accepted / theme saved (writes current.thm)
37
- OptionsAccept = 39,
38
- /// Network provider selection screen
39
- NetworkSelection = 1700,
40
- /// LAN game
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  Lan = 1702,
42
- /// WormNET online lobby
43
  WormNet = 1703,
44
- /// Lobby as host
45
  LobbyHost = 1704,
46
- /// Lobby as client / join game
 
 
47
  LobbyClient = 1706,
48
- /// Lobby game start
49
  LobbyGameStart = 1707,
50
  }
51
 
@@ -55,21 +126,42 @@ impl TryFrom<i32> for ScreenId {
55
  fn try_from(value: i32) -> Result<Self, Self::Error> {
56
  match value {
57
  0 => Ok(Self::Cancel),
58
- 10 => Ok(Self::MainMenu),
 
 
 
 
59
  11 => Ok(Self::LocalMultiplayer),
60
- 15 => Ok(Self::Minimized),
61
- 16 => Ok(Self::InitialLoad),
 
 
 
 
62
  21 => Ok(Self::SinglePlayer),
63
- 26 => Ok(Self::NetworkSubScreen),
64
- 35 => Ok(Self::StartGameA),
65
- 36 => Ok(Self::StartGameB),
66
- 37 => Ok(Self::NetworkSetupA),
67
- 38 => Ok(Self::NetworkSetupB),
68
- 39 => Ok(Self::OptionsAccept),
69
- 1700 => Ok(Self::NetworkSelection),
 
 
 
 
 
 
 
 
 
 
 
 
70
  1702 => Ok(Self::Lan),
71
  1703 => Ok(Self::WormNet),
72
  1704 => Ok(Self::LobbyHost),
 
73
  1706 => Ok(Self::LobbyClient),
74
  1707 => Ok(Self::LobbyGameStart),
75
  _ => Err(value),
 
1
+ /// Frontend screen/result IDs used by the navigation state machine.
2
  ///
3
+ /// The main navigation loop lives at 0x4E6440 (CWinApp::Run override).
4
+ /// It reads `g_CurrentScreen` (0x6B3504) and dispatches to a dialog via
5
+ /// DoModal. FrontendChangeScreen (0x447A20) calls EndDialog(screen_id),
6
+ /// which becomes the DoModal return value, stored back into g_CurrentScreen.
7
  ///
8
+ /// IDs fall into two categories:
9
+ /// - **Screen IDs** (10-30, 1700-1707): valid dispatch targets in the main loop,
10
+ /// each corresponding to a specific dialog constructor.
11
+ /// - **Result codes** (0-5, 32, 35-39, 50-58): returned by EndDialog but NOT
12
+ /// dispatched by the main loop. They either signal "cancel/back" (0), trigger
13
+ /// exit (unknown IDs hit default → exit), or are only meaningful within
14
+ /// sub-dialog contexts (e.g., button handlers that launch the game before
15
+ /// the main loop sees the code).
16
  ///
17
+ /// Source: Ghidra decompilation of Frontend__MainNavigationLoop (0x4E6440),
18
+ /// 65 FrontendChangeScreen call sites, wkJellyWorm Lobby.cpp,
19
+ /// + runtime observation via openwa-wormkit
20
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21
  #[repr(i32)]
22
  pub enum ScreenId {
23
+ // --- Dialog result codes (not main-loop dispatch targets) ---
24
+
25
+ /// Cancel / return to previous screen (EndDialog result only)
26
  Cancel = 0,
27
+ /// Splash / intro trigger (result code)
28
+ Splash = 1,
29
+ /// Network option result A
30
+ NetworkOptionA = 2,
31
+ /// Single player prep result
32
+ SinglePlayerPrep = 4,
33
+ /// Single player confirm result
34
+ SinglePlayerConfirm = 5,
35
+
36
+ // --- Valid main-loop screen IDs ---
37
+
38
+ /// Deathmatch setup dialog (constructor 0x440F40)
39
+ Deathmatch = 10,
40
+ /// Local multiplayer setup dialog (constructor 0x49C420)
41
  LocalMultiplayer = 11,
42
+ /// Training mode dialog (constructor 0x4E0880)
43
+ Training = 12,
44
+ /// Missions dialog (constructor 0x499190)
45
+ Missions = 14,
46
+ /// Exit frontend — cleans up and returns from CWinApp::Run
47
+ Exit = 15,
48
+ /// Post-init main menu (constructor 0x4C91B0)
49
+ PostInitMenu = 16,
50
+ /// Redirect to MainMenu (sets g_CurrentScreen = 18)
51
+ MainMenuRedirect = 17,
52
+ /// Main menu dialog (constructor 0x4866C0)
53
+ MainMenu = 18,
54
+ /// Single player game dialog (constructor 0x4D69F0)
55
  SinglePlayer = 21,
56
+ /// Campaign selection A dialog (constructor 0x4A2B70)
57
+ CampaignA = 22,
58
+ /// Campaign selection B dialog (constructor 0x4A24D0)
59
+ CampaignB = 23,
60
+ /// Advanced settings dialog (constructor 0x4279E0)
61
+ AdvancedSettings = 24,
62
+ /// Intro/splash movie screen (constructor 0x470870, hardcodes next=18)
63
+ IntroMovie = 25,
64
+ /// Network host setup dialog (constructor 0x4ADCA0)
65
+ NetworkHostSetup = 27,
66
+ /// Network online setup dialog (constructor 0x4ACBC0)
67
+ NetworkOnlineSetup = 28,
68
+ /// Network dialog A (constructor 0x4BC970)
69
+ NetworkDialogA = 29,
70
+ /// Network dialog B (constructor 0x4BBC00)
71
+ NetworkDialogB = 30,
72
+
73
+ // --- Dialog result codes (not main-loop dispatch targets) ---
74
+ //
75
+ // These are returned by EndDialog from sub-dialogs (button handlers,
76
+ // options screens, etc.). If they reach the main loop, they hit the
77
+ // default case → exit. But by then the game has already launched or
78
+ // the action has been taken.
79
+ //
80
+ // 35/36 and 37/38 form offline/online pairs:
81
+ // Start Game path: this+0x244 flag → 35 (no network) / 36 (network)
82
+ // Multiplayer path: this+0x140 flag → 37 (no network) / 38 (network)
83
+
84
+ /// Scheme/weapon editor done — return from editor
85
+ SchemeEditorDone = 32,
86
+ /// Start game, network unavailable (offline/local path)
87
+ StartGameOffline = 35,
88
+ /// Start game, network available (online path)
89
+ StartGameOnline = 36,
90
+ /// Multiplayer, network unavailable (local play path)
91
+ MultiplayerLocal = 37,
92
+ /// Multiplayer, network available (online play path)
93
+ MultiplayerOnline = 38,
94
+ /// Options accepted — return screen after applying options + saving theme
95
+ OptionsAccepted = 39,
96
+ /// Sub-dialog result: in-game state A (hits default → exit)
97
+ InGameA = 50,
98
+ /// Sub-dialog result: in-game state B (hits default → exit)
99
+ InGameB = 52,
100
+ /// Sub-dialog result: in-game state C (hits default → exit)
101
+ InGameC = 58,
102
+
103
+ // --- Network/lobby screen IDs (high range, 0x6A4-0x6AB) ---
104
+
105
+ /// Network provider selection dialog (constructor 0x4A7990)
106
+ NetworkProviderSelect = 1700,
107
+ /// Network settings dialog (constructor 0x4C23C0)
108
+ NetworkSettings = 1701,
109
+ /// LAN game dialog (constructor 0x480A80)
110
  Lan = 1702,
111
+ /// WormNET online lobby dialog (constructor 0x472400)
112
  WormNet = 1703,
113
+ /// Lobby host dialog (constructor 0x4B0160)
114
  LobbyHost = 1704,
115
+ /// Lobby host continued (fall-through from 1704 setup)
116
+ LobbyHostContinued = 1705,
117
+ /// Lobby client / join game dialog
118
  LobbyClient = 1706,
119
+ /// Lobby game start dialog (constructor 0x4BDBE0)
120
  LobbyGameStart = 1707,
121
  }
122
 
 
126
  fn try_from(value: i32) -> Result<Self, Self::Error> {
127
  match value {
128
  0 => Ok(Self::Cancel),
129
+ 1 => Ok(Self::Splash),
130
+ 2 => Ok(Self::NetworkOptionA),
131
+ 4 => Ok(Self::SinglePlayerPrep),
132
+ 5 => Ok(Self::SinglePlayerConfirm),
133
+ 10 => Ok(Self::Deathmatch),
134
  11 => Ok(Self::LocalMultiplayer),
135
+ 12 => Ok(Self::Training),
136
+ 14 => Ok(Self::Missions),
137
+ 15 => Ok(Self::Exit),
138
+ 16 => Ok(Self::PostInitMenu),
139
+ 17 => Ok(Self::MainMenuRedirect),
140
+ 18 => Ok(Self::MainMenu),
141
  21 => Ok(Self::SinglePlayer),
142
+ 22 => Ok(Self::CampaignA),
143
+ 23 => Ok(Self::CampaignB),
144
+ 24 => Ok(Self::AdvancedSettings),
145
+ 25 => Ok(Self::IntroMovie),
146
+ 27 => Ok(Self::NetworkHostSetup),
147
+ 28 => Ok(Self::NetworkOnlineSetup),
148
+ 29 => Ok(Self::NetworkDialogA),
149
+ 30 => Ok(Self::NetworkDialogB),
150
+ 32 => Ok(Self::SchemeEditorDone),
151
+ 35 => Ok(Self::StartGameOffline),
152
+ 36 => Ok(Self::StartGameOnline),
153
+ 37 => Ok(Self::MultiplayerLocal),
154
+ 38 => Ok(Self::MultiplayerOnline),
155
+ 39 => Ok(Self::OptionsAccepted),
156
+ 50 => Ok(Self::InGameA),
157
+ 52 => Ok(Self::InGameB),
158
+ 58 => Ok(Self::InGameC),
159
+ 1700 => Ok(Self::NetworkProviderSelect),
160
+ 1701 => Ok(Self::NetworkSettings),
161
  1702 => Ok(Self::Lan),
162
  1703 => Ok(Self::WormNet),
163
  1704 => Ok(Self::LobbyHost),
164
+ 1705 => Ok(Self::LobbyHostContinued),
165
  1706 => Ok(Self::LobbyClient),
166
  1707 => Ok(Self::LobbyGameStart),
167
  _ => Err(value),
crates/openwa-validator/src/hooks/detour.rs CHANGED
@@ -33,23 +33,6 @@ unsafe fn install_hook(ghidra_addr: u32, detour_fn: *const (), name: &str) -> Re
33
  Ok(trampoline_addr)
34
  }
35
 
36
- // ---------------------------------------------------------------------------
37
- // FrontendChangeScreen hook
38
- // ---------------------------------------------------------------------------
39
-
40
- static ORIG_FRONTEND_CHANGE_SCREEN: AtomicU32 = AtomicU32::new(0);
41
-
42
- /// Hook for FrontendChangeScreen (0x447A20).
43
- /// Called on every menu screen transition. Logs the screen ID.
44
- ///
45
- /// RET 0x4 → stdcall with 1 param (screen_id). ESI = dialog this (implicit).
46
- unsafe extern "stdcall" fn hook_frontend_change_screen(screen_id: u32) {
47
- let _ = log_line(&format!("[HOOK] FrontendChangeScreen(screen_id={screen_id})"));
48
- let orig: unsafe extern "stdcall" fn(u32) =
49
- core::mem::transmute(ORIG_FRONTEND_CHANGE_SCREEN.load(Ordering::Relaxed));
50
- orig(screen_id);
51
- }
52
-
53
  // ---------------------------------------------------------------------------
54
  // CTask::Constructor hook
55
  // ---------------------------------------------------------------------------
@@ -62,12 +45,7 @@ static ORIG_CTASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
62
  unsafe extern "stdcall" fn hook_ctask_constructor(this: u32, parent: u32, ddgame: u32) -> u32 {
63
  let orig: unsafe extern "stdcall" fn(u32, u32, u32) -> u32 =
64
  core::mem::transmute(ORIG_CTASK_CONSTRUCTOR.load(Ordering::Relaxed));
65
- let result = orig(this, parent, ddgame);
66
- // Log after calling original (so vtable is set)
67
- let _ = log_line(&format!(
68
- "[HOOK] CTask::ctor(this=0x{this:08X}, parent=0x{parent:08X}, ddgame=0x{ddgame:08X}) -> 0x{result:08X}"
69
- ));
70
- result
71
  }
72
 
73
  // ---------------------------------------------------------------------------
@@ -82,11 +60,7 @@ static ORIG_CGAMETASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
82
  unsafe extern "stdcall" fn hook_cgametask_constructor(this: u32, parent: u32, param3: u32, param4: u32) -> u32 {
83
  let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
84
  core::mem::transmute(ORIG_CGAMETASK_CONSTRUCTOR.load(Ordering::Relaxed));
85
- let result = orig(this, parent, param3, param4);
86
- let _ = log_line(&format!(
87
- "[HOOK] CGameTask::ctor(this=0x{this:08X}, parent=0x{parent:08X}) -> 0x{result:08X}"
88
- ));
89
- result
90
  }
91
 
92
  // ---------------------------------------------------------------------------
@@ -95,14 +69,6 @@ unsafe extern "stdcall" fn hook_cgametask_constructor(this: u32, parent: u32, pa
95
 
96
  pub fn install() -> Result<(), String> {
97
  unsafe {
98
- // FrontendChangeScreen — called on every menu navigation
99
- let trampoline = install_hook(
100
- va::FRONTEND_CHANGE_SCREEN,
101
- hook_frontend_change_screen as *const (),
102
- "FrontendChangeScreen",
103
- )?;
104
- ORIG_FRONTEND_CHANGE_SCREEN.store(trampoline, Ordering::Relaxed);
105
-
106
  // CTask::Constructor — base class constructor
107
  let trampoline = install_hook(
108
  va::CTASK_CONSTRUCTOR,
@@ -120,6 +86,6 @@ pub fn install() -> Result<(), String> {
120
  ORIG_CGAMETASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
121
  }
122
 
123
- let _ = log_line(" 3 inline hooks installed via MinHook");
124
  Ok(())
125
  }
 
33
  Ok(trampoline_addr)
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  // ---------------------------------------------------------------------------
37
  // CTask::Constructor hook
38
  // ---------------------------------------------------------------------------
 
45
  unsafe extern "stdcall" fn hook_ctask_constructor(this: u32, parent: u32, ddgame: u32) -> u32 {
46
  let orig: unsafe extern "stdcall" fn(u32, u32, u32) -> u32 =
47
  core::mem::transmute(ORIG_CTASK_CONSTRUCTOR.load(Ordering::Relaxed));
48
+ orig(this, parent, ddgame)
 
 
 
 
 
49
  }
50
 
51
  // ---------------------------------------------------------------------------
 
60
  unsafe extern "stdcall" fn hook_cgametask_constructor(this: u32, parent: u32, param3: u32, param4: u32) -> u32 {
61
  let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
62
  core::mem::transmute(ORIG_CGAMETASK_CONSTRUCTOR.load(Ordering::Relaxed));
63
+ orig(this, parent, param3, param4)
 
 
 
 
64
  }
65
 
66
  // ---------------------------------------------------------------------------
 
69
 
70
  pub fn install() -> Result<(), String> {
71
  unsafe {
 
 
 
 
 
 
 
 
72
  // CTask::Constructor — base class constructor
73
  let trampoline = install_hook(
74
  va::CTASK_CONSTRUCTOR,
 
86
  ORIG_CGAMETASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
87
  }
88
 
89
+ let _ = log_line(" 2 inline hooks installed via MinHook");
90
  Ok(())
91
  }
crates/openwa-wormkit/Cargo.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "openwa-wormkit"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "WormKit DLL that replaces WA.exe functions with Rust reimplementations"
6
+
7
+ [lib]
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ openwa-types = { path = "../openwa-types" }
12
+ minhook = "0.9"
13
+
14
+ [target.'cfg(target_os = "windows")'.dependencies]
15
+ windows-sys = { version = "0.59", features = ["Win32_System_Memory"] }
crates/openwa-wormkit/src/lib.rs ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #![allow(non_snake_case)]
2
+
3
+ use std::ffi::c_void;
4
+
5
+ pub mod rebase;
6
+ pub mod wa_call;
7
+ mod replacements;
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // DllMain
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const DLL_PROCESS_ATTACH: u32 = 1;
14
+
15
+ #[no_mangle]
16
+ unsafe extern "system" fn DllMain(
17
+ _module: *mut c_void,
18
+ reason: u32,
19
+ _reserved: *mut c_void,
20
+ ) -> i32 {
21
+ if reason == DLL_PROCESS_ATTACH {
22
+ std::thread::spawn(|| {
23
+ if let Err(e) = run() {
24
+ let _ = log_line(&format!("[FATAL] {e}"));
25
+ }
26
+ });
27
+ }
28
+ 1 // TRUE
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Logging
33
+ // ---------------------------------------------------------------------------
34
+
35
+ pub fn log_line(msg: &str) -> std::io::Result<()> {
36
+ use std::io::Write;
37
+ let mut f = std::fs::OpenOptions::new()
38
+ .create(true)
39
+ .append(true)
40
+ .open("OpenWA.log")?;
41
+ writeln!(f, "{msg}")?;
42
+ Ok(())
43
+ }
44
+
45
+ fn clear_log() -> std::io::Result<()> {
46
+ std::fs::write("OpenWA.log", "")?;
47
+ Ok(())
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Main entry
52
+ // ---------------------------------------------------------------------------
53
+
54
+ fn run() -> Result<(), String> {
55
+ let _ = clear_log();
56
+
57
+ let delta = rebase::init();
58
+ let _ = log_line(&format!(
59
+ "=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
60
+ ));
61
+
62
+ replacements::install_all()?;
63
+
64
+ let _ = log_line("=== All replacements installed ===");
65
+ Ok(())
66
+ }
crates/openwa-wormkit/src/rebase.rs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! ASLR rebasing — convert Ghidra VAs to runtime addresses.
2
+
3
+ use std::ffi::c_void;
4
+ use std::sync::atomic::{AtomicU32, Ordering};
5
+
6
+ use openwa_types::address::va;
7
+
8
+ /// Delta to add to Ghidra addresses to get runtime addresses.
9
+ static REBASE_DELTA: AtomicU32 = AtomicU32::new(0);
10
+
11
+ /// Rebase a Ghidra VA to the actual runtime address.
12
+ #[inline]
13
+ pub fn rb(ghidra_addr: u32) -> u32 {
14
+ ghidra_addr.wrapping_add(REBASE_DELTA.load(Ordering::Relaxed))
15
+ }
16
+
17
+ extern "system" {
18
+ fn GetModuleHandleA(lpModuleName: *const u8) -> *mut c_void;
19
+ }
20
+
21
+ pub fn init() -> i32 {
22
+ let base = unsafe { GetModuleHandleA(std::ptr::null()) } as u32;
23
+ let delta = base.wrapping_sub(va::IMAGE_BASE);
24
+ REBASE_DELTA.store(delta, Ordering::Relaxed);
25
+ delta as i32
26
+ }
crates/openwa-wormkit/src/replacements/frontend.rs ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! FrontendChangeScreen replacement.
2
+ //!
3
+ //! Original: 0x447A20, stdcall(screen_id), ESI = dialog this (__usercall).
4
+ //! Navigates between frontend menu screens via MFC CDialog::EndDialog.
5
+
6
+ use std::ffi::c_void;
7
+ use std::sync::atomic::{AtomicU32, Ordering};
8
+
9
+ use minhook::MinHook;
10
+
11
+ use crate::log_line;
12
+ use crate::rebase::rb;
13
+ use crate::wa_call;
14
+ use openwa_types::address::va;
15
+ use openwa_types::frontend::ScreenId;
16
+
17
+ /// Trampoline to the original FrontendChangeScreen (for fallback if needed).
18
+ static ORIG_FRONTEND_CHANGE_SCREEN: AtomicU32 = AtomicU32::new(0);
19
+
20
+ /// Naked trampoline that captures ESI (the implicit dialog this pointer)
21
+ /// and routes it to our cdecl Rust implementation.
22
+ ///
23
+ /// Stack on entry (from MinHook redirect):
24
+ /// [ESP+0] = return address
25
+ /// [ESP+4] = screen_id
26
+ /// ESI = dialog this (MSVC __usercall convention)
27
+ #[unsafe(naked)]
28
+ unsafe extern "stdcall" fn trampoline(_screen_id: u32) {
29
+ core::arch::naked_asm!(
30
+ "push [esp+4]", // push screen_id
31
+ "push esi", // push dialog this
32
+ "call {impl_fn}", // cdecl call: impl(dialog, screen_id)
33
+ "add esp, 8", // clean our two pushes
34
+ "ret 0x4", // clean original screen_id from caller's stack
35
+ impl_fn = sym frontend_change_screen_impl,
36
+ );
37
+ }
38
+
39
+ /// Rust reimplementation of FrontendChangeScreen.
40
+ ///
41
+ /// Two code paths based on g_FrontendFrame:
42
+ /// - If 0 (initializing): store screen_id in dialog fields
43
+ /// - If nonzero (normal): disable window → palette anim → vtable calls → EndDialog
44
+ unsafe extern "cdecl" fn frontend_change_screen_impl(dialog: u32, screen_id: u32) {
45
+ let g_frontend_frame = wa_call::read_global(va::G_FRONTEND_FRAME);
46
+
47
+ if g_frontend_frame == 0 {
48
+ // Init path: store screen_id, clear flag bit 0x10
49
+ let flags = *((dialog + 0x3c) as *const u32);
50
+ if (flags & 0x18) != 0 {
51
+ *((dialog + 0x44) as *mut u32) = screen_id;
52
+ *((dialog + 0x3c) as *mut u32) = flags & 0xFFFF_FFEF;
53
+ }
54
+ } else {
55
+ // Normal path: full screen transition
56
+
57
+ // CWnd::EnableWindow(dialog, FALSE)
58
+ wa_call::thiscall_1(0x5C647A, dialog, 0);
59
+
60
+ // Frontend__PaletteAnimation(&DAT_007be560, [dialog+0x134])
61
+ // Note: original also loads [ESI+0x12c] into EAX before the call.
62
+ // We replicate this with inline asm to set EAX as the implicit param.
63
+ let palette_param = *((dialog + 0x134) as *const u32);
64
+ let eax_value = *((dialog + 0x12c) as *const u32);
65
+ let palette_anim_addr = rb(va::FRONTEND_PALETTE_ANIMATION);
66
+ let palette_data_addr = rb(0x7be560);
67
+ core::arch::asm!(
68
+ "push {param}",
69
+ "push {palette}",
70
+ "call {func}",
71
+ param = in(reg) palette_param,
72
+ palette = in(reg) palette_data_addr,
73
+ func = in(reg) palette_anim_addr,
74
+ in("eax") eax_value,
75
+ clobber_abi("C"),
76
+ );
77
+
78
+ // Virtual calls: vtable[0x15C](1) then vtable[0x15C](2)
79
+ for i in 1u32..=2 {
80
+ let vtable = *(dialog as *const u32);
81
+ wa_call::thiscall_indirect_1(vtable + 0x15C, dialog, i);
82
+ }
83
+
84
+ // CWnd::EnableWindow(dialog, TRUE)
85
+ wa_call::thiscall_1(0x5C647A, dialog, 1);
86
+
87
+ // CDialog::EndDialog(dialog, screen_id)
88
+ wa_call::thiscall_1(0x5CAB72, dialog, screen_id);
89
+ }
90
+
91
+ // Log the transition
92
+ let name = ScreenId::try_from(screen_id as i32)
93
+ .map(|s| format!("{s:?}"))
94
+ .unwrap_or_else(|v| format!("Unknown({v})"));
95
+ let _ = log_line(&format!("[FrontendChangeScreen] screen_id={screen_id} ({name})"));
96
+ }
97
+
98
+ pub fn install() -> Result<(), String> {
99
+ unsafe {
100
+ let target = rb(va::FRONTEND_CHANGE_SCREEN) as *mut c_void;
101
+ let detour = trampoline as *const () as *mut c_void;
102
+
103
+ let trampoline_ptr = MinHook::create_hook(target, detour)
104
+ .map_err(|e| format!("MinHook create_hook failed for FrontendChangeScreen: {e}"))?;
105
+
106
+ MinHook::enable_hook(target)
107
+ .map_err(|e| format!("MinHook enable_hook failed for FrontendChangeScreen: {e}"))?;
108
+
109
+ ORIG_FRONTEND_CHANGE_SCREEN.store(trampoline_ptr as u32, Ordering::Relaxed);
110
+
111
+ let _ = log_line(&format!(
112
+ " [REPLACE] FrontendChangeScreen: target 0x{:08X}, trampoline 0x{:08X}",
113
+ target as u32, trampoline_ptr as u32
114
+ ));
115
+ }
116
+
117
+ Ok(())
118
+ }
crates/openwa-wormkit/src/replacements/mod.rs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ mod frontend;
2
+
3
+ pub fn install_all() -> Result<(), String> {
4
+ frontend::install()?;
5
+ Ok(())
6
+ }
crates/openwa-wormkit/src/wa_call.rs ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Helpers for calling WA.exe functions by Ghidra address.
2
+ //!
3
+ //! These handle ASLR rebasing and calling convention translation.
4
+
5
+ use crate::rebase::rb;
6
+
7
+ /// Read a u32 global variable at a Ghidra address.
8
+ #[inline]
9
+ pub unsafe fn read_global(ghidra_addr: u32) -> u32 {
10
+ *(rb(ghidra_addr) as *const u32)
11
+ }
12
+
13
+ /// Call a thiscall method (ECX = this) with 1 stack argument.
14
+ /// Uses the fastcall trick: ECX = this, EDX = dummy.
15
+ #[inline]
16
+ pub unsafe fn thiscall_1(ghidra_addr: u32, this: u32, arg1: u32) {
17
+ let f: unsafe extern "fastcall" fn(u32, u32, u32) =
18
+ core::mem::transmute(rb(ghidra_addr));
19
+ f(this, 0, arg1);
20
+ }
21
+
22
+ /// Call a thiscall method (ECX = this) with 1 stack argument, returning u32.
23
+ #[inline]
24
+ pub unsafe fn thiscall_1_ret(ghidra_addr: u32, this: u32, arg1: u32) -> u32 {
25
+ let f: unsafe extern "fastcall" fn(u32, u32, u32) -> u32 =
26
+ core::mem::transmute(rb(ghidra_addr));
27
+ f(this, 0, arg1)
28
+ }
29
+
30
+ /// Call a thiscall method via an indirect pointer (vtable entry).
31
+ /// Reads the function pointer from `vtable_slot_addr` then calls it.
32
+ #[inline]
33
+ pub unsafe fn thiscall_indirect_1(vtable_slot_addr: u32, this: u32, arg1: u32) {
34
+ let fn_ptr = *(vtable_slot_addr as *const u32);
35
+ let f: unsafe extern "fastcall" fn(u32, u32, u32) =
36
+ core::mem::transmute(fn_ptr);
37
+ f(this, 0, arg1);
38
+ }
39
+
40
+ /// Call a stdcall function with 2 arguments.
41
+ #[inline]
42
+ pub unsafe fn stdcall_2(ghidra_addr: u32, arg1: u32, arg2: u32) {
43
+ let f: unsafe extern "stdcall" fn(u32, u32) =
44
+ core::mem::transmute(rb(ghidra_addr));
45
+ f(arg1, arg2);
46
+ }