paavohtl Claude Opus 4.6 commited on
Commit
2438f44
·
1 Parent(s): 7c9e8ea

chore: remove old openwa-validator crate (now part of openwa-wormkit)

Browse files
crates/openwa-validator/Cargo.toml DELETED
@@ -1,20 +0,0 @@
1
- [package]
2
- name = "openwa-validator"
3
- version = "0.1.0"
4
- edition = "2021"
5
- description = "WormKit DLL that validates openwa-types against live WA.exe memory"
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 = [
16
- "Win32_System_Memory",
17
- "Win32_System_LibraryLoader",
18
- "Win32_System_Threading",
19
- "Win32_UI_Input_KeyboardAndMouse",
20
- ] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-validator/src/hooks/detour.rs DELETED
@@ -1,91 +0,0 @@
1
- //! MinHook-based inline hooking for arbitrary functions.
2
- //!
3
- //! Uses MinHook to create trampolines that correctly handle SEH prologues
4
- //! (which retour-rs could not). This allows hooking constructors and free
5
- //! functions that aren't in vtables.
6
-
7
- use std::ffi::c_void;
8
- use std::sync::atomic::{AtomicU32, Ordering};
9
-
10
- use minhook::MinHook;
11
-
12
- use crate::{log_line, rb};
13
- use openwa_types::address::va;
14
-
15
- /// Install an inline hook at a Ghidra address, redirecting to `detour_fn`.
16
- /// Returns the trampoline pointer for calling the original.
17
- unsafe fn install_hook(ghidra_addr: u32, detour_fn: *const (), name: &str) -> Result<u32, String> {
18
- let target = rb(ghidra_addr) as *mut c_void;
19
- let detour = detour_fn as *mut c_void;
20
-
21
- let trampoline = MinHook::create_hook(target, detour)
22
- .map_err(|e| format!("MinHook create_hook failed for {name}: {e}"))?;
23
-
24
- MinHook::enable_hook(target)
25
- .map_err(|e| format!("MinHook enable_hook failed for {name}: {e}"))?;
26
-
27
- let trampoline_addr = trampoline as u32;
28
- let _ = log_line(&format!(
29
- " [MH] {name}: target 0x{:08X} (ghidra 0x{ghidra_addr:08X}), trampoline 0x{trampoline_addr:08X}",
30
- target as u32
31
- ));
32
-
33
- Ok(trampoline_addr)
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // CTask::Constructor hook
38
- // ---------------------------------------------------------------------------
39
-
40
- static ORIG_CTASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
41
-
42
- /// Hook for CTask::Constructor (0x5625A0).
43
- /// stdcall(this, parent, ddgame) -> this
44
- /// 3rd param is the DDGame pointer, stored at this+0x2C.
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
- // ---------------------------------------------------------------------------
52
- // CGameTask::Constructor hook
53
- // ---------------------------------------------------------------------------
54
-
55
- static ORIG_CGAMETASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
56
-
57
- /// Hook for CGameTask::Constructor (0x4FED50).
58
- /// RET 0x10 → stdcall with 4 params: (this, parent, param3, param4)
59
- /// param3/param4 are stored at this+0x30/this+0x38 (likely initial position)
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
- // ---------------------------------------------------------------------------
67
- // Installation
68
- // ---------------------------------------------------------------------------
69
-
70
- pub fn install() -> Result<(), String> {
71
- unsafe {
72
- // CTask::Constructor — base class constructor
73
- let trampoline = install_hook(
74
- va::CTASK_CONSTRUCTOR,
75
- hook_ctask_constructor as *const (),
76
- "CTask::Constructor",
77
- )?;
78
- ORIG_CTASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
79
-
80
- // CGameTask::Constructor — game entity constructor
81
- let trampoline = install_hook(
82
- va::CGAMETASK_CONSTRUCTOR,
83
- hook_cgametask_constructor as *const (),
84
- "CGameTask::Constructor",
85
- )?;
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-validator/src/hooks/mod.rs DELETED
@@ -1,19 +0,0 @@
1
- pub mod vtable;
2
- pub mod detour;
3
-
4
- use crate::log_line;
5
-
6
- /// Install all hooks. Called from run_validation() after rebase init.
7
- pub fn install_all() -> Result<(), String> {
8
- let _ = log_line("");
9
- let _ = log_line("--- Installing Hooks ---");
10
-
11
- // VTable hooks (pure Rust, no dependencies)
12
- vtable::install()?;
13
-
14
- // Inline hooks via MinHook (for constructors and free functions)
15
- detour::install()?;
16
-
17
- let _ = log_line(" All hooks installed.");
18
- Ok(())
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-validator/src/hooks/vtable.rs DELETED
@@ -1,129 +0,0 @@
1
- //! VTable hooking — swap function pointers in vtable memory.
2
- //!
3
- //! This is the simplest hooking approach: write a new function pointer
4
- //! into a vtable slot and save the original. No trampoline, no prologue
5
- //! analysis, no external dependencies.
6
-
7
- use std::sync::atomic::{AtomicU32, Ordering};
8
-
9
- use crate::{log_line, rb};
10
- use openwa_types::address::va;
11
-
12
- #[cfg(target_os = "windows")]
13
- use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_READWRITE};
14
-
15
- /// A single vtable hook — remembers the slot address and original pointer.
16
- pub struct VtableHook {
17
- #[allow(dead_code)]
18
- slot_addr: *mut u32,
19
- original_fn: u32,
20
- #[allow(dead_code)]
21
- name: &'static str,
22
- }
23
-
24
- // VtableHook contains a raw pointer but is only used from the main thread
25
- unsafe impl Send for VtableHook {}
26
- unsafe impl Sync for VtableHook {}
27
-
28
- impl VtableHook {
29
- /// Install a vtable hook at `vtable_ghidra_addr + slot_index * 4`.
30
- ///
31
- /// # Safety
32
- /// - `vtable_ghidra_addr` must be a valid vtable base in .rdata
33
- /// - `hook_fn` must match the calling convention of the original method
34
- #[cfg(target_os = "windows")]
35
- pub unsafe fn install(
36
- vtable_ghidra_addr: u32,
37
- slot_index: usize,
38
- hook_fn: u32,
39
- name: &'static str,
40
- ) -> Result<Self, String> {
41
- let slot_addr = rb(vtable_ghidra_addr + (slot_index as u32) * 4) as *mut u32;
42
-
43
- // Make the vtable slot writable
44
- let mut old_protect: u32 = 0;
45
- let ok = VirtualProtect(slot_addr as *mut _, 4, PAGE_READWRITE, &mut old_protect);
46
- if ok == 0 {
47
- return Err(format!("VirtualProtect failed for {name} at 0x{:08X}", slot_addr as u32));
48
- }
49
-
50
- // Read original and write hook
51
- let original_fn = *slot_addr;
52
- *slot_addr = hook_fn;
53
-
54
- // Restore protection
55
- VirtualProtect(slot_addr as *mut _, 4, old_protect, &mut old_protect);
56
-
57
- let _ = log_line(&format!(
58
- " [VT] {name}: slot 0x{:08X}, orig 0x{original_fn:08X} -> hook 0x{hook_fn:08X}",
59
- slot_addr as u32
60
- ));
61
-
62
- Ok(VtableHook { slot_addr, original_fn, name })
63
- }
64
-
65
- /// Get the original function pointer (for calling the original from the hook).
66
- pub fn original(&self) -> u32 {
67
- self.original_fn
68
- }
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // Installed hooks — stored globally so originals can be called
73
- // ---------------------------------------------------------------------------
74
-
75
- /// Original CTask::ProcessFrame function pointer (thiscall: ECX = this)
76
- static ORIG_CTASK_PROCESS_FRAME: AtomicU32 = AtomicU32::new(0);
77
-
78
- /// Hook for CTask::ProcessFrame (vtable slot 7).
79
- /// thiscall(this, flags) — RET 0x4 confirms 1 stack param.
80
- /// flags bit 0: if set, frees `this` (scalar deleting destructor pattern).
81
- unsafe extern "fastcall" fn hook_ctask_process_frame(this: u32, _edx: u32, flags: u32) -> u32 {
82
- let orig: unsafe extern "fastcall" fn(u32, u32, u32) -> u32 =
83
- core::mem::transmute(ORIG_CTASK_PROCESS_FRAME.load(Ordering::Relaxed));
84
- orig(this, 0, flags)
85
- }
86
-
87
- /// Original CTask::HandleMessage function pointer
88
- static ORIG_CTASK_HANDLE_MESSAGE: AtomicU32 = AtomicU32::new(0);
89
-
90
- /// Hook for CTask::HandleMessage (vtable slot 2).
91
- /// thiscall(this, sender, msg_type, size, data) — RET 0x10 confirms 4 stack params.
92
- /// Iterates children list and forwards message to each child's HandleMessage.
93
- unsafe extern "fastcall" fn hook_ctask_handle_message(
94
- this: u32, _edx: u32, sender: u32, msg_type: u32, size: u32, data: u32,
95
- ) -> u32 {
96
- let orig: unsafe extern "fastcall" fn(u32, u32, u32, u32, u32, u32) -> u32 =
97
- core::mem::transmute(ORIG_CTASK_HANDLE_MESSAGE.load(Ordering::Relaxed));
98
- orig(this, 0, sender, msg_type, size, data)
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Installation
103
- // ---------------------------------------------------------------------------
104
-
105
- pub fn install() -> Result<(), String> {
106
- unsafe {
107
- // CTask::ProcessFrame — slot 7
108
- let hook = VtableHook::install(
109
- va::CTASK_VTABLE,
110
- 7,
111
- hook_ctask_process_frame as *const () as u32,
112
- "CTask::ProcessFrame",
113
- )?;
114
- ORIG_CTASK_PROCESS_FRAME.store(hook.original(), Ordering::Relaxed);
115
-
116
- // CTask::HandleMessage — slot 2
117
- let hook = VtableHook::install(
118
- va::CTASK_VTABLE,
119
- 2,
120
- hook_ctask_handle_message as *const () as u32,
121
- "CTask::HandleMessage",
122
- )?;
123
- ORIG_CTASK_HANDLE_MESSAGE.store(hook.original(), Ordering::Relaxed);
124
-
125
- let _ = log_line(" 2 vtable hooks installed");
126
- }
127
-
128
- Ok(())
129
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-validator/src/lib.rs DELETED
@@ -1,883 +0,0 @@
1
- #![allow(non_snake_case)]
2
-
3
- use std::ffi::c_void;
4
- use std::sync::atomic::{AtomicU32, Ordering};
5
-
6
- mod hooks;
7
-
8
- use openwa_types::address::va;
9
- use openwa_types::task::{CTask, CGameTask};
10
- use openwa_types::ddgame::DDGame;
11
- use openwa_types::ddgame_wrapper::DDGameWrapper;
12
-
13
- use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
14
- use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
15
-
16
- // ---------------------------------------------------------------------------
17
- // ASLR rebasing
18
- // ---------------------------------------------------------------------------
19
-
20
- /// Delta to add to Ghidra addresses to get runtime addresses.
21
- /// Computed as: actual_base - IMAGE_BASE (0x400000)
22
- static REBASE_DELTA: AtomicU32 = AtomicU32::new(0);
23
- static DUMP_COUNTER: AtomicU32 = AtomicU32::new(0);
24
-
25
- /// Rebase a Ghidra VA to the actual runtime address.
26
- #[inline]
27
- pub(crate) fn rb(ghidra_addr: u32) -> u32 {
28
- ghidra_addr.wrapping_add(REBASE_DELTA.load(Ordering::Relaxed))
29
- }
30
-
31
- fn init_rebase() -> i32 {
32
- let base = unsafe { GetModuleHandleA(std::ptr::null()) } as u32;
33
- let delta = base.wrapping_sub(va::IMAGE_BASE);
34
- REBASE_DELTA.store(delta, Ordering::Relaxed);
35
- delta as i32
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // DllMain
40
- // ---------------------------------------------------------------------------
41
-
42
- const DLL_PROCESS_ATTACH: u32 = 1;
43
-
44
- #[no_mangle]
45
- unsafe extern "system" fn DllMain(
46
- _module: *mut c_void,
47
- reason: u32,
48
- _reserved: *mut c_void,
49
- ) -> i32 {
50
- if reason == DLL_PROCESS_ATTACH {
51
- std::thread::spawn(|| {
52
- if let Err(e) = run_validation() {
53
- let _ = log_line(&format!("[FATAL] Validation failed to run: {}", e));
54
- }
55
- });
56
- }
57
- 1 // TRUE
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Logging
62
- // ---------------------------------------------------------------------------
63
-
64
- pub(crate) fn log_line(msg: &str) -> std::io::Result<()> {
65
- use std::io::Write;
66
- let mut f = std::fs::OpenOptions::new()
67
- .create(true)
68
- .append(true)
69
- .open("OpenWA_validation.log")?;
70
- writeln!(f, "{}", msg)?;
71
- Ok(())
72
- }
73
-
74
- fn clear_log() -> std::io::Result<()> {
75
- std::fs::write("OpenWA_validation.log", "")?;
76
- Ok(())
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // ValidationResult
81
- // ---------------------------------------------------------------------------
82
-
83
- struct ValidationResult {
84
- pass: u32,
85
- fail: u32,
86
- }
87
-
88
- impl ValidationResult {
89
- fn new() -> Self {
90
- Self { pass: 0, fail: 0 }
91
- }
92
-
93
- fn check(&mut self, name: &str, ok: bool, detail: &str) {
94
- if ok {
95
- self.pass += 1;
96
- let _ = log_line(&format!("[PASS] {} - {}", name, detail));
97
- } else {
98
- self.fail += 1;
99
- let _ = log_line(&format!("[FAIL] {} - {}", name, detail));
100
- }
101
- }
102
-
103
- fn total(&self) -> u32 {
104
- self.pass + self.fail
105
- }
106
-
107
- fn summary_line(&self) -> String {
108
- format!(
109
- "Results: {}/{} passed, {} failed",
110
- self.pass,
111
- self.total(),
112
- self.fail
113
- )
114
- }
115
- }
116
-
117
- // ---------------------------------------------------------------------------
118
- // Memory helpers
119
- // ---------------------------------------------------------------------------
120
-
121
- #[inline]
122
- unsafe fn read_u32(addr: u32) -> u32 {
123
- *(addr as *const u32)
124
- }
125
-
126
- #[inline]
127
- unsafe fn read_u8(addr: u32) -> u8 {
128
- *(addr as *const u8)
129
- }
130
-
131
- #[inline]
132
- fn is_in_text(addr: u32) -> bool {
133
- addr >= rb(va::TEXT_START) && addr <= rb(va::TEXT_END)
134
- }
135
-
136
- #[inline]
137
- fn is_in_rdata(addr: u32) -> bool {
138
- addr >= rb(va::RDATA_START) && addr < rb(va::DATA_START)
139
- }
140
-
141
- // ---------------------------------------------------------------------------
142
- // Task 3: Address validation
143
- // ---------------------------------------------------------------------------
144
-
145
- fn validate_addresses(result: &mut ValidationResult) {
146
- let _ = log_line("");
147
- let _ = log_line("--- Address Validation ---");
148
-
149
- // Vtable addresses (Ghidra VAs — will be rebased via rb())
150
- let vtables: &[(&str, u32)] = &[
151
- ("CTask vtable", va::CTASK_VTABLE),
152
- ("CGameTask vtable", va::CGAMETASK_VTABLE),
153
- ("CGameTask vtable2", va::CGAMETASK_VTABLE2),
154
- ("DDGameWrapper vtable", va::DDGAME_WRAPPER_VTABLE),
155
- ("GfxHandler vtable", va::GFX_HANDLER_VTABLE),
156
- ("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE),
157
- ("PCLandscape vtable", va::PC_LANDSCAPE_VTABLE),
158
- ("LandscapeShader vtable", va::LANDSCAPE_SHADER_VTABLE),
159
- ("DSSound vtable", va::DS_SOUND_VTABLE),
160
- ("TaskStateMachine vtable", va::TASK_STATE_MACHINE_VTABLE),
161
- ("OpenGLCPU vtable", va::OPENGL_CPU_VTABLE),
162
- ("WaterEffect vtable", va::WATER_EFFECT_VTABLE),
163
- ];
164
-
165
- // 1. Vtable location checks
166
- let _ = log_line("");
167
- let _ = log_line(" Vtable location checks (.rdata range):");
168
- for (name, ghidra_addr) in vtables {
169
- let addr = rb(*ghidra_addr);
170
- let in_rdata = is_in_rdata(addr);
171
- result.check(
172
- &format!("{} location", name),
173
- in_rdata,
174
- &format!("0x{:08X} (ghidra 0x{:08X}) {}", addr, ghidra_addr, if in_rdata { "in .rdata" } else { "NOT in .rdata" }),
175
- );
176
- }
177
-
178
- // 2. Vtable content checks: first entry should be a .text pointer
179
- let _ = log_line("");
180
- let _ = log_line(" Vtable first-entry checks (should point to .text):");
181
- for (name, ghidra_addr) in vtables {
182
- let addr = rb(*ghidra_addr);
183
- unsafe {
184
- let first_entry = read_u32(addr);
185
- let in_text = is_in_text(first_entry);
186
- result.check(
187
- &format!("{} first entry", name),
188
- in_text,
189
- &format!(
190
- "[0x{:08X}] = 0x{:08X} {}",
191
- addr,
192
- first_entry,
193
- if in_text { "in .text" } else { "NOT in .text" }
194
- ),
195
- );
196
- }
197
- }
198
-
199
- // 3. CTask vtable method verification (compare rebased expected vs actual)
200
- let _ = log_line("");
201
- let _ = log_line(" CTask vtable method verification:");
202
- let ctask_vt_methods: &[(&str, u32, u32)] = &[
203
- ("vt0 init", 0, va::CTASK_VT0_INIT),
204
- ("vt1 Free", 4, va::CTASK_VT1_FREE),
205
- ("vt2 HandleMessage", 8, va::CTASK_VT2_HANDLE_MESSAGE),
206
- ("vt3", 12, va::CTASK_VT3),
207
- ("vt4", 16, va::CTASK_VT4),
208
- ("vt5", 20, va::CTASK_VT5),
209
- ("vt6", 24, va::CTASK_VT6),
210
- ("vt7 ProcessFrame", 28, va::CTASK_VT7_PROCESS_FRAME),
211
- ];
212
- for (name, offset, expected_ghidra) in ctask_vt_methods {
213
- unsafe {
214
- let actual = read_u32(rb(va::CTASK_VTABLE) + offset);
215
- let expected = rb(*expected_ghidra);
216
- let ok = actual == expected;
217
- result.check(
218
- &format!("CTask::{}", name),
219
- ok,
220
- &format!(
221
- "vtable+0x{:02X}: expected 0x{:08X}, got 0x{:08X}",
222
- offset, expected, actual
223
- ),
224
- );
225
- }
226
- }
227
-
228
- // 4. Function prologue checks
229
- let _ = log_line("");
230
- let _ = log_line(" Function prologue checks:");
231
- let valid_prologues: &[u8] = &[
232
- 0x55, // push ebp
233
- 0x53, // push ebx
234
- 0x56, // push esi
235
- 0x57, // push edi
236
- 0x83, // sub esp, ...
237
- 0x8B, // mov reg, ...
238
- 0x6A, // push imm8
239
- 0x81, // sub esp, imm32
240
- 0xB8, // mov eax, imm32
241
- 0x51, // push ecx
242
- 0x52, // push edx
243
- 0x64, // FS: segment prefix (SEH/stack cookies)
244
- 0x85, // test
245
- 0x8D, // lea
246
- ];
247
-
248
- let functions: &[(&str, u32)] = &[
249
- ("CTask::Constructor", va::CTASK_CONSTRUCTOR),
250
- ("CGameTask::Constructor", va::CGAMETASK_CONSTRUCTOR),
251
- ("DDGameWrapper::Constructor", va::CONSTRUCT_DD_GAME_WRAPPER),
252
- ("DDGame::Constructor", va::CONSTRUCT_DD_GAME),
253
- ("CreateExplosion", va::CREATE_EXPLOSION),
254
- ("SpawnObject", va::SPAWN_OBJECT),
255
- ("WeaponRelease", va::WEAPON_RELEASE),
256
- ("FireWeapon", va::FIRE_WEAPON),
257
- ("InitWeaponTable", va::INIT_WEAPON_TABLE),
258
- ("BlitScreen", va::BLIT_SCREEN),
259
- ("RenderDrawingQueue", va::RENDER_DRAWING_QUEUE),
260
- ("DrawLandscape", va::DRAW_LANDSCAPE),
261
- ("ConstructPCLandscape", va::CONSTRUCT_PC_LANDSCAPE),
262
- ("ConstructDSSound", va::CONSTRUCT_DS_SOUND),
263
- ("ShowChatMessage", va::SHOW_CHAT_MESSAGE),
264
- ("FrontendChangeScreen", va::FRONTEND_CHANGE_SCREEN),
265
- ("WA_MallocMemset", va::WA_MALLOC_MEMSET),
266
- ("CTask::vt0_init", va::CTASK_VT0_INIT),
267
- ("CTask::vt1_Free", va::CTASK_VT1_FREE),
268
- ("CTask::vt2_HandleMessage", va::CTASK_VT2_HANDLE_MESSAGE),
269
- ("CGameTask::vt0", va::CGAMETASK_VT0),
270
- ("CGameTask::vt1_Free", va::CGAMETASK_VT1_FREE),
271
- ("CGameTask::vt2_HandleMessage", va::CGAMETASK_VT2_HANDLE_MESSAGE),
272
- ];
273
-
274
- for (name, ghidra_addr) in functions {
275
- let addr = rb(*ghidra_addr);
276
- let in_text = is_in_text(addr);
277
- if !in_text {
278
- result.check(
279
- &format!("{} prologue", name),
280
- false,
281
- &format!("0x{:08X} (ghidra 0x{:08X}) not in .text range", addr, ghidra_addr),
282
- );
283
- continue;
284
- }
285
- unsafe {
286
- let first_byte = read_u8(addr);
287
- let ok = valid_prologues.contains(&first_byte);
288
- result.check(
289
- &format!("{} prologue", name),
290
- ok,
291
- &format!(
292
- "0x{:08X}: first byte 0x{:02X} {}",
293
- addr,
294
- first_byte,
295
- if ok { "valid" } else { "UNEXPECTED" }
296
- ),
297
- );
298
- }
299
- }
300
- }
301
-
302
- // ---------------------------------------------------------------------------
303
- // Task 6: offset_of! validation
304
- // ---------------------------------------------------------------------------
305
-
306
- fn validate_struct_offsets(result: &mut ValidationResult) {
307
- let _ = log_line("");
308
- let _ = log_line("--- Struct Offset Validation (offset_of!) ---");
309
-
310
- // Helper macro to reduce boilerplate
311
- macro_rules! check_offset {
312
- ($result:expr, $struct:ty, $field:ident, $expected:expr) => {
313
- let actual = core::mem::offset_of!($struct, $field);
314
- $result.check(
315
- &format!("{}::{}", stringify!($struct), stringify!($field)),
316
- actual == $expected,
317
- &format!("expected 0x{:X}, got 0x{:X}", $expected, actual),
318
- );
319
- };
320
- }
321
-
322
- // CTask offsets
323
- let _ = log_line("");
324
- let _ = log_line(" CTask:");
325
- check_offset!(result, CTask, vtable, 0x00);
326
- check_offset!(result, CTask, parent, 0x04);
327
- check_offset!(result, CTask, children_max_size, 0x08);
328
- check_offset!(result, CTask, children_data, 0x14);
329
- check_offset!(result, CTask, class_type, 0x20);
330
- check_offset!(result, CTask, shared_data, 0x24);
331
- check_offset!(result, CTask, owns_shared_data, 0x28);
332
- check_offset!(result, CTask, ddgame, 0x2C);
333
-
334
- // CGameTask offsets
335
- let _ = log_line("");
336
- let _ = log_line(" CGameTask:");
337
- check_offset!(result, CGameTask, base, 0x00);
338
- check_offset!(result, CGameTask, pos_x, 0x84);
339
- check_offset!(result, CGameTask, pos_y, 0x88);
340
- check_offset!(result, CGameTask, speed_x, 0x90);
341
- check_offset!(result, CGameTask, speed_y, 0x94);
342
- check_offset!(result, CGameTask, vtable2, 0xE8);
343
-
344
- // DDGame offsets
345
- let _ = log_line("");
346
- let _ = log_line(" DDGame:");
347
- check_offset!(result, DDGame, landscape, 0x20);
348
- check_offset!(result, DDGame, game_state, 0x24);
349
- check_offset!(result, DDGame, arrow_sprites, 0x38);
350
- check_offset!(result, DDGame, arrow_gfxdirs, 0xB8);
351
- check_offset!(result, DDGame, display_gfx, 0x138);
352
- check_offset!(result, DDGame, task_state_machine, 0x380);
353
- check_offset!(result, DDGame, sprite_regions, 0x46C);
354
- check_offset!(result, DDGame, coord_list, 0x50C);
355
-
356
- // DDGameWrapper offsets
357
- let _ = log_line("");
358
- let _ = log_line(" DDGameWrapper:");
359
- check_offset!(result, DDGameWrapper, vtable, 0x00);
360
- check_offset!(result, DDGameWrapper, ddgame, 0x488);
361
- check_offset!(result, DDGameWrapper, _field_4c0, 0x4C0);
362
- check_offset!(result, DDGameWrapper, landscape, 0x4CC);
363
- check_offset!(result, DDGameWrapper, display, 0x4D0);
364
- }
365
-
366
- // ---------------------------------------------------------------------------
367
- // Deferred validation via global pointer polling
368
- // ---------------------------------------------------------------------------
369
- // Constructor hooks failed due to retour trampoline issues with SEH prologues.
370
- // Instead, we read the DDGameWrapper pointer from the global game session
371
- // after waiting for initialization to complete.
372
-
373
- fn deferred_global_validation() {
374
- let _ = log_line("");
375
- let _ = log_line("--- Deferred Global Validation (10s after load) ---");
376
-
377
- let mut result = ValidationResult::new();
378
-
379
- unsafe {
380
- // g_GameSession is a pointer to the game session context
381
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
382
- let _ = log_line(&format!(" g_GameSession = 0x{:08X}", session_ptr));
383
-
384
- if session_ptr == 0 {
385
- let _ = log_line(" Game session not initialized yet — no game started?");
386
- return;
387
- }
388
-
389
- // DDGameWrapper at session+0xA0
390
- let wrapper_addr = read_u32(session_ptr + 0xA0);
391
- let _ = log_line(&format!(" DDGameWrapper = 0x{:08X}", wrapper_addr));
392
-
393
- if wrapper_addr == 0 {
394
- let _ = log_line(" DDGameWrapper not created — need to start a game first.");
395
- return;
396
- }
397
-
398
- // vtable at +0x00 should be DDGAME_WRAPPER_VTABLE
399
- let vtable_ptr = read_u32(wrapper_addr);
400
- let expected_vt = rb(va::DDGAME_WRAPPER_VTABLE);
401
- result.check(
402
- "DDGameWrapper.vtable",
403
- vtable_ptr == expected_vt,
404
- &format!("expected 0x{:08X}, got 0x{:08X}", expected_vt, vtable_ptr),
405
- );
406
-
407
- // ddgame pointer at +0x488 should be non-null
408
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
409
- let ddgame_valid = ddgame_ptr != 0;
410
- result.check(
411
- "DDGameWrapper.ddgame != NULL",
412
- ddgame_valid,
413
- &format!("0x{:08X}", ddgame_ptr),
414
- );
415
-
416
- if ddgame_valid {
417
- // DDGame.landscape at +0x20 should match wrapper.landscape at +0x4CC
418
- let ddgame_landscape = read_u32(ddgame_ptr + 0x20);
419
- let wrapper_landscape = read_u32(wrapper_addr + 0x4CC);
420
- result.check(
421
- "DDGame.landscape == wrapper.landscape",
422
- ddgame_landscape == wrapper_landscape,
423
- &format!(
424
- "DDGame+0x20=0x{:08X}, wrapper+0x4CC=0x{:08X}",
425
- ddgame_landscape, wrapper_landscape
426
- ),
427
- );
428
-
429
- // Subsystem vtable checks
430
- let display_gfx = read_u32(ddgame_ptr + 0x138);
431
- if display_gfx != 0 {
432
- let dgfx_vt = read_u32(display_gfx);
433
- let expected = rb(va::DISPLAY_GFX_VTABLE);
434
- result.check(
435
- "DDGame.display_gfx vtable",
436
- dgfx_vt == expected,
437
- &format!("expected 0x{:08X}, got 0x{:08X}", expected, dgfx_vt),
438
- );
439
- } else {
440
- result.check("DDGame.display_gfx", false, "NULL pointer");
441
- }
442
-
443
- let tsm = read_u32(ddgame_ptr + 0x380);
444
- if tsm != 0 {
445
- let tsm_vt = read_u32(tsm);
446
- let expected = rb(va::TASK_STATE_MACHINE_VTABLE);
447
- result.check(
448
- "DDGame.task_state_machine vtable",
449
- tsm_vt == expected,
450
- &format!("expected 0x{:08X}, got 0x{:08X}", expected, tsm_vt),
451
- );
452
- } else {
453
- result.check("DDGame.task_state_machine", false, "NULL pointer");
454
- }
455
- }
456
-
457
- // landscape at wrapper+0x4CC
458
- let landscape = read_u32(wrapper_addr + 0x4CC);
459
- if landscape != 0 {
460
- let land_vt = read_u32(landscape);
461
- let expected = rb(va::PC_LANDSCAPE_VTABLE);
462
- result.check(
463
- "wrapper.landscape vtable",
464
- land_vt == expected,
465
- &format!("expected 0x{:08X}, got 0x{:08X}", expected, land_vt),
466
- );
467
- } else {
468
- result.check("wrapper.landscape", false, "NULL pointer");
469
- }
470
- }
471
-
472
- let _ = log_line(&format!(" Deferred {}", result.summary_line()));
473
- }
474
-
475
- // ---------------------------------------------------------------------------
476
- // Team block memory dump (for WormEntry/FullTeamBlock verification)
477
- // ---------------------------------------------------------------------------
478
-
479
- fn dump_team_blocks() {
480
- use openwa_types::ddgame::{offsets, FullTeamBlock, TeamWeaponState};
481
-
482
- let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
483
- let _ = log_line("");
484
- let _ = log_line(&format!("--- Team Block Dump #{} ---", dump_num));
485
-
486
- unsafe {
487
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
488
- if session_ptr == 0 {
489
- let _ = log_line(" No game session — skipping.");
490
- return;
491
- }
492
-
493
- let wrapper_addr = read_u32(session_ptr + 0xA0);
494
- if wrapper_addr == 0 {
495
- let _ = log_line(" No DDGameWrapper — need to start a game first.");
496
- return;
497
- }
498
-
499
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
500
- if ddgame_ptr == 0 {
501
- let _ = log_line(" No DDGame — need to be in gameplay.");
502
- return;
503
- }
504
-
505
- let _ = log_line(&format!(" DDGame = 0x{:08X}", ddgame_ptr));
506
-
507
- // Get TeamWeaponState via struct
508
- let tws_base = (ddgame_ptr + offsets::TEAM_WEAPON_STATE as u32) as *const u8;
509
- let tws = &*(tws_base as *const TeamWeaponState);
510
- let _ = log_line(&format!(" team_count = {} (TeamWeaponState.team_count)", tws.team_count));
511
-
512
- // Get FullTeamBlock array via TWS_TO_BLOCKS offset
513
- let blocks = tws_base.sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
514
- let blocks_addr = blocks as u32;
515
- let _ = log_line(&format!(" blocks_base = 0x{:08X} (DDGame+0x{:X})",
516
- blocks_addr, blocks_addr - ddgame_ptr));
517
-
518
- let mut result = ValidationResult::new();
519
-
520
- // Validate TWS_TO_BLOCKS: blocks_base should == DDGame + TEAM_BLOCKS
521
- let expected_blocks = ddgame_ptr + offsets::TEAM_BLOCKS as u32;
522
- result.check("TWS_TO_BLOCKS derivation",
523
- blocks_addr == expected_blocks,
524
- &format!("got 0x{:08X}, expected 0x{:08X}", blocks_addr, expected_blocks));
525
-
526
- // Validate each real team (1-indexed: blocks[1..=team_count])
527
- let num_blocks = (tws.team_count as u32 + 1).max(3).min(7);
528
- for b in 0..num_blocks {
529
- let block = &*blocks.add(b as usize);
530
- let _ = log_line(&format!("\n === Block {} (0x{:08X}) ===",
531
- b, blocks_addr + b * 0x51C));
532
-
533
- // Sentinel: block[b+1].worms[0] holds metadata for entry_ptr(b)
534
- if (b + 1) < 7 {
535
- let sentinel = &(*blocks.add(b as usize + 1)).worms[0];
536
- let worm_count = sentinel.sentinel_worm_count();
537
- let eliminated = sentinel.sentinel_eliminated();
538
-
539
- // Cross-check sentinel vs raw entry_ptr reads
540
- let entry_ptr = tws_base.add(b as usize * 0x51C);
541
- let raw_worm_count = *(entry_ptr.sub(4) as *const i32);
542
- let raw_alliance = *(entry_ptr.add(4) as *const i32);
543
-
544
- result.check(
545
- &format!("block[{}] sentinel_worm_count vs raw", b),
546
- worm_count == raw_worm_count,
547
- &format!("struct={}, raw={}", worm_count, raw_worm_count),
548
- );
549
-
550
- let _ = log_line(&format!(" sentinel: worm_count={}, eliminated={}, alliance(entry_ptr+4)={}",
551
- worm_count, eliminated, raw_alliance));
552
-
553
- // Dump playable worms using struct field access
554
- for w in 0..8usize {
555
- let worm = &block.worms[w];
556
- let active = worm.active_flag;
557
- let name_bytes = &worm.name;
558
- let name_len = name_bytes.iter().position(|&c| c == 0).unwrap_or(name_bytes.len());
559
- let name_str = core::str::from_utf8(&name_bytes[..name_len]).unwrap_or("?");
560
-
561
- if worm.state != 0 || worm.health != 0 || active != 0 || w == 0 {
562
- let _ = log_line(&format!(
563
- " worm[{}]: state=0x{:04X} active={} max_hp={} hp={} name=\"{}\"",
564
- w, worm.state, active, worm.max_health, worm.health, name_str
565
- ));
566
- }
567
- }
568
-
569
- // Validate GetTeamTotalHealth pattern: sum block.worms[1..=count].health
570
- if worm_count > 0 && worm_count <= 7 {
571
- let struct_total: i32 = (1..=worm_count as usize)
572
- .map(|w| block.worms[w].health)
573
- .sum();
574
-
575
- // Cross-check vs raw pointer method (old ARRAY_OFFSET=0x4A0 pattern)
576
- let raw_health_ptr = entry_ptr.sub(0x4A0) as *const i32;
577
- let mut raw_total = 0i32;
578
- for i in 0..worm_count {
579
- raw_total += *raw_health_ptr.add(i as usize * (0x9C / 4));
580
- }
581
-
582
- result.check(
583
- &format!("block[{}] health sum (struct vs raw)", b),
584
- struct_total == raw_total,
585
- &format!("struct={}, raw={}", struct_total, raw_total),
586
- );
587
- }
588
-
589
- // Validate IsWormInSpecialState pattern: block.worms[w].state
590
- // Cross-check worm[1].state via struct vs raw (STATE_OFFSET=0x598)
591
- if worm_count > 0 {
592
- let struct_state = block.worms[1].state;
593
- let raw_state = *(entry_ptr.sub(0x598).add(0x9C) as *const u32);
594
- result.check(
595
- &format!("block[{}] worm[1].state (struct vs raw)", b),
596
- struct_state == raw_state,
597
- &format!("struct=0x{:X}, raw=0x{:X}", struct_state, raw_state),
598
- );
599
- }
600
-
601
- // Validate CheckWormState0x64 pattern: reads worms[].state, NOT .health
602
- // Show both values for each worm so we can verify
603
- if worm_count > 0 {
604
- let _ = log_line(&format!(" CheckWormState0x64 field check (state vs health):"));
605
- for w in 1..=worm_count as usize {
606
- let worm = &block.worms[w];
607
- let _ = log_line(&format!(
608
- " worm[{}]: state=0x{:04X}({}) health=0x{:04X}({})",
609
- w, worm.state, worm.state, worm.health, worm.health
610
- ));
611
- }
612
- }
613
- }
614
- }
615
-
616
- let _ = log_line(&format!("\n Struct Validation {}", result.summary_line()));
617
- }
618
- }
619
-
620
- // ---------------------------------------------------------------------------
621
- // PCLandscape dump (for landscape struct verification)
622
- // ---------------------------------------------------------------------------
623
-
624
- fn dump_landscape() {
625
- use openwa_types::landscape::PCLandscape;
626
-
627
- let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
628
- let _ = log_line("");
629
- let _ = log_line(&format!("--- PCLandscape Dump #{} ---", dump_num));
630
-
631
- unsafe {
632
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
633
- if session_ptr == 0 {
634
- let _ = log_line(" No game session — skipping.");
635
- return;
636
- }
637
-
638
- let wrapper_addr = read_u32(session_ptr + 0xA0);
639
- if wrapper_addr == 0 {
640
- let _ = log_line(" No DDGameWrapper — need to start a game first.");
641
- return;
642
- }
643
-
644
- let landscape_ptr = read_u32(wrapper_addr + 0x4CC);
645
- if landscape_ptr == 0 {
646
- let _ = log_line(" PCLandscape is NULL — no level loaded.");
647
- return;
648
- }
649
-
650
- let land = &*(landscape_ptr as *const PCLandscape);
651
- let _ = log_line(&format!(" PCLandscape @ 0x{:08X}", landscape_ptr));
652
-
653
- // Vtable validation
654
- let expected_vt = rb(va::PC_LANDSCAPE_VTABLE);
655
- let vt_ok = land.vtable as u32 == expected_vt;
656
- let _ = log_line(&format!(" vtable: 0x{:08X} (expected 0x{:08X}) {}",
657
- land.vtable as u32, expected_vt, if vt_ok { "OK" } else { "MISMATCH" }));
658
-
659
- // DDGame pointer
660
- let _ = log_line(&format!(" ddgame: 0x{:08X}", land.ddgame as u32));
661
- let _ = log_line(&format!(" _unknown_900: 0x{:08X}", land._unknown_900 as u32));
662
-
663
- // Collision bitmap
664
- let _ = log_line(&format!(" collision_bitmap: 0x{:08X}", land.collision_bitmap as u32));
665
-
666
- // Initialized flag
667
- let _ = log_line(&format!(" initialized: {}", land.initialized));
668
-
669
- // Crater sprites — count non-null
670
- let primary_count = land.crater_sprites.iter().filter(|&&p| !p.is_null()).count();
671
- let secondary_count = land.crater_sprites_secondary.iter().filter(|&&p| !p.is_null()).count();
672
- let _ = log_line(&format!(" crater_sprites: {}/16 non-null, secondary: {}/16 non-null",
673
- primary_count, secondary_count));
674
-
675
- // Terrain layers
676
- let _ = log_line(&format!(" layer_0: 0x{:08X}", land.layer_0 as u32));
677
- let _ = log_line(&format!(" layer_1: 0x{:08X}", land.layer_1 as u32));
678
- let _ = log_line(&format!(" layer_terrain: 0x{:08X}", land.layer_terrain as u32));
679
- let _ = log_line(&format!(" layer_edges: 0x{:08X}", land.layer_edges as u32));
680
- let _ = log_line(&format!(" layer_shadow: 0x{:08X}", land.layer_shadow as u32));
681
- let _ = log_line(&format!(" layer_5: 0x{:08X}", land.layer_5 as u32));
682
-
683
- // If layer_terrain is valid, read DisplayGfx fields
684
- if !land.layer_terrain.is_null() {
685
- let dgfx = land.layer_terrain as u32;
686
- let pixel_data = read_u32(dgfx + 0x08);
687
- let stride = read_u32(dgfx + 0x10);
688
- let width = read_u32(dgfx + 0x14);
689
- let height = read_u32(dgfx + 0x18);
690
- let _ = log_line(&format!(" layer_terrain DisplayGfx: pixels=0x{:08X} stride={} width={} height={}",
691
- pixel_data, stride, width, height));
692
- }
693
-
694
- // Dirty rects
695
- let _ = log_line(&format!(" dirty_rect_count: {}", land.dirty_rect_count));
696
- let _ = log_line(&format!(" dirty_flag: {}", land.dirty_flag));
697
- if land.dirty_rect_count > 0 && land.dirty_rect_count <= 256 {
698
- for i in 0..land.dirty_rect_count.min(5) as usize {
699
- let r = &land.dirty_rects[i];
700
- let _ = log_line(&format!(" rect[{}]: ({},{})..({},{})",
701
- i, r.x1, r.y1, r.x2, r.y2));
702
- }
703
- if land.dirty_rect_count > 5 {
704
- let _ = log_line(&format!(" ... and {} more", land.dirty_rect_count - 5));
705
- }
706
- }
707
-
708
- // Unknown fields around palette area
709
- let _ = log_line(&format!(" _unknown_8ec: 0x{:08X}", land._unknown_8ec));
710
- let _ = log_line(&format!(" _unknown_8f0: 0x{:08X}", land._unknown_8f0));
711
- let _ = log_line(&format!(" _unknown_8f4: 0x{:08X}", land._unknown_8f4));
712
-
713
- // Resource handle
714
- let _ = log_line(&format!(" resource_handle: 0x{:08X}", land.resource_handle as u32));
715
-
716
- // GfxHandlers
717
- let _ = log_line(&format!(" level_gfx_handler: 0x{:08X}", land.level_gfx_handler as u32));
718
- let _ = log_line(&format!(" water_gfx_handler: 0x{:08X}", land.water_gfx_handler as u32));
719
-
720
- // Visible bounds
721
- let _ = log_line(&format!(" visible_bounds: left={} top={} right={} bottom={}",
722
- land.visible_left, land.visible_top, land.visible_right, land.visible_bottom));
723
-
724
- // Path buffers — read as C strings
725
- let level_path = std::ffi::CStr::from_ptr(land.level_dir_path.as_ptr() as *const i8);
726
- let theme_path = std::ffi::CStr::from_ptr(land.theme_dir_path.as_ptr() as *const i8);
727
- let _ = log_line(&format!(" level_dir_path: {:?}", level_path));
728
- let _ = log_line(&format!(" theme_dir_path: {:?}", theme_path));
729
-
730
- // DDGame level dimensions (from DDGame, not PCLandscape)
731
- let ddgame_ptr = land.ddgame as u32;
732
- if ddgame_ptr != 0 {
733
- let level_w = read_u32(ddgame_ptr + 0x77C0);
734
- let level_h = read_u32(ddgame_ptr + 0x77C4);
735
- let level_total = read_u32(ddgame_ptr + 0x77C8);
736
- let _ = log_line(&format!(" DDGame level dims: {}x{} (total={})",
737
- level_w, level_h, level_total));
738
- }
739
-
740
- // Vtable slot dump (first 10 entries)
741
- let _ = log_line(" Vtable slots:");
742
- let vt_addr = land.vtable as u32;
743
- for slot in 0..10u32 {
744
- let entry = read_u32(vt_addr + slot * 4);
745
- let in_text = is_in_text(entry);
746
- let _ = log_line(&format!(" [{}]: 0x{:08X} {}",
747
- slot, entry, if in_text { "" } else { "(NOT .text)" }));
748
- }
749
-
750
- let _ = log_line(&format!(" _unknown_b3c: 0x{:08X}", land._unknown_b3c));
751
-
752
- // Hex dump of unknown regions for discovery
753
- let _ = log_line(" _unknown_088 (0x44 bytes):");
754
- let _ = log_hex_dump(&land._unknown_088);
755
- let _ = log_line(" _unknown_8d9 (0x13 bytes):");
756
- let _ = log_hex_dump(&land._unknown_8d9);
757
- let _ = log_line(" _unknown_8f8 (8 bytes):");
758
- let _ = log_hex_dump(&land._unknown_8f8);
759
- let _ = log_line(" _unknown_918 (4 bytes):");
760
- let _ = log_hex_dump(&land._unknown_918);
761
- }
762
- }
763
-
764
- fn log_hex_dump(data: &[u8]) {
765
- for chunk in data.chunks(16) {
766
- let hex: String = chunk.iter().map(|b| format!("{:02X}", b)).collect::<Vec<_>>().join(" ");
767
- let ascii: String = chunk.iter().map(|&b| if (0x20..0x7F).contains(&b) { b as char } else { '.' }).collect();
768
- let _ = log_line(&format!(" {} | {}", hex, ascii));
769
- }
770
- }
771
-
772
- // ---------------------------------------------------------------------------
773
- // Main validation entry point
774
- // ---------------------------------------------------------------------------
775
-
776
- fn run_validation() -> Result<(), Box<dyn std::error::Error>> {
777
- // 1. Clear log file
778
- clear_log()?;
779
-
780
- // 2. Log header
781
- log_line("============================================")?;
782
- log_line(" OpenWA Runtime Validator")?;
783
- log_line(" Target: WA.exe 3.8.1 (Steam)")?;
784
- log_line("============================================")?;
785
-
786
- // 2b. Detect ASLR rebase
787
- let delta = init_rebase();
788
- let base = unsafe { GetModuleHandleA(std::ptr::null()) } as u32;
789
- log_line(&format!(
790
- " Module base: 0x{:08X} (Ghidra base: 0x{:08X}, delta: {}{:08X})",
791
- base,
792
- va::IMAGE_BASE,
793
- if delta >= 0 { "+" } else { "-" },
794
- if delta >= 0 { delta as u32 } else { (-(delta as i64)) as u32 }
795
- ))?;
796
-
797
- let mut result = ValidationResult::new();
798
-
799
- // 3. Address validation (vtables, functions)
800
- validate_addresses(&mut result);
801
-
802
- // 4. Struct offset validation (offset_of! checks)
803
- validate_struct_offsets(&mut result);
804
-
805
- // 5. Print intermediate summary
806
- let _ = log_line("");
807
- let _ = log_line("--- Static Validation Summary ---");
808
- let _ = log_line(&format!(" {}", result.summary_line()));
809
-
810
- // 6. Install hooks (vtable + inline)
811
- match hooks::install_all() {
812
- Ok(()) => {}
813
- Err(e) => { let _ = log_line(&format!("[ERROR] Hook installation failed: {}", e)); }
814
- }
815
-
816
- // 7. Mode-dependent: auto-capture vs interactive
817
- let auto_mode = std::env::var("OPENWA_REPLAY_TEST").is_ok();
818
-
819
- if auto_mode {
820
- let _ = log_line("");
821
- let _ = log_line("--- Auto-Capture Mode (OPENWA_REPLAY_TEST) ---");
822
- std::thread::spawn(move || {
823
- let _ = log_line(" Waiting 5s for replay to reach gameplay...");
824
- std::thread::sleep(std::time::Duration::from_secs(5));
825
-
826
- let _ = log_line(" Running deferred global validation...");
827
- deferred_global_validation();
828
-
829
- let _ = log_line(" Running team block dump...");
830
- dump_team_blocks();
831
-
832
- let _ = log_line(" Running landscape dump...");
833
- dump_landscape();
834
-
835
- let _ = log_line("");
836
- let _ = log_line("--- Auto-capture complete, exiting ---");
837
-
838
- unsafe {
839
- windows_sys::Win32::System::Threading::ExitProcess(0);
840
- }
841
- });
842
- } else {
843
- // Existing interactive mode: deferred timers + hotkey listener
844
- let _ = log_line("");
845
- let _ = log_line("--- Interactive Mode (deferred polling + hotkeys) ---");
846
- std::thread::spawn(move || {
847
- std::thread::sleep(std::time::Duration::from_secs(10));
848
- deferred_global_validation();
849
- });
850
- let _ = log_line(" Polling thread started (10s delay).");
851
-
852
- std::thread::spawn(move || {
853
- std::thread::sleep(std::time::Duration::from_secs(30));
854
- dump_team_blocks();
855
- });
856
- let _ = log_line(" Team block dump thread started (30s delay).");
857
-
858
- std::thread::spawn(move || {
859
- std::thread::sleep(std::time::Duration::from_secs(15));
860
- dump_landscape();
861
- });
862
- let _ = log_line(" Landscape dump thread started (15s delay).");
863
-
864
- std::thread::spawn(|| {
865
- const VK_F9: i32 = 0x78;
866
- const VK_F10: i32 = 0x79;
867
- let _ = log_line(" Hotkey listener started (F9=team blocks, F10=landscape).");
868
- loop {
869
- std::thread::sleep(std::time::Duration::from_millis(100));
870
- unsafe {
871
- if GetAsyncKeyState(VK_F9) & 1 != 0 {
872
- dump_team_blocks();
873
- }
874
- if GetAsyncKeyState(VK_F10) & 1 != 0 {
875
- dump_landscape();
876
- }
877
- }
878
- }
879
- });
880
- }
881
-
882
- Ok(())
883
- }