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

feat: unify validator into wormkit DLL, triggered via OPENWA_VALIDATE env var

Browse files

Merge validation logic from openwa-validator into openwa-wormkit as a
conditional module. Set OPENWA_VALIDATE=1 to enable validation checks,
OPENWA_REPLAY_TEST=1 for auto-capture mode. Eliminates the need for a
separate wkOpenWAValidator.dll.

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

.claude/skills/replay-test/SKILL.md CHANGED
@@ -16,8 +16,9 @@ powershell -ExecutionPolicy Bypass -File replay-test.ps1
16
  ```
17
 
18
  This will:
19
- - Build openwa-wormkit and openwa-validator
20
- - Deploy DLLs to the WA game directory
 
21
  - Launch WA.exe with the default replay file (testdata/replays/bots.WAgame)
22
  - Wait for auto-capture (5s) and process exit
23
  - Copy logs to testdata/logs/
@@ -32,8 +33,16 @@ Read `testdata/logs/validation_latest.log` and summarize:
32
 
33
  3. If the user also has `testdata/logs/openwa_latest.log`, read it and note any errors or interesting hook activity.
34
 
 
 
 
 
 
 
 
35
  ## Notes
36
 
37
  - The script takes ~10 seconds total (build + 5s auto-capture wait)
38
- - If WA.exe doesn't exit, the validator DLL may not have loaded. Check that wkOpenWAValidator.dll exists in the game directory.
 
39
  - Replay file can be changed by editing the script invocation: `powershell -File replay-test.ps1 path\to\other.WAgame`
 
16
  ```
17
 
18
  This will:
19
+ - Build the unified openwa-wormkit DLL (includes validation)
20
+ - Deploy wkOpenWA.dll to the WA game directory
21
+ - Set `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1` env vars
22
  - Launch WA.exe with the default replay file (testdata/replays/bots.WAgame)
23
  - Wait for auto-capture (5s) and process exit
24
  - Copy logs to testdata/logs/
 
33
 
34
  3. If the user also has `testdata/logs/openwa_latest.log`, read it and note any errors or interesting hook activity.
35
 
36
+ ## Environment Variables
37
+
38
+ - `OPENWA_VALIDATE=1` — Enables the validation module (struct checks, vtable validation, memory dumps). Without this, only the replacement hooks run.
39
+ - `OPENWA_REPLAY_TEST=1` — Enables auto-capture mode: waits 5s, runs all validation + dumps, then calls ExitProcess(0). Without this, validation runs in interactive mode with hotkeys (F9=team blocks, F10=landscape).
40
+
41
+ Both are set automatically by `replay-test.ps1`.
42
+
43
  ## Notes
44
 
45
  - The script takes ~10 seconds total (build + 5s auto-capture wait)
46
+ - There is only one DLL now: `wkOpenWA.dll` (unified wormkit + validator)
47
+ - The old `wkOpenWAValidator.dll` is automatically removed by the script
48
  - Replay file can be changed by editing the script invocation: `powershell -File replay-test.ps1 path\to\other.WAgame`
CLAUDE.md CHANGED
@@ -32,17 +32,15 @@ cargo test -p openwa-types -- scheme_parse::parse_beginner_v2
32
 
33
  Game directory: `I:\games\SteamLibrary\steamapps\common\Worms Armageddon`
34
 
35
- Copy release DLLs from `target/i686-pc-windows-msvc/release/` to the game directory:
36
  - `openwa_wormkit.dll` → rename to `wkOpenWA.dll`
37
- - `openwa_validator.dll` → rename to `wkOpenWAValidator.dll`
38
 
39
  Quick deploy:
40
  ```bash
41
  cp target/i686-pc-windows-msvc/release/openwa_wormkit.dll "I:/games/SteamLibrary/steamapps/common/Worms Armageddon/wkOpenWA.dll"
42
- cp target/i686-pc-windows-msvc/release/openwa_validator.dll "I:/games/SteamLibrary/steamapps/common/Worms Armageddon/wkOpenWAValidator.dll"
43
  ```
44
 
45
- WormKit auto-loads any `wk*.dll` from the game directory. Logs appear in the game directory as `OpenWA.log` and `OpenWA_validation.log`.
46
 
47
  ## Replay Testing
48
 
@@ -54,8 +52,8 @@ powershell -ExecutionPolicy Bypass -File replay-test.ps1
54
  ```
55
 
56
  How it works:
57
- 1. `replay-test.ps1` builds both DLLs, deploys to game dir, sets `OPENWA_REPLAY_TEST=1`, launches `WA.exe` minimized with a replay file
58
- 2. The validator DLL detects the env var and enters **auto-capture mode**: waits 5s, runs all validation + dumps, then calls `ExitProcess(0)`
59
  3. Script copies logs to `testdata/logs/` and prints a PASS/FAIL summary
60
 
61
  Key paths:
@@ -64,13 +62,14 @@ Key paths:
64
  - Script: `replay-test.ps1`
65
  - Skill: `.claude/skills/replay-test/SKILL.md`
66
 
67
- Without `OPENWA_REPLAY_TEST`, the validator runs in interactive mode with hotkeys (F9=team blocks, F10=landscape) and timed dumps.
 
 
68
 
69
  ## Crate Architecture
70
 
71
  - **`openwa-types`** — Enums, structs, addresses, parsers (no_std compatible). The source of truth for all reverse-engineered type layouts and known addresses (`address.rs`). No game dependency.
72
- - **`openwa-validator`** — WormKit cdylib that validates openwa-types against live WA.exe memory. Logs to `OpenWA_validation.log`. Read-only — never modifies game state.
73
- - **`openwa-wormkit`** — WormKit cdylib that replaces WA functions with Rust. Logs to `OpenWA.log`. Uses MinHook for inline hooking. This is where reimplemented game logic lives.
74
  - **`openwa-harness`** — Offline test harness that loads WA.exe into process memory via `LoadLibraryExA(DONT_RESOLVE_DLL_REFERENCES)` for testing without running the game.
75
 
76
  ## ASLR Rebasing
 
32
 
33
  Game directory: `I:\games\SteamLibrary\steamapps\common\Worms Armageddon`
34
 
35
+ Copy the unified DLL from `target/i686-pc-windows-msvc/release/` to the game directory:
36
  - `openwa_wormkit.dll` → rename to `wkOpenWA.dll`
 
37
 
38
  Quick deploy:
39
  ```bash
40
  cp target/i686-pc-windows-msvc/release/openwa_wormkit.dll "I:/games/SteamLibrary/steamapps/common/Worms Armageddon/wkOpenWA.dll"
 
41
  ```
42
 
43
+ WormKit auto-loads any `wk*.dll` from the game directory. Logs appear in the game directory as `OpenWA.log` and `OpenWA_validation.log` (when validation is enabled).
44
 
45
  ## Replay Testing
46
 
 
52
  ```
53
 
54
  How it works:
55
+ 1. `replay-test.ps1` builds the unified DLL, deploys to game dir, sets `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1`, launches `WA.exe` minimized with a replay file
56
+ 2. The DLL detects the env vars and enters **auto-capture mode**: waits 5s, runs all validation + dumps, then calls `ExitProcess(0)`
57
  3. Script copies logs to `testdata/logs/` and prints a PASS/FAIL summary
