feat: add openwa-wormkit DLL with FrontendChangeScreen replacement
Browse filesNew 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 +9 -0
- crates/openwa-types/src/address.rs +16 -0
- crates/openwa-types/src/frontend.rs +137 -45
- crates/openwa-validator/src/hooks/detour.rs +3 -37
- crates/openwa-wormkit/Cargo.toml +15 -0
- crates/openwa-wormkit/src/lib.rs +66 -0
- crates/openwa-wormkit/src/rebase.rs +26 -0
- crates/openwa-wormkit/src/replacements/frontend.rs +118 -0
- crates/openwa-wormkit/src/replacements/mod.rs +6 -0
- crates/openwa-wormkit/src/wa_call.rs +46 -0
|
@@ -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"
|
|
@@ -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
|
|
@@ -1,51 +1,122 @@
|
|
| 1 |
-
/// Frontend screen
|
| 2 |
///
|
| 3 |
-
///
|
| 4 |
-
///
|
| 5 |
-
///
|
|
|
|
| 6 |
///
|
| 7 |
-
///
|
| 8 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
///
|
| 10 |
-
/// Source: Ghidra decompilation of
|
|
|
|
|
|
|
| 11 |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 12 |
#[repr(i32)]
|
| 13 |
pub enum ScreenId {
|
| 14 |
-
//
|
|
|
|
|
|
|
| 15 |
Cancel = 0,
|
| 16 |
-
///
|
| 17 |
-
|
| 18 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
LocalMultiplayer = 11,
|
| 20 |
-
///
|
| 21 |
-
|
| 22 |
-
///
|
| 23 |
-
|
| 24 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
SinglePlayer = 21,
|
| 26 |
-
///
|
| 27 |
-
|
| 28 |
-
///
|
| 29 |
-
|
| 30 |
-
///
|
| 31 |
-
|
| 32 |
-
///
|
| 33 |
-
|
| 34 |
-
/// Network setup
|
| 35 |
-
|
| 36 |
-
///
|
| 37 |
-
|
| 38 |
-
/// Network
|
| 39 |
-
|
| 40 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
Lan = 1702,
|
| 42 |
-
/// WormNET online lobby
|
| 43 |
WormNet = 1703,
|
| 44 |
-
/// Lobby
|
| 45 |
LobbyHost = 1704,
|
| 46 |
-
/// Lobby
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
11 => Ok(Self::LocalMultiplayer),
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
21 => Ok(Self::SinglePlayer),
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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),
|
|
@@ -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 |
-
|
| 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 |
-
|
| 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("
|
| 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 |
}
|
|
@@ -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"] }
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mod frontend;
|
| 2 |
+
|
| 3 |
+
pub fn install_all() -> Result<(), String> {
|
| 4 |
+
frontend::install()?;
|
| 5 |
+
Ok(())
|
| 6 |
+
}
|
|
@@ -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 |
+
}
|