58
 
59
  Key paths:
 
62
  - Script: `replay-test.ps1`
63
  - Skill: `.claude/skills/replay-test/SKILL.md`
64
 
65
+ Environment variables:
66
+ - `OPENWA_VALIDATE=1` — Enable validation module (struct checks, vtable validation, memory dumps)
67
+ - `OPENWA_REPLAY_TEST=1` — Auto-capture mode (5s wait, validate, exit). Without this, validation runs interactively with hotkeys (F9=team blocks, F10=landscape)
68
 
69
  ## Crate Architecture
70
 
71
  - **`openwa-types`** — Enums, structs, addresses, parsers (no_std compatible). The source of truth for all reverse-engineered type layouts and known addresses (`address.rs`). No game dependency.
72
+ - **`openwa-wormkit`** — Unified WormKit cdylib that replaces WA functions with Rust and optionally validates types against live memory. Logs to `OpenWA.log` (hooks) and `OpenWA_validation.log` (validation). Uses MinHook for inline hooking. Validation is enabled via `OPENWA_VALIDATE=1` env var.
 
73
  - **`openwa-harness`** — Offline test harness that loads WA.exe into process memory via `LoadLibraryExA(DONT_RESOLVE_DLL_REFERENCES)` for testing without running the game.
74
 
75
  ## ASLR Rebasing
crates/openwa-wormkit/Cargo.toml CHANGED
@@ -15,5 +15,7 @@ minhook = "0.9"
15
  [target.'cfg(target_os = "windows")'.dependencies]
16
  windows-sys = { version = "0.59", features = [
17
  "Win32_System_Memory",
 
 
18
  "Win32_UI_WindowsAndMessaging",
19
  ] }
 
15
  [target.'cfg(target_os = "windows")'.dependencies]
16
  windows-sys = { version = "0.59", features = [
17
  "Win32_System_Memory",
18
+ "Win32_System_Threading",
19
+ "Win32_UI_Input_KeyboardAndMouse",
20
  "Win32_UI_WindowsAndMessaging",
21
  ] }
crates/openwa-wormkit/src/lib.rs CHANGED
@@ -4,6 +4,7 @@ use std::ffi::c_void;
4
 
5
  pub mod hook;
6
  mod replacements;
 
7
 
8
  // ---------------------------------------------------------------------------
9
  // DllMain
@@ -61,5 +62,14 @@ fn run() -> Result<(), String> {
61
  replacements::install_all()?;
62
 
63
  let _ = log_line("=== All replacements installed ===");
 
 
 
 
 
 
 
 
 
64
  Ok(())
65
  }
 
4
 
5
  pub mod hook;
6
  mod replacements;
7
+ mod validation;
8
 
9
  // ---------------------------------------------------------------------------
10
  // DllMain
 
62
  replacements::install_all()?;
63
 
64
  let _ = log_line("=== All replacements installed ===");
65
+
66
+ // Run validation if OPENWA_VALIDATE=1
67
+ if std::env::var("OPENWA_VALIDATE").is_ok() {
68
+ let _ = log_line("=== Validation enabled (OPENWA_VALIDATE) ===");
69
+ if let Err(e) = validation::run() {
70
+ let _ = log_line(&format!("[ERROR] Validation failed: {e}"));
71
+ }
72
+ }
73
+
74
  Ok(())
75
  }
crates/openwa-wormkit/src/validation/hooks/detour.rs ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! MinHook-based inline hooking for constructors (passthrough).
2
+
3
+ use std::ffi::c_void;
4
+ use std::sync::atomic::{AtomicU32, Ordering};
5
+
6
+ use minhook::MinHook;
7
+
8
+ use openwa_lib::rebase::rb;
9
+ use openwa_types::address::va;
10
+
11
+ use super::log_validation;
12
+
13
+ unsafe fn install_hook(ghidra_addr: u32, detour_fn: *const (), name: &str) -> Result<u32, String> {
14
+ let target = rb(ghidra_addr) as *mut c_void;
15
+ let detour = detour_fn as *mut c_void;
16
+
17
+ let trampoline = MinHook::create_hook(target, detour)
18
+ .map_err(|e| format!("MinHook create_hook failed for {name}: {e}"))?;
19
+
20
+ MinHook::enable_hook(target)
21
+ .map_err(|e| format!("MinHook enable_hook failed for {name}: {e}"))?;
22
+
23
+ let trampoline_addr = trampoline as u32;
24
+ let _ = log_validation(&format!(
25
+ " [MH] {name}: target 0x{:08X} (ghidra 0x{ghidra_addr:08X}), trampoline 0x{trampoline_addr:08X}",
26
+ target as u32
27
+ ));
28
+
29
+ Ok(trampoline_addr)
30
+ }
31
+
32
+ static ORIG_CTASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
33
+
34
+ unsafe extern "stdcall" fn hook_ctask_constructor(this: u32, parent: u32, ddgame: u32) -> u32 {
35
+ let orig: unsafe extern "stdcall" fn(u32, u32, u32) -> u32 =
36
+ core::mem::transmute(ORIG_CTASK_CONSTRUCTOR.load(Ordering::Relaxed));
37
+ orig(this, parent, ddgame)
38
+ }
39
+
40
+ static ORIG_CGAMETASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
41
+
42
+ unsafe extern "stdcall" fn hook_cgametask_constructor(this: u32, parent: u32, param3: u32, param4: u32) -> u32 {
43
+ let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
44
+ core::mem::transmute(ORIG_CGAMETASK_CONSTRUCTOR.load(Ordering::Relaxed));
45
+ orig(this, parent, param3, param4)
46
+ }
47
+
48
+ pub fn install() -> Result<(), String> {
49
+ unsafe {
50
+ let trampoline = install_hook(
51
+ va::CTASK_CONSTRUCTOR,
52
+ hook_ctask_constructor as *const (),
53
+ "CTask::Constructor",
54
+ )?;
55
+ ORIG_CTASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
56
+
57
+ let trampoline = install_hook(
58
+ va::CGAMETASK_CONSTRUCTOR,
59
+ hook_cgametask_constructor as *const (),
60
+ "CGameTask::Constructor",
61
+ )?;
62
+ ORIG_CGAMETASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
63
+ }
64
+
65
+ let _ = log_validation(" 2 inline hooks installed via MinHook");
66
+ Ok(())
67
+ }
crates/openwa-wormkit/src/validation/hooks/mod.rs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pub mod vtable;
2
+ pub mod detour;
3
+
4
+ use super::log_validation;
5
+
6
+ pub fn install_all() -> Result<(), String> {
7
+ let _ = log_validation("");
8
+ let _ = log_validation("--- Installing Validation Hooks ---");
9
+ vtable::install()?;
10
+ detour::install()?;
11
+ let _ = log_validation(" All validation hooks installed.");
12
+ Ok(())
13
+ }
crates/openwa-wormkit/src/validation/hooks/vtable.rs ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! VTable hooking — swap function pointers in vtable memory.
2
+
3
+ use std::sync::atomic::{AtomicU32, Ordering};
4
+
5
+ use openwa_lib::rebase::rb;
6
+ use openwa_types::address::va;
7
+
8
+ use super::log_validation;
9
+
10
+ #[cfg(target_os = "windows")]
11
+ use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_READWRITE};
12
+
13
+ pub struct VtableHook {
14
+ #[allow(dead_code)]
15
+ slot_addr: *mut u32,
16
+ original_fn: u32,
17
+ #[allow(dead_code)]
18
+ name: &'static str,
19
+ }
20
+
21
+ unsafe impl Send for VtableHook {}
22
+ unsafe impl Sync for VtableHook {}
23
+
24
+ impl VtableHook {
25
+ #[cfg(target_os = "windows")]
26
+ pub unsafe fn install(
27
+ vtable_ghidra_addr: u32,
28
+ slot_index: usize,
29
+ hook_fn: u32,
30
+ name: &'static str,
31
+ ) -> Result<Self, String> {
32
+ let slot_addr = rb(vtable_ghidra_addr + (slot_index as u32) * 4) as *mut u32;
33
+
34
+ let mut old_protect: u32 = 0;
35
+ let ok = VirtualProtect(slot_addr as *mut _, 4, PAGE_READWRITE, &mut old_protect);
36
+ if ok == 0 {
37
+ return Err(format!("VirtualProtect failed for {name} at 0x{:08X}", slot_addr as u32));
38
+ }
39
+
40
+ let original_fn = *slot_addr;
41
+ *slot_addr = hook_fn;
42
+
43
+ VirtualProtect(slot_addr as *mut _, 4, old_protect, &mut old_protect);
44
+
45
+ let _ = log_validation(&format!(
46
+ " [VT] {name}: slot 0x{:08X}, orig 0x{original_fn:08X} -> hook 0x{hook_fn:08X}",
47
+ slot_addr as u32
48
+ ));
49
+
50
+ Ok(VtableHook { slot_addr, original_fn, name })
51
+ }
52
+
53
+ pub fn original(&self) -> u32 {
54
+ self.original_fn
55
+ }
56
+ }
57
+
58
+ static ORIG_CTASK_PROCESS_FRAME: AtomicU32 = AtomicU32::new(0);
59
+
60
+ unsafe extern "fastcall" fn hook_ctask_process_frame(this: u32, _edx: u32, flags: u32) -> u32 {
61
+ let orig: unsafe extern "fastcall" fn(u32, u32, u32) -> u32 =
62
+ core::mem::transmute(ORIG_CTASK_PROCESS_FRAME.load(Ordering::Relaxed));
63
+ orig(this, 0, flags)
64
+ }
65
+
66
+ static ORIG_CTASK_HANDLE_MESSAGE: AtomicU32 = AtomicU32::new(0);
67
+
68
+ unsafe extern "fastcall" fn hook_ctask_handle_message(
69
+ this: u32, _edx: u32, sender: u32, msg_type: u32, size: u32, data: u32,
70
+ ) -> u32 {
71
+ let orig: unsafe extern "fastcall" fn(u32, u32, u32, u32, u32, u32) -> u32 =
72
+ core::mem::transmute(ORIG_CTASK_HANDLE_MESSAGE.load(Ordering::Relaxed));
73
+ orig(this, 0, sender, msg_type, size, data)
74
+ }
75
+
76
+ pub fn install() -> Result<(), String> {
77
+ unsafe {
78
+ let hook = VtableHook::install(
79
+ va::CTASK_VTABLE, 7,
80
+ hook_ctask_process_frame as *const () as u32,
81
+ "CTask::ProcessFrame",
82
+ )?;
83
+ ORIG_CTASK_PROCESS_FRAME.store(hook.original(), Ordering::Relaxed);
84
+
85
+ let hook = VtableHook::install(
86
+ va::CTASK_VTABLE, 2,
87
+ hook_ctask_handle_message as *const () as u32,
88
+ "CTask::HandleMessage",
89
+ )?;
90
+ ORIG_CTASK_HANDLE_MESSAGE.store(hook.original(), Ordering::Relaxed);
91
+
92
+ let _ = log_validation(" 2 vtable hooks installed");
93
+ }
94
+ Ok(())
95
+ }
crates/openwa-wormkit/src/validation/mod.rs ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Runtime validation of openwa-types against live WA.exe memory.
2
+ //!
3
+ //! Enabled by setting `OPENWA_VALIDATE=1` environment variable.
4
+ //! When `OPENWA_REPLAY_TEST=1` is also set, enters auto-capture mode:
5
+ //! waits 5s, runs all validation + dumps, then calls ExitProcess(0).
6
+
7
+ mod hooks;
8
+
9
+ use std::sync::atomic::{AtomicU32, Ordering};
10
+
11
+ use openwa_lib::rebase::rb;
12
+ use openwa_types::address::va;
13
+ use openwa_types::task::{CTask, CGameTask};
14
+ use openwa_types::ddgame::DDGame;
15
+ use openwa_types::ddgame_wrapper::DDGameWrapper;
16
+
17
+ static DUMP_COUNTER: AtomicU32 = AtomicU32::new(0);
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Logging (separate file from main OpenWA.log)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ pub(crate) fn log_validation(msg: &str) -> std::io::Result<()> {
24
+ use std::io::Write;
25
+ let mut f = std::fs::OpenOptions::new()
26
+ .create(true)
27
+ .append(true)
28
+ .open("OpenWA_validation.log")?;
29
+ writeln!(f, "{}", msg)?;
30
+ Ok(())
31
+ }
32
+
33
+ fn clear_validation_log() -> std::io::Result<()> {
34
+ std::fs::write("OpenWA_validation.log", "")?;
35
+ Ok(())
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // ValidationResult
40
+ // ---------------------------------------------------------------------------
41
+
42
+ struct ValidationResult {
43
+ pass: u32,
44
+ fail: u32,
45
+ }
46
+
47
+ impl ValidationResult {
48
+ fn new() -> Self {
49
+ Self { pass: 0, fail: 0 }
50
+ }
51
+
52
+ fn check(&mut self, name: &str, ok: bool, detail: &str) {
53
+ if ok {
54
+ self.pass += 1;
55
+ let _ = log_validation(&format!("[PASS] {} - {}", name, detail));
56
+ } else {
57
+ self.fail += 1;
58
+ let _ = log_validation(&format!("[FAIL] {} - {}", name, detail));
59
+ }
60
+ }
61
+
62
+ fn total(&self) -> u32 {
63
+ self.pass + self.fail
64
+ }
65
+
66
+ fn summary_line(&self) -> String {
67
+ format!(
68
+ "Results: {}/{} passed, {} failed",
69
+ self.pass, self.total(), self.fail
70
+ )
71
+ }
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Memory helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ #[inline]
79
+ unsafe fn read_u32(addr: u32) -> u32 {
80
+ *(addr as *const u32)
81
+ }
82
+
83
+ #[inline]
84
+ unsafe fn read_u8(addr: u32) -> u8 {
85
+ *(addr as *const u8)
86
+ }
87
+
88
+ #[inline]
89
+ fn is_in_text(addr: u32) -> bool {
90
+ addr >= rb(va::TEXT_START) && addr <= rb(va::TEXT_END)
91
+ }
92
+
93
+ #[inline]
94
+ fn is_in_rdata(addr: u32) -> bool {
95
+ addr >= rb(va::RDATA_START) && addr < rb(va::DATA_START)
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Address validation
100
+ // ---------------------------------------------------------------------------
101
+
102
+ fn validate_addresses(result: &mut ValidationResult) {
103
+ let _ = log_validation("");
104
+ let _ = log_validation("--- Address Validation ---");
105
+
106
+ let vtables: &[(&str, u32)] = &[
107
+ ("CTask vtable", va::CTASK_VTABLE),
108
+ ("CGameTask vtable", va::CGAMETASK_VTABLE),
109
+ ("CGameTask vtable2", va::CGAMETASK_VTABLE2),
110
+ ("DDGameWrapper vtable", va::DDGAME_WRAPPER_VTABLE),
111
+ ("GfxHandler vtable", va::GFX_HANDLER_VTABLE),
112
+ ("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE),
113
+ ("PCLandscape vtable", va::PC_LANDSCAPE_VTABLE),
114
+ ("LandscapeShader vtable", va::LANDSCAPE_SHADER_VTABLE),
115
+ ("DSSound vtable", va::DS_SOUND_VTABLE),
116
+ ("TaskStateMachine vtable", va::TASK_STATE_MACHINE_VTABLE),
117
+ ("OpenGLCPU vtable", va::OPENGL_CPU_VTABLE),
118
+ ("WaterEffect vtable", va::WATER_EFFECT_VTABLE),
119
+ ];
120
+
121
+ let _ = log_validation("");
122
+ let _ = log_validation(" Vtable location checks (.rdata range):");
123
+ for (name, ghidra_addr) in vtables {
124
+ let addr = rb(*ghidra_addr);
125
+ let in_rdata = is_in_rdata(addr);
126
+ result.check(
127
+ &format!("{} location", name),
128
+ in_rdata,
129
+ &format!("0x{:08X} (ghidra 0x{:08X}) {}", addr, ghidra_addr, if in_rdata { "in .rdata" } else { "NOT in .rdata" }),
130
+ );
131
+ }
132
+
133
+ let _ = log_validation("");
134
+ let _ = log_validation(" Vtable first-entry checks (should point to .text):");
135
+ for (name, ghidra_addr) in vtables {
136
+ let addr = rb(*ghidra_addr);
137
+ unsafe {
138
+ let first_entry = read_u32(addr);
139
+ let in_text = is_in_text(first_entry);
140
+ result.check(
141
+ &format!("{} first entry", name),
142
+ in_text,
143
+ &format!(
144
+ "[0x{:08X}] = 0x{:08X} {}",
145
+ addr, first_entry,
146
+ if in_text { "in .text" } else { "NOT in .text" }
147
+ ),
148
+ );
149
+ }
150
+ }
151
+
152
+ let _ = log_validation("");
153
+ let _ = log_validation(" CTask vtable method verification:");
154
+ let ctask_vt_methods: &[(&str, u32, u32)] = &[
155
+ ("vt0 init", 0, va::CTASK_VT0_INIT),
156
+ ("vt1 Free", 4, va::CTASK_VT1_FREE),
157
+ ("vt2 HandleMessage", 8, va::CTASK_VT2_HANDLE_MESSAGE),
158
+ ("vt3", 12, va::CTASK_VT3),
159
+ ("vt4", 16, va::CTASK_VT4),
160
+ ("vt5", 20, va::CTASK_VT5),
161
+ ("vt6", 24, va::CTASK_VT6),
162
+ ("vt7 ProcessFrame", 28, va::CTASK_VT7_PROCESS_FRAME),
163
+ ];
164
+ for (name, offset, expected_ghidra) in ctask_vt_methods {
165
+ unsafe {
166
+ let actual = read_u32(rb(va::CTASK_VTABLE) + offset);
167
+ let expected = rb(*expected_ghidra);
168
+ let ok = actual == expected;
169
+ result.check(
170
+ &format!("CTask::{}", name),
171
+ ok,
172
+ &format!(
173
+ "vtable+0x{:02X}: expected 0x{:08X}, got 0x{:08X}",
174
+ offset, expected, actual
175
+ ),
176
+ );
177
+ }
178
+ }
179
+
180
+ let _ = log_validation("");
181
+ let _ = log_validation(" Function prologue checks:");
182
+ let valid_prologues: &[u8] = &[
183
+ 0x55, 0x53, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81,
184
+ 0xB8, 0x51, 0x52, 0x64, 0x85, 0x8D,
185
+ 0xE9, // JMP — MinHook trampoline (function has been hooked)
186
+ ];
187
+
188
+ let functions: &[(&str, u32)] = &[
189
+ ("CTask::Constructor", va::CTASK_CONSTRUCTOR),
190
+ ("CGameTask::Constructor", va::CGAMETASK_CONSTRUCTOR),
191
+ ("DDGameWrapper::Constructor", va::CONSTRUCT_DD_GAME_WRAPPER),
192
+ ("DDGame::Constructor", va::CONSTRUCT_DD_GAME),
193
+ ("CreateExplosion", va::CREATE_EXPLOSION),
194
+ ("SpawnObject", va::SPAWN_OBJECT),
195
+ ("WeaponRelease", va::WEAPON_RELEASE),
196
+ ("FireWeapon", va::FIRE_WEAPON),
197
+ ("InitWeaponTable", va::INIT_WEAPON_TABLE),
198
+ ("BlitScreen", va::BLIT_SCREEN),
199
+ ("RenderDrawingQueue", va::RENDER_DRAWING_QUEUE),
200
+ ("DrawLandscape", va::DRAW_LANDSCAPE),
201
+ ("ConstructPCLandscape", va::CONSTRUCT_PC_LANDSCAPE),
202
+ ("ConstructDSSound", va::CONSTRUCT_DS_SOUND),
203
+ ("ShowChatMessage", va::SHOW_CHAT_MESSAGE),
204
+ ("FrontendChangeScreen", va::FRONTEND_CHANGE_SCREEN),
205
+ ("WA_MallocMemset", va::WA_MALLOC_MEMSET),
206
+ ("CTask::vt0_init", va::CTASK_VT0_INIT),
207
+ ("CTask::vt1_Free", va::CTASK_VT1_FREE),
208
+ ("CTask::vt2_HandleMessage", va::CTASK_VT2_HANDLE_MESSAGE),
209
+ ("CGameTask::vt0", va::CGAMETASK_VT0),
210
+ ("CGameTask::vt1_Free", va::CGAMETASK_VT1_FREE),
211
+ ("CGameTask::vt2_HandleMessage", va::CGAMETASK_VT2_HANDLE_MESSAGE),
212
+ ];
213
+
214
+ for (name, ghidra_addr) in functions {
215
+ let addr = rb(*ghidra_addr);
216
+ let in_text = is_in_text(addr);
217
+ if !in_text {
218
+ result.check(
219
+ &format!("{} prologue", name),
220
+ false,
221
+ &format!("0x{:08X} (ghidra 0x{:08X}) not in .text range", addr, ghidra_addr),
222
+ );
223
+ continue;
224
+ }
225
+ unsafe {
226
+ let first_byte = read_u8(addr);
227
+ let ok = valid_prologues.contains(&first_byte);
228
+ result.check(
229
+ &format!("{} prologue", name),
230
+ ok,
231
+ &format!(
232
+ "0x{:08X}: first byte 0x{:02X} {}",
233
+ addr, first_byte,
234
+ if ok { "valid" } else { "UNEXPECTED" }
235
+ ),
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Struct offset validation
243
+ // ---------------------------------------------------------------------------
244
+
245
+ fn validate_struct_offsets(result: &mut ValidationResult) {
246
+ let _ = log_validation("");
247
+ let _ = log_validation("--- Struct Offset Validation (offset_of!) ---");
248
+
249
+ macro_rules! check_offset {
250
+ ($result:expr, $struct:ty, $field:ident, $expected:expr) => {
251
+ let actual = core::mem::offset_of!($struct, $field);
252
+ $result.check(
253
+ &format!("{}::{}", stringify!($struct), stringify!($field)),
254
+ actual == $expected,
255
+ &format!("expected 0x{:X}, got 0x{:X}", $expected, actual),
256
+ );
257
+ };
258
+ }
259
+
260
+ let _ = log_validation("");
261
+ let _ = log_validation(" CTask:");
262
+ check_offset!(result, CTask, vtable, 0x00);
263
+ check_offset!(result, CTask, parent, 0x04);
264
+ check_offset!(result, CTask, children_max_size, 0x08);
265
+ check_offset!(result, CTask, children_data, 0x14);
266
+ check_offset!(result, CTask, class_type, 0x20);
267
+ check_offset!(result, CTask, shared_data, 0x24);
268
+ check_offset!(result, CTask, owns_shared_data, 0x28);
269
+ check_offset!(result, CTask, ddgame, 0x2C);
270
+
271
+ let _ = log_validation("");
272
+ let _ = log_validation(" CGameTask:");
273
+ check_offset!(result, CGameTask, base, 0x00);
274
+ check_offset!(result, CGameTask, pos_x, 0x84);
275
+ check_offset!(result, CGameTask, pos_y, 0x88);
276
+ check_offset!(result, CGameTask, speed_x, 0x90);
277
+ check_offset!(result, CGameTask, speed_y, 0x94);
278
+ check_offset!(result, CGameTask, vtable2, 0xE8);
279
+
280
+ let _ = log_validation("");
281
+ let _ = log_validation(" DDGame:");
282
+ check_offset!(result, DDGame, landscape, 0x20);
283
+ check_offset!(result, DDGame, game_state, 0x24);
284
+ check_offset!(result, DDGame, arrow_sprites, 0x38);
285
+ check_offset!(result, DDGame, arrow_gfxdirs, 0xB8);
286
+ check_offset!(result, DDGame, display_gfx, 0x138);
287
+ check_offset!(result, DDGame, task_state_machine, 0x380);
288
+ check_offset!(result, DDGame, sprite_regions, 0x46C);
289
+ check_offset!(result, DDGame, coord_list, 0x50C);
290
+
291
+ let _ = log_validation("");
292
+ let _ = log_validation(" DDGameWrapper:");
293
+ check_offset!(result, DDGameWrapper, vtable, 0x00);
294
+ check_offset!(result, DDGameWrapper, ddgame, 0x488);
295
+ check_offset!(result, DDGameWrapper, _field_4c0, 0x4C0);
296
+ check_offset!(result, DDGameWrapper, landscape, 0x4CC);
297
+ check_offset!(result, DDGameWrapper, display, 0x4D0);
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Deferred global validation
302
+ // ---------------------------------------------------------------------------
303
+
304
+ fn deferred_global_validation() {
305
+ let _ = log_validation("");
306
+ let _ = log_validation("--- Deferred Global Validation (10s after load) ---");
307
+
308
+ let mut result = ValidationResult::new();
309
+
310
+ unsafe {
311
+ let session_ptr = read_u32(rb(va::G_GAME_SESSION));
312
+ let _ = log_validation(&format!(" g_GameSession = 0x{:08X}", session_ptr));
313
+
314
+ if session_ptr == 0 {
315
+ let _ = log_validation(" Game session not initialized yet — no game started?");
316
+ return;
317
+ }
318
+
319
+ let wrapper_addr = read_u32(session_ptr + 0xA0);
320
+ let _ = log_validation(&format!(" DDGameWrapper = 0x{:08X}", wrapper_addr));
321
+
322
+ if wrapper_addr == 0 {
323
+ let _ = log_validation(" DDGameWrapper not created — need to start a game first.");
324
+ return;
325
+ }
326
+
327
+ let vtable_ptr = read_u32(wrapper_addr);
328
+ let expected_vt = rb(va::DDGAME_WRAPPER_VTABLE);
329
+ result.check(
330
+ "DDGameWrapper.vtable",
331
+ vtable_ptr == expected_vt,
332
+ &format!("expected 0x{:08X}, got 0x{:08X}", expected_vt, vtable_ptr),
333
+ );
334
+
335
+ let ddgame_ptr = read_u32(wrapper_addr + 0x488);
336
+ let ddgame_valid = ddgame_ptr != 0;
337
+ result.check(
338
+ "DDGameWrapper.ddgame != NULL",
339
+ ddgame_valid,
340
+ &format!("0x{:08X}", ddgame_ptr),
341
+ );
342
+
343
+ if ddgame_valid {
344
+ let ddgame_landscape = read_u32(ddgame_ptr + 0x20);
345
+ let wrapper_landscape = read_u32(wrapper_addr + 0x4CC);
346
+ result.check(
347
+ "DDGame.landscape == wrapper.landscape",
348
+ ddgame_landscape == wrapper_landscape,
349
+ &format!(
350
+ "DDGame+0x20=0x{:08X}, wrapper+0x4CC=0x{:08X}",
351
+ ddgame_landscape, wrapper_landscape
352
+ ),
353
+ );
354
+
355
+ let display_gfx = read_u32(ddgame_ptr + 0x138);
356
+ if display_gfx != 0 {
357
+ let dgfx_vt = read_u32(display_gfx);
358
+ let expected = rb(va::DISPLAY_GFX_VTABLE);
359
+ result.check(
360
+ "DDGame.display_gfx vtable",
361
+ dgfx_vt == expected,
362
+ &format!("expected 0x{:08X}, got 0x{:08X}", expected, dgfx_vt),
363
+ );
364
+ } else {
365
+ result.check("DDGame.display_gfx", false, "NULL pointer");
366
+ }
367
+
368
+ let tsm = read_u32(ddgame_ptr + 0x380);
369
+ if tsm != 0 {
370
+ let tsm_vt = read_u32(tsm);
371
+ let expected = rb(va::TASK_STATE_MACHINE_VTABLE);
372
+ result.check(
373
+ "DDGame.task_state_machine vtable",
374
+ tsm_vt == expected,
375
+ &format!("expected 0x{:08X}, got 0x{:08X}", expected, tsm_vt),
376
+ );
377
+ } else {
378
+ result.check("DDGame.task_state_machine", false, "NULL pointer");
379
+ }
380
+ }
381
+
382
+ let landscape = read_u32(wrapper_addr + 0x4CC);
383
+ if landscape != 0 {
384
+ let land_vt = read_u32(landscape);
385
+ let expected = rb(va::PC_LANDSCAPE_VTABLE);
386
+ result.check(
387
+ "wrapper.landscape vtable",
388
+ land_vt == expected,
389
+ &format!("expected 0x{:08X}, got 0x{:08X}", expected, land_vt),
390
+ );
391
+ } else {
392
+ result.check("wrapper.landscape", false, "NULL pointer");
393
+ }
394
+ }
395
+
396
+ let _ = log_validation(&format!(" Deferred {}", result.summary_line()));
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Team block dump
401
+ // ---------------------------------------------------------------------------
402
+
403
+ fn dump_team_blocks() {
404
+ use openwa_types::ddgame::{offsets, FullTeamBlock, TeamWeaponState};
405
+
406
+ let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
407
+ let _ = log_validation("");
408
+ let _ = log_validation(&format!("--- Team Block Dump #{} ---", dump_num));
409
+
410
+ unsafe {
411
+ let session_ptr = read_u32(rb(va::G_GAME_SESSION));
412
+ if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; }
413
+
414
+ let wrapper_addr = read_u32(session_ptr + 0xA0);
415
+ if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
416
+
417
+ let ddgame_ptr = read_u32(wrapper_addr + 0x488);
418
+ if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; }
419
+
420
+ let _ = log_validation(&format!(" DDGame = 0x{:08X}", ddgame_ptr));
421
+
422
+ let tws_base = (ddgame_ptr + offsets::TEAM_WEAPON_STATE as u32) as *const u8;
423
+ let tws = &*(tws_base as *const TeamWeaponState);
424
+ let _ = log_validation(&format!(" team_count = {} (TeamWeaponState.team_count)", tws.team_count));
425
+
426
+ let blocks = tws_base.sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
427
+ let blocks_addr = blocks as u32;
428
+ let _ = log_validation(&format!(" blocks_base = 0x{:08X} (DDGame+0x{:X})",
429
+ blocks_addr, blocks_addr - ddgame_ptr));
430
+
431
+ let mut result = ValidationResult::new();
432
+
433
+ let expected_blocks = ddgame_ptr + offsets::TEAM_BLOCKS as u32;
434
+ result.check("TWS_TO_BLOCKS derivation",
435
+ blocks_addr == expected_blocks,
436
+ &format!("got 0x{:08X}, expected 0x{:08X}", blocks_addr, expected_blocks));
437
+
438
+ let num_blocks = (tws.team_count as u32 + 1).max(3).min(7);
439
+ for b in 0..num_blocks {
440
+ let block = &*blocks.add(b as usize);
441
+ let _ = log_validation(&format!("\n === Block {} (0x{:08X}) ===",
442
+ b, blocks_addr + b * 0x51C));
443
+
444
+ if (b + 1) < 7 {
445
+ let sentinel = &(*blocks.add(b as usize + 1)).worms[0];
446
+ let worm_count = sentinel.sentinel_worm_count();
447
+ let eliminated = sentinel.sentinel_eliminated();
448
+
449
+ let entry_ptr = tws_base.add(b as usize * 0x51C);
450
+ let raw_worm_count = *(entry_ptr.sub(4) as *const i32);
451
+ let raw_alliance = *(entry_ptr.add(4) as *const i32);
452
+
453
+ result.check(
454
+ &format!("block[{}] sentinel_worm_count vs raw", b),
455
+ worm_count == raw_worm_count,
456
+ &format!("struct={}, raw={}", worm_count, raw_worm_count),
457
+ );
458
+
459
+ let _ = log_validation(&format!(" sentinel: worm_count={}, eliminated={}, alliance(entry_ptr+4)={}",
460
+ worm_count, eliminated, raw_alliance));
461
+
462
+ for w in 0..8usize {
463
+ let worm = &block.worms[w];
464
+ let active = worm.active_flag;
465
+ let name_bytes = &worm.name;
466
+ let name_len = name_bytes.iter().position(|&c| c == 0).unwrap_or(name_bytes.len());
467
+ let name_str = core::str::from_utf8(&name_bytes[..name_len]).unwrap_or("?");
468
+
469
+ if worm.state != 0 || worm.health != 0 || active != 0 || w == 0 {
470
+ let _ = log_validation(&format!(
471
+ " worm[{}]: state=0x{:04X} active={} max_hp={} hp={} name=\"{}\"",
472
+ w, worm.state, active, worm.max_health, worm.health, name_str
473
+ ));
474
+ }
475
+ }
476
+
477
+ if worm_count > 0 && worm_count <= 7 {
478
+ let struct_total: i32 = (1..=worm_count as usize)
479
+ .map(|w| block.worms[w].health)
480
+ .sum();
481
+
482
+ let raw_health_ptr = entry_ptr.sub(0x4A0) as *const i32;
483
+ let mut raw_total = 0i32;
484
+ for i in 0..worm_count {
485
+ raw_total += *raw_health_ptr.add(i as usize * (0x9C / 4));
486
+ }
487
+
488
+ result.check(
489
+ &format!("block[{}] health sum (struct vs raw)", b),
490
+ struct_total == raw_total,
491
+ &format!("struct={}, raw={}", struct_total, raw_total),
492
+ );
493
+ }
494
+
495
+ if worm_count > 0 {
496
+ let struct_state = block.worms[1].state;
497
+ let raw_state = *(entry_ptr.sub(0x598).add(0x9C) as *const u32);
498
+ result.check(
499
+ &format!("block[{}] worm[1].state (struct vs raw)", b),
500
+ struct_state == raw_state,
501
+ &format!("struct=0x{:X}, raw=0x{:X}", struct_state, raw_state),
502
+ );
503
+
504
+ let _ = log_validation(" CheckWormState0x64 field check (state vs health):");
505
+ for w in 1..=worm_count as usize {
506
+ let worm = &block.worms[w];
507
+ let _ = log_validation(&format!(
508
+ " worm[{}]: state=0x{:04X}({}) health=0x{:04X}({})",
509
+ w, worm.state, worm.state, worm.health, worm.health
510
+ ));
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ let _ = log_validation(&format!("\n Struct Validation {}", result.summary_line()));
517
+ }
518
+ }
519
+
520
+ // ---------------------------------------------------------------------------
521
+ // PCLandscape dump
522
+ // ---------------------------------------------------------------------------
523
+
524
+ fn dump_landscape() {
525
+ use openwa_types::landscape::PCLandscape;
526
+
527
+ let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
528
+ let _ = log_validation("");
529
+ let _ = log_validation(&format!("--- PCLandscape Dump #{} ---", dump_num));
530
+
531
+ unsafe {
532
+ let session_ptr = read_u32(rb(va::G_GAME_SESSION));
533
+ if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; }
534
+
535
+ let wrapper_addr = read_u32(session_ptr + 0xA0);
536
+ if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
537
+
538
+ let landscape_ptr = read_u32(wrapper_addr + 0x4CC);
539
+ if landscape_ptr == 0 { let _ = log_validation(" PCLandscape is NULL."); return; }
540
+
541
+ let land = &*(landscape_ptr as *const PCLandscape);
542
+ let _ = log_validation(&format!(" PCLandscape @ 0x{:08X}", landscape_ptr));
543
+
544
+ let expected_vt = rb(va::PC_LANDSCAPE_VTABLE);
545
+ let vt_ok = land.vtable as u32 == expected_vt;
546
+ let _ = log_validation(&format!(" vtable: 0x{:08X} (expected 0x{:08X}) {}",
547
+ land.vtable as u32, expected_vt, if vt_ok { "OK" } else { "MISMATCH" }));
548
+
549
+ let _ = log_validation(&format!(" ddgame: 0x{:08X}", land.ddgame as u32));
550
+ let _ = log_validation(&format!(" _unknown_900: 0x{:08X}", land._unknown_900 as u32));
551
+ let _ = log_validation(&format!(" collision_bitmap: 0x{:08X}", land.collision_bitmap as u32));
552
+ let _ = log_validation(&format!(" initialized: {}", land.initialized));
553
+
554
+ let primary_count = land.crater_sprites.iter().filter(|&&p| !p.is_null()).count();
555
+ let secondary_count = land.crater_sprites_secondary.iter().filter(|&&p| !p.is_null()).count();
556
+ let _ = log_validation(&format!(" crater_sprites: {}/16 non-null, secondary: {}/16 non-null",
557
+ primary_count, secondary_count));
558
+
559
+ let _ = log_validation(&format!(" layer_0: 0x{:08X}", land.layer_0 as u32));
560
+ let _ = log_validation(&format!(" layer_1: 0x{:08X}", land.layer_1 as u32));
561
+ let _ = log_validation(&format!(" layer_terrain: 0x{:08X}", land.layer_terrain as u32));
562
+ let _ = log_validation(&format!(" layer_edges: 0x{:08X}", land.layer_edges as u32));
563
+ let _ = log_validation(&format!(" layer_shadow: 0x{:08X}", land.layer_shadow as u32));
564
+ let _ = log_validation(&format!(" layer_5: 0x{:08X}", land.layer_5 as u32));
565
+
566
+ if !land.layer_terrain.is_null() {
567
+ let dgfx = land.layer_terrain as u32;
568
+ let pixel_data = read_u32(dgfx + 0x08);
569
+ let stride = read_u32(dgfx + 0x10);
570
+ let width = read_u32(dgfx + 0x14);
571
+ let height = read_u32(dgfx + 0x18);
572
+ let _ = log_validation(&format!(" layer_terrain DisplayGfx: pixels=0x{:08X} stride={} width={} height={}",
573
+ pixel_data, stride, width, height));
574
+ }
575
+
576
+ let _ = log_validation(&format!(" dirty_rect_count: {}", land.dirty_rect_count));
577
+ let _ = log_validation(&format!(" dirty_flag: {}", land.dirty_flag));
578
+ if land.dirty_rect_count > 0 && land.dirty_rect_count <= 256 {
579
+ for i in 0..land.dirty_rect_count.min(5) as usize {
580
+ let r = &land.dirty_rects[i];
581
+ let _ = log_validation(&format!(" rect[{}]: ({},{})..({},{})",
582
+ i, r.x1, r.y1, r.x2, r.y2));
583
+ }
584
+ if land.dirty_rect_count > 5 {
585
+ let _ = log_validation(&format!(" ... and {} more", land.dirty_rect_count - 5));
586
+ }
587
+ }
588
+
589
+ let _ = log_validation(&format!(" _unknown_8ec: 0x{:08X}", land._unknown_8ec));
590
+ let _ = log_validation(&format!(" _unknown_8f0: 0x{:08X}", land._unknown_8f0));
591
+ let _ = log_validation(&format!(" _unknown_8f4: 0x{:08X}", land._unknown_8f4));
592
+ let _ = log_validation(&format!(" resource_handle: 0x{:08X}", land.resource_handle as u32));
593
+ let _ = log_validation(&format!(" level_gfx_handler: 0x{:08X}", land.level_gfx_handler as u32));
594
+ let _ = log_validation(&format!(" water_gfx_handler: 0x{:08X}", land.water_gfx_handler as u32));
595
+ let _ = log_validation(&format!(" visible_bounds: left={} top={} right={} bottom={}",
596
+ land.visible_left, land.visible_top, land.visible_right, land.visible_bottom));
597
+
598
+ let level_path = std::ffi::CStr::from_ptr(land.level_dir_path.as_ptr() as *const i8);
599
+ let theme_path = std::ffi::CStr::from_ptr(land.theme_dir_path.as_ptr() as *const i8);
600
+ let _ = log_validation(&format!(" level_dir_path: {:?}", level_path));
601
+ let _ = log_validation(&format!(" theme_dir_path: {:?}", theme_path));
602
+
603
+ let ddgame_ptr = land.ddgame as u32;
604
+ if ddgame_ptr != 0 {
605
+ let level_w = read_u32(ddgame_ptr + 0x77C0);
606
+ let level_h = read_u32(ddgame_ptr + 0x77C4);
607
+ let level_total = read_u32(ddgame_ptr + 0x77C8);
608
+ let _ = log_validation(&format!(" DDGame level dims: {}x{} (total={})",
609
+ level_w, level_h, level_total));
610
+ }
611
+
612
+ let _ = log_validation(" Vtable slots:");
613
+ let vt_addr = land.vtable as u32;
614
+ for slot in 0..10u32 {
615
+ let entry = read_u32(vt_addr + slot * 4);
616
+ let in_text = is_in_text(entry);
617
+ let _ = log_validation(&format!(" [{}]: 0x{:08X} {}",
618
+ slot, entry, if in_text { "" } else { "(NOT .text)" }));
619
+ }
620
+
621
+ let _ = log_validation(&format!(" _unknown_b3c: 0x{:08X}", land._unknown_b3c));
622
+
623
+ let _ = log_validation(" _unknown_088 (0x44 bytes):");
624
+ log_hex_dump(&land._unknown_088);
625
+ let _ = log_validation(" _unknown_8d9 (0x13 bytes):");
626
+ log_hex_dump(&land._unknown_8d9);
627
+ let _ = log_validation(" _unknown_8f8 (8 bytes):");
628
+ log_hex_dump(&land._unknown_8f8);
629
+ let _ = log_validation(" _unknown_918 (4 bytes):");
630
+ log_hex_dump(&land._unknown_918);
631
+ }
632
+ }
633
+
634
+ fn log_hex_dump(data: &[u8]) {
635
+ for chunk in data.chunks(16) {
636
+ let hex: String = chunk.iter().map(|b| format!("{:02X}", b)).collect::<Vec<_>>().join(" ");
637
+ let ascii: String = chunk.iter().map(|&b| if (0x20..0x7F).contains(&b) { b as char } else { '.' }).collect();
638
+ let _ = log_validation(&format!(" {} | {}", hex, ascii));
639
+ }
640
+ }
641
+
642
+ // ---------------------------------------------------------------------------
643
+ // Public entry point — called from wormkit's run()
644
+ // ---------------------------------------------------------------------------
645
+
646
+ pub fn run() -> Result<(), String> {
647
+ clear_validation_log().map_err(|e| format!("Failed to clear validation log: {e}"))?;
648
+
649
+ log_validation("============================================").map_err(|e| e.to_string())?;
650
+ log_validation(" OpenWA Runtime Validator").map_err(|e| e.to_string())?;
651
+ log_validation(" Target: WA.exe 3.8.1 (Steam)").map_err(|e| e.to_string())?;
652
+ log_validation("============================================").map_err(|e| e.to_string())?;
653
+
654
+ // rb(IMAGE_BASE) gives us the actual runtime base
655
+ let base = rb(va::IMAGE_BASE);
656
+ let delta = base.wrapping_sub(va::IMAGE_BASE);
657
+ let _ = log_validation(&format!(
658
+ " Module base: 0x{:08X} (Ghidra base: 0x{:08X}, delta: 0x{:08X})",
659
+ base, va::IMAGE_BASE, delta
660
+ ));
661
+
662
+ let mut result = ValidationResult::new();
663
+
664
+ validate_addresses(&mut result);
665
+ validate_struct_offsets(&mut result);
666
+
667
+ let _ = log_validation("");
668
+ let _ = log_validation("--- Static Validation Summary ---");
669
+ let _ = log_validation(&format!(" {}", result.summary_line()));
670
+
671
+ match hooks::install_all() {
672
+ Ok(()) => {}
673
+ Err(e) => { let _ = log_validation(&format!("[ERROR] Hook installation failed: {}", e)); }
674
+ }
675
+
676
+ let auto_mode = std::env::var("OPENWA_REPLAY_TEST").is_ok();
677
+
678
+ if auto_mode {
679
+ let _ = log_validation("");
680
+ let _ = log_validation("--- Auto-Capture Mode (OPENWA_REPLAY_TEST) ---");
681
+ std::thread::spawn(move || {
682
+ let _ = log_validation(" Waiting 5s for replay to reach gameplay...");
683
+ std::thread::sleep(std::time::Duration::from_secs(5));
684
+
685
+ let _ = log_validation(" Running deferred global validation...");
686
+ deferred_global_validation();
687
+
688
+ let _ = log_validation(" Running team block dump...");
689
+ dump_team_blocks();
690
+
691
+ let _ = log_validation(" Running landscape dump...");
692
+ dump_landscape();
693
+
694
+ let _ = log_validation("");
695
+ let _ = log_validation("--- Auto-capture complete, exiting ---");
696
+
697
+ unsafe {
698
+ windows_sys::Win32::System::Threading::ExitProcess(0);
699
+ }
700
+ });
701
+ } else {
702
+ let _ = log_validation("");
703
+ let _ = log_validation("--- Interactive Mode (deferred polling + hotkeys) ---");
704
+ std::thread::spawn(move || {
705
+ std::thread::sleep(std::time::Duration::from_secs(10));
706
+ deferred_global_validation();
707
+ });
708
+ let _ = log_validation(" Polling thread started (10s delay).");
709
+
710
+ std::thread::spawn(move || {
711
+ std::thread::sleep(std::time::Duration::from_secs(30));
712
+ dump_team_blocks();
713
+ });
714
+ let _ = log_validation(" Team block dump thread started (30s delay).");
715
+
716
+ std::thread::spawn(move || {
717
+ std::thread::sleep(std::time::Duration::from_secs(15));
718
+ dump_landscape();
719
+ });
720
+ let _ = log_validation(" Landscape dump thread started (15s delay).");
721
+
722
+ std::thread::spawn(|| {
723
+ use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
724
+ const VK_F9: i32 = 0x78;
725
+ const VK_F10: i32 = 0x79;
726
+ let _ = log_validation(" Hotkey listener started (F9=team blocks, F10=landscape).");
727
+ loop {
728
+ std::thread::sleep(std::time::Duration::from_millis(100));
729
+ unsafe {
730
+ if GetAsyncKeyState(VK_F9) & 1 != 0 {
731
+ dump_team_blocks();
732
+ }
733
+ if GetAsyncKeyState(VK_F10) & 1 != 0 {
734
+ dump_landscape();
735
+ }
736
+ }
737
+ }
738
+ });
739
+ }
740
+
741
+ Ok(())
742
+ }
replay-test.ps1 CHANGED
@@ -22,27 +22,33 @@ if (-not (Test-Path $ReplayFile)) {
22
  exit 1
23
  }
24
 
25
- # 1. Build
26
  Write-Host "Building..." -ForegroundColor Cyan
27
- cargo build --release -p openwa-wormkit -p openwa-validator
28
  if ($LASTEXITCODE -ne 0) { exit 1 }
29
 
30
  # 2. Deploy
31
  Write-Host "Deploying to $gameDir..." -ForegroundColor Cyan
32
  Copy-Item "$src\openwa_wormkit.dll" "$gameDir\wkOpenWA.dll"
33
- Copy-Item "$src\openwa_validator.dll" "$gameDir\wkOpenWAValidator.dll"
 
 
 
 
34
 
35
  # 3. Clear old logs
36
  Remove-Item "$gameDir\OpenWA.log" -ErrorAction SilentlyContinue
37
  Remove-Item "$gameDir\OpenWA_validation.log" -ErrorAction SilentlyContinue
38
 
39
- # 4. Launch WA.exe with replay, env var set
40
  Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
41
  Write-Host " (Auto-capture mode: will exit after 5s)" -ForegroundColor Yellow
42
 
 
43
  $env:OPENWA_REPLAY_TEST = "1"
44
  $proc = Start-Process -FilePath $waExe -ArgumentList "`"$ReplayFile`"" -WindowStyle Minimized -PassThru
45
  $proc.WaitForExit()
 
46
  $env:OPENWA_REPLAY_TEST = $null
47
 
48
  Write-Host "WA.exe exited with code $($proc.ExitCode)" -ForegroundColor Cyan
 
22
  exit 1
23
  }
24
 
25
+ # 1. Build (single unified DLL)
26
  Write-Host "Building..." -ForegroundColor Cyan
27
+ cargo build --release -p openwa-wormkit
28
  if ($LASTEXITCODE -ne 0) { exit 1 }
29
 
30
  # 2. Deploy
31
  Write-Host "Deploying to $gameDir..." -ForegroundColor Cyan
32
  Copy-Item "$src\openwa_wormkit.dll" "$gameDir\wkOpenWA.dll"
33
+
34
+ # Remove old validator DLL if present (now unified)
35
+ if (Test-Path "$gameDir\wkOpenWAValidator.dll") {
36
+ Remove-Item "$gameDir\wkOpenWAValidator.dll"
37
+ }
38
 
39
  # 3. Clear old logs
40
  Remove-Item "$gameDir\OpenWA.log" -ErrorAction SilentlyContinue
41
  Remove-Item "$gameDir\OpenWA_validation.log" -ErrorAction SilentlyContinue
42
 
43
+ # 4. Launch WA.exe with replay, env vars set
44
  Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
45
  Write-Host " (Auto-capture mode: will exit after 5s)" -ForegroundColor Yellow
46
 
47
+ $env:OPENWA_VALIDATE = "1"
48
  $env:OPENWA_REPLAY_TEST = "1"
49
  $proc = Start-Process -FilePath $waExe -ArgumentList "`"$ReplayFile`"" -WindowStyle Minimized -PassThru
50
  $proc.WaitForExit()
51
+ $env:OPENWA_VALIDATE = $null
52
  $env:OPENWA_REPLAY_TEST = $null
53
 
54
  Write-Host "WA.exe exited with code $($proc.ExitCode)" -ForegroundColor Cyan