paavohtl Claude Opus 4.6 (1M context) commited on
Commit
78a42eb
·
1 Parent(s): 2b5bc3c

refactor: replace validation module with registry-driven startup checks

Browse files

Delete the 2100-line validation/ module (stale checks, wrong vtable slot,
no-op hooks, sleep-based dumps) and replace with ~120-line startup_checks.rs
that auto-generates all checks from define_addresses! and #[vtable(...)]
metadata. Checks run unconditionally at DLL load — no OPENWA_VALIDATE env
var needed. Test runner scans OpenWA.log for [CHECK FAIL] and reports once.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

.claude/skills/replay-test/SKILL.md CHANGED
@@ -81,7 +81,6 @@ powershell -ExecutionPolicy Bypass -File replay-test.ps1
81
  ## Environment Variables
82
 
83
  - `OPENWA_HEADLESS=1` — Headless mode: auto-dismiss MessageBoxA, SW_HIDE window, file isolation
84
- - `OPENWA_VALIDATE=1` — (headful) Enable validation module
85
  - `OPENWA_REPLAY_TEST=1` — (headful) Enable fast-forward mode (50x speed)
86
  - `OPENWA_LOG_PATH=<path>` — Override OpenWA.log location (per-instance isolation)
87
 
 
81
  ## Environment Variables
82
 
83
  - `OPENWA_HEADLESS=1` — Headless mode: auto-dismiss MessageBoxA, SW_HIDE window, file isolation
 
84
  - `OPENWA_REPLAY_TEST=1` — (headful) Enable fast-forward mode (50x speed)
85
  - `OPENWA_LOG_PATH=<path>` — Override OpenWA.log location (per-instance isolation)
86
 
CLAUDE.md CHANGED
@@ -16,7 +16,6 @@ cargo build --release
16
 
17
  # Build a specific crate
18
  cargo build -p openwa-wormkit --release
19
- cargo build -p openwa-validator --release
20
 
21
  # Run tests (openwa-types is pure Rust, works on any host)
22
  cargo test -p openwa-core
@@ -70,19 +69,18 @@ Key paths:
70
 
71
  ### Headful replay testing (interactive)
72
 
73
- Use the `/replay-test` skill for interactive testing with graphics, sound, and validation module:
74
 
75
  ```bash
76
  powershell -ExecutionPolicy Bypass -File replay-test.ps1
77
  powershell -ExecutionPolicy Bypass -File replay-test.ps1 -Headless testdata/replays/longbow.WAgame
78
  ```
79
 
80
- Headful mode enables fast-forward (50x via DDGame+0x98B0) and runs struct validation at 5s. Use for debugging specific replays or testing visual/audio hooks.
81
 
82
  ### Environment variables
83
 
84
  - `OPENWA_HEADLESS=1` — Headless mode: hooks MessageBoxA to auto-dismiss, launcher uses SW_HIDE, file isolation hook active, semaphore renamed per-PID
85
- - `OPENWA_VALIDATE=1` — Enable validation module (struct checks, vtable validation, memory dumps)
86
  - `OPENWA_REPLAY_TEST=1` — Fast-forward mode for headful testing (50x speed, 120s safety timeout)
87
  - `OPENWA_LOG_PATH=<path>` — Override OpenWA.log location (used by test runner for per-instance isolation)
88
  - `OPENWA_ERRORLOG_PATH=<path>` — Redirect ERRORLOG.TXT to specified path (used by test runner for crash capture)
@@ -159,7 +157,7 @@ Key env vars:
159
 
160
  - **`openwa-core`** — Types, addresses, parsers, ASLR rebasing, and typed WA function wrappers. The source of truth for all reverse-engineered type layouts and known addresses. Contains `registry` (structured address database + field registries), `rebase` (ASLR delta), `wa_call` (calling convention helpers), and `wa/` (typed handle wrappers like `DDGameWrapperHandle`, `CWndHandle`).
161
  - **`openwa-derive`** — Proc macro crate. Provides `#[derive(FieldRegistry)]` for struct field maps and `#[vtable(...)]` for typed vtable definitions with introspection, calling wrappers, and replacement support.
162
- - **`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.
163
  - **`openwa-test-runner`** — Headless replay test runner (`openwa-test` binary). Discovers replay tests, runs them concurrently via WA.exe's `/getlog` mode, compares output logs. See "Replay Testing" section.
164
  - **`openwa-debug-cli`** — CLI tool for live memory inspection (`openwa-debug` binary). Connects to the debug server in the DLL.
165
  - **`openwa-debug-proto`** — Shared protocol types (Request/Response enums, MessagePack framing) between CLI and server.
 
16
 
17
  # Build a specific crate
18
  cargo build -p openwa-wormkit --release
 
19
 
20
  # Run tests (openwa-types is pure Rust, works on any host)
21
  cargo test -p openwa-core
 
69
 
70
  ### Headful replay testing (interactive)
71
 
72
+ Use the `/replay-test` skill for interactive testing with graphics and sound:
73
 
74
  ```bash
75
  powershell -ExecutionPolicy Bypass -File replay-test.ps1
76
  powershell -ExecutionPolicy Bypass -File replay-test.ps1 -Headless testdata/replays/longbow.WAgame
77
  ```
78
 
79
+ Headful mode enables fast-forward (50x via DDGame+0x98B0). Use for debugging specific replays or testing visual/audio hooks.
80
 
81
  ### Environment variables
82
 
83
  - `OPENWA_HEADLESS=1` — Headless mode: hooks MessageBoxA to auto-dismiss, launcher uses SW_HIDE, file isolation hook active, semaphore renamed per-PID
 
84
  - `OPENWA_REPLAY_TEST=1` — Fast-forward mode for headful testing (50x speed, 120s safety timeout)
85
  - `OPENWA_LOG_PATH=<path>` — Override OpenWA.log location (used by test runner for per-instance isolation)
86
  - `OPENWA_ERRORLOG_PATH=<path>` — Redirect ERRORLOG.TXT to specified path (used by test runner for crash capture)
 
157
 
158
  - **`openwa-core`** — Types, addresses, parsers, ASLR rebasing, and typed WA function wrappers. The source of truth for all reverse-engineered type layouts and known addresses. Contains `registry` (structured address database + field registries), `rebase` (ASLR delta), `wa_call` (calling convention helpers), and `wa/` (typed handle wrappers like `DDGameWrapperHandle`, `CWndHandle`).
159
  - **`openwa-derive`** — Proc macro crate. Provides `#[derive(FieldRegistry)]` for struct field maps and `#[vtable(...)]` for typed vtable definitions with introspection, calling wrappers, and replacement support.
160
+ - **`openwa-wormkit`** — Unified WormKit cdylib that replaces WA functions with Rust. Logs to `OpenWA.log`. Uses MinHook for inline hooking. Runs registry-driven startup checks (vtable locations, function prologues) automatically at load; the test runner reports any failures after the test summary.
161
  - **`openwa-test-runner`** — Headless replay test runner (`openwa-test` binary). Discovers replay tests, runs them concurrently via WA.exe's `/getlog` mode, compares output logs. See "Replay Testing" section.
162
  - **`openwa-debug-cli`** — CLI tool for live memory inspection (`openwa-debug` binary). Connects to the debug server in the DLL.
163
  - **`openwa-debug-proto`** — Shared protocol types (Request/Response enums, MessagePack framing) between CLI and server.
crates/openwa-test-runner/src/main.rs CHANGED
@@ -980,6 +980,46 @@ fn run_trace_desync(args: TraceDesyncArgs) {
980
  println!("\nRun directory: {}", run_dir.display());
981
  }
982
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
  // ─── Main ───────────────────────────────────────────────────────────────────
984
 
985
  fn main() {
@@ -1048,6 +1088,9 @@ fn main() {
1048
  print_summary(&results, wall_time);
1049
  write_summary(&results, wall_time, &run_dir.join("summary.txt"));
1050
 
 
 
 
1051
  // Clean up per-PID temp files from the game directory
1052
  cleanup_temp_files(&wa_exe);
1053
 
 
980
  println!("\nRun directory: {}", run_dir.display());
981
  }
982
 
983
+ // ─── Startup check reporting ─────────────────────────────────────────────────
984
+
985
+ /// Scan the first test's OpenWA.log for `[CHECK FAIL]` lines and report them
986
+ /// once. All test instances load the same DLL against the same WA.exe, so the
987
+ /// startup check results are deterministic — checking one log is sufficient.
988
+ fn report_startup_check_failures(results: &[TestResult], run_dir: &Path) {
989
+ let first = match results.first() {
990
+ Some(r) => r,
991
+ None => return,
992
+ };
993
+ let log_path = run_dir.join(format!("{}.openwa.log", first.name));
994
+ let content = match fs::read_to_string(&log_path) {
995
+ Ok(c) => c,
996
+ Err(_) => return,
997
+ };
998
+
999
+ let failures: Vec<&str> = content
1000
+ .lines()
1001
+ .filter(|l| l.contains("[CHECK FAIL]"))
1002
+ .collect();
1003
+
1004
+ if failures.is_empty() {
1005
+ return;
1006
+ }
1007
+
1008
+ println!();
1009
+ let msg = format!(
1010
+ "Startup check failures detected (from {}.openwa.log):",
1011
+ first.name
1012
+ );
1013
+ if use_color() {
1014
+ println!("\x1b[33m{msg}\x1b[0m");
1015
+ } else {
1016
+ println!("{msg}");
1017
+ }
1018
+ for line in &failures {
1019
+ println!(" {line}");
1020
+ }
1021
+ }
1022
+
1023
  // ─── Main ───────────────────────────────────────────────────────────────────
1024
 
1025
  fn main() {
 
1088
  print_summary(&results, wall_time);
1089
  write_summary(&results, wall_time, &run_dir.join("summary.txt"));
1090
 
1091
+ // Report startup check failures from the DLL (once, from the first test's log)
1092
+ report_startup_check_failures(&results, &run_dir);
1093
+
1094
  // Clean up per-PID temp files from the game directory
1095
  cleanup_temp_files(&wa_exe);
1096
 
crates/openwa-wormkit/src/debug_watchpoint.rs CHANGED
@@ -4,7 +4,7 @@
4
  //! A Vectored Exception Handler logs the exact instruction address (as Ghidra VA)
5
  //! of every write to the watched offsets.
6
  //!
7
- //! Activated by `OPENWA_VALIDATE=1`. No external debugger needed — the DLL
8
  //! instruments itself by triggering INT3 exceptions whose VEH handler sets
9
  //! the debug registers directly in the thread context.
10
  //!
 
4
  //! A Vectored Exception Handler logs the exact instruction address (as Ghidra VA)
5
  //! of every write to the watched offsets.
6
  //!
7
+ //! Currently dormant — activate by wiring calls in `game_session.rs`. No external debugger needed — the DLL
8
  //! instruments itself by triggering INT3 exceptions whose VEH handler sets
9
  //! the debug registers directly in the thread context.
10
  //!
crates/openwa-wormkit/src/lib.rs CHANGED
@@ -13,7 +13,7 @@ mod debug_watchpoint;
13
  pub mod hook;
14
  mod replacements;
15
  mod snapshot;
16
- mod validation;
17
 
18
  // ---------------------------------------------------------------------------
19
  // DllMain
@@ -66,10 +66,6 @@ fn clear_log() -> std::io::Result<()> {
66
 
67
  fn run() -> Result<(), String> {
68
  let _ = clear_log();
69
- let validation_path =
70
- std::env::var_os("OPENWA_VALIDATION_LOG_PATH").unwrap_or("OpenWA_validation.log".into());
71
- let _ = std::fs::write(validation_path, "");
72
-
73
  // Install panic hook that writes to our log file
74
  std::panic::set_hook(Box::new(|info| {
75
  let _ = log_line(&format!("[PANIC] {info}"));
@@ -97,16 +93,8 @@ fn run() -> Result<(), String> {
97
  // module loading), the event won't exist and this is a harmless no-op.
98
  signal_hooks_ready();
99
 
100
- // Run validation if OPENWA_VALIDATE=1
101
- if std::env::var("OPENWA_VALIDATE").is_ok() {
102
- let _ = log_line("=== Validation enabled (OPENWA_VALIDATE) ===");
103
- if let Err(e) = validation::run() {
104
- let _ = log_line(&format!("[ERROR] Validation failed: {e}"));
105
- }
106
- }
107
-
108
- // Debug hotkeys (F9/F10) are always available, even without OPENWA_VALIDATE
109
- validation::start_hotkeys();
110
 
111
  // Debug UI window (requires "debug-ui" feature + OPENWA_DEBUG_UI=1)
112
  debug_ui::maybe_spawn();
 
13
  pub mod hook;
14
  mod replacements;
15
  mod snapshot;
16
+ mod startup_checks;
17
 
18
  // ---------------------------------------------------------------------------
19
  // DllMain
 
66
 
67
  fn run() -> Result<(), String> {
68
  let _ = clear_log();
 
 
 
 
69
  // Install panic hook that writes to our log file
70
  std::panic::set_hook(Box::new(|info| {
71
  let _ = log_line(&format!("[PANIC] {info}"));
 
93
  // module loading), the event won't exist and this is a harmless no-op.
94
  signal_hooks_ready();
95
 
96
+ // Run startup address checks (fast, always-on)
97
+ startup_checks::run();
 
 
 
 
 
 
 
 
98
 
99
  // Debug UI window (requires "debug-ui" feature + OPENWA_DEBUG_UI=1)
100
  debug_ui::maybe_spawn();
crates/openwa-wormkit/src/replacements/replay_test.rs CHANGED
@@ -112,41 +112,40 @@ unsafe fn check_milestones(ddgame: *const DDGame, frame: u32) {
112
  }
113
  }
114
 
115
- /// Write gameplay milestone report to the validation log.
116
  ///
117
  /// Called from DLL_PROCESS_DETACH to provide a final summary of game progress.
118
  pub fn write_gameplay_report() {
119
- use crate::validation::log_validation;
120
 
121
  let frames = super::frame_hook::frames_processed();
122
  let started = MATCH_STARTED.load(Ordering::Relaxed);
123
  let completed = MATCH_COMPLETED.load(Ordering::Relaxed);
124
 
125
- let _ = log_validation("");
126
- let _ = log_validation("--- Gameplay Checks ---");
127
 
128
  if frames > 0 {
129
- let _ = log_validation(&format!(
130
  "[GAMEPLAY PASS] Game initialized - {} frames processed",
131
  frames
132
  ));
133
  } else {
134
- let _ = log_validation(
135
  "[GAMEPLAY FAIL] Game initialized - no frames processed (game may not have started)",
136
  );
137
  }
138
 
139
  if started {
140
  let teams = TEAMS_AT_START.load(Ordering::Relaxed);
141
- let _ = log_validation(&format!(
142
  "[GAMEPLAY PASS] Match started - {} teams with alive worms detected",
143
  teams
144
  ));
145
  } else if frames > 0 {
146
  let _ =
147
- log_validation("[GAMEPLAY FAIL] Match started - never detected multiple alive teams");
148
  } else {
149
- let _ = log_validation("[GAMEPLAY FAIL] Match started - game never initialized");
150
  }
151
 
152
  if completed {
@@ -157,15 +156,15 @@ pub fn write_gameplay_report() {
157
  } else {
158
  "draw (all eliminated)"
159
  };
160
- let _ = log_validation(&format!(
161
  "[GAMEPLAY PASS] Match completed - {} at frame {}",
162
  outcome, end_frame
163
  ));
164
  } else if started {
165
  let _ =
166
- log_validation("[GAMEPLAY FAIL] Match completed - match started but never finished");
167
  } else {
168
- let _ = log_validation("[GAMEPLAY FAIL] Match completed - match never started");
169
  }
170
  }
171
 
 
112
  }
113
  }
114
 
115
+ /// Write gameplay milestone report to the log.
116
  ///
117
  /// Called from DLL_PROCESS_DETACH to provide a final summary of game progress.
118
  pub fn write_gameplay_report() {
119
+ use crate::log_line;
120
 
121
  let frames = super::frame_hook::frames_processed();
122
  let started = MATCH_STARTED.load(Ordering::Relaxed);
123
  let completed = MATCH_COMPLETED.load(Ordering::Relaxed);
124
 
125
+ let _ = log_line("--- Gameplay Checks ---");
 
126
 
127
  if frames > 0 {
128
+ let _ = log_line(&format!(
129
  "[GAMEPLAY PASS] Game initialized - {} frames processed",
130
  frames
131
  ));
132
  } else {
133
+ let _ = log_line(
134
  "[GAMEPLAY FAIL] Game initialized - no frames processed (game may not have started)",
135
  );
136
  }
137
 
138
  if started {
139
  let teams = TEAMS_AT_START.load(Ordering::Relaxed);
140
+ let _ = log_line(&format!(
141
  "[GAMEPLAY PASS] Match started - {} teams with alive worms detected",
142
  teams
143
  ));
144
  } else if frames > 0 {
145
  let _ =
146
+ log_line("[GAMEPLAY FAIL] Match started - never detected multiple alive teams");
147
  } else {
148
+ let _ = log_line("[GAMEPLAY FAIL] Match started - game never initialized");
149
  }
150
 
151
  if completed {
 
156
  } else {
157
  "draw (all eliminated)"
158
  };
159
+ let _ = log_line(&format!(
160
  "[GAMEPLAY PASS] Match completed - {} at frame {}",
161
  outcome, end_frame
162
  ));
163
  } else if started {
164
  let _ =
165
+ log_line("[GAMEPLAY FAIL] Match completed - match started but never finished");
166
  } else {
167
+ let _ = log_line("[GAMEPLAY FAIL] Match completed - match never started");
168
  }
169
  }
170
 
crates/openwa-wormkit/src/startup_checks.rs ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Registry-driven startup validation of WA.exe addresses.
2
+ //!
3
+ //! Runs unconditionally at DLL load (after hook installation). Checks that
4
+ //! all registered vtables, functions, and constructors point to valid memory
5
+ //! regions in the live binary. Fully auto-generated from `define_addresses!`
6
+ //! and `#[vtable(...)]` metadata — no hardcoded address lists.
7
+
8
+ use openwa_core::address::va;
9
+ use openwa_core::log::log_line;
10
+ use openwa_core::rebase::rb;
11
+ use openwa_core::registry::{self, AddrKind};
12
+
13
+ /// Valid first bytes for x86 function prologues (MSVC).
14
+ /// This covers standard prologues plus common non-standard function starts.
15
+ const VALID_PROLOGUES: &[u8] = &[
16
+ 0x33, // xor reg, reg
17
+ 0x51, // push ecx
18
+ 0x52, // push edx
19
+ 0x53, // push ebx
20
+ 0x55, // push ebp
21
+ 0x56, // push esi
22
+ 0x57, // push edi
23
+ 0x64, // fs: prefix (SEH setup)
24
+ 0x66, // operand-size prefix
25
+ 0x6A, // push imm8
26
+ 0x80, // cmp byte, imm8
27
+ 0x81, // sub esp, imm32
28
+ 0x83, // sub esp, imm8
29
+ 0x85, // test reg, reg
30
+ 0x89, // mov reg, reg
31
+ 0x8B, // mov reg, ...
32
+ 0x8D, // lea reg, ...
33
+ 0xA1, // mov eax, [imm32]
34
+ 0xB8, // mov eax, imm32
35
+ 0xC1, // shr/shl reg, imm8
36
+ 0xE8, // call rel32
37
+ 0xE9, // jmp rel32 (MinHook trampoline — function is hooked)
38
+ 0xEB, // jmp rel8 (short jump)
39
+ 0xFF, // call/jmp [mem]
40
+ ];
41
+
42
+ #[inline]
43
+ fn is_in_text(addr: u32) -> bool {
44
+ addr >= rb(va::TEXT_START) && addr <= rb(va::TEXT_END)
45
+ }
46
+
47
+ #[inline]
48
+ fn is_in_rdata(addr: u32) -> bool {
49
+ addr >= rb(va::RDATA_START) && addr < rb(va::DATA_START)
50
+ }
51
+
52
+ /// Check if an address points to executable memory (any module).
53
+ /// Uses VirtualQuery to inspect the memory protection flags.
54
+ #[inline]
55
+ fn is_executable(addr: u32) -> bool {
56
+ use windows_sys::Win32::System::Memory::{
57
+ VirtualQuery, MEMORY_BASIC_INFORMATION, PAGE_EXECUTE, PAGE_EXECUTE_READ,
58
+ PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
59
+ };
60
+ unsafe {
61
+ let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
62
+ let size = VirtualQuery(
63
+ addr as *const _,
64
+ &mut mbi,
65
+ core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
66
+ );
67
+ if size == 0 {
68
+ return false;
69
+ }
70
+ matches!(
71
+ mbi.Protect,
72
+ PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY
73
+ )
74
+ }
75
+ }
76
+
77
+ /// Run all startup checks. Returns the number of failures.
78
+ pub fn run() -> u32 {
79
+ let mut pass = 0u32;
80
+ let mut fail = 0u32;
81
+
82
+ // 1. Vtable location checks: all registered vtables should be in .rdata
83
+ for entry in registry::entries_by_kind(AddrKind::Vtable) {
84
+ let addr = rb(entry.va);
85
+ if is_in_rdata(addr) {
86
+ pass += 1;
87
+ } else {
88
+ fail += 1;
89
+ let _ = log_line(&format!(
90
+ "[CHECK FAIL] {} location: 0x{:08X} (ghidra 0x{:08X}) NOT in .rdata",
91
+ entry.name, addr, entry.va
92
+ ));
93
+ }
94
+ }
95
+
96
+ // 2. Vtable first-entry checks: first slot should point to executable code.
97
+ // May be in WA.exe .text or in our DLL (if we replaced the vtable slot).
98
+ for entry in registry::entries_by_kind(AddrKind::Vtable) {
99
+ let addr = rb(entry.va);
100
+ unsafe {
101
+ let first_entry = *(addr as *const u32);
102
+ if is_executable(first_entry) {
103
+ pass += 1;
104
+ } else {
105
+ fail += 1;
106
+ let _ = log_line(&format!(
107
+ "[CHECK FAIL] {} first entry: [0x{:08X}] = 0x{:08X} NOT executable",
108
+ entry.name, addr, first_entry
109
+ ));
110
+ }
111
+ }
112
+ }
113
+
114
+ // 3. Typed vtable slot checks: named slots should point to executable code.
115
+ // Slots may point to WA.exe .text or to our DLL (replaced vtable methods).
116
+ for info in registry::all_vtable_info() {
117
+ if info.ghidra_va == 0 {
118
+ continue;
119
+ }
120
+ let vt_base = rb(info.ghidra_va);
121
+ for slot in info.slots {
122
+ unsafe {
123
+ let slot_addr = vt_base + slot.index * 4;
124
+ let fn_ptr = *(slot_addr as *const u32);
125
+ if is_executable(fn_ptr) {
126
+ pass += 1;
127
+ } else {
128
+ fail += 1;
129
+ let _ = log_line(&format!(
130
+ "[CHECK FAIL] {}::{} [slot {}]: 0x{:08X} NOT executable",
131
+ info.class_name, slot.name, slot.index, fn_ptr
132
+ ));
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // 4. Function/constructor prologue checks
139
+ let prologue_kinds = [AddrKind::Function, AddrKind::Constructor];
140
+ for kind in &prologue_kinds {
141
+ for entry in registry::entries_by_kind(*kind) {
142
+ let addr = rb(entry.va);
143
+ if !is_in_text(addr) {
144
+ fail += 1;
145
+ let _ = log_line(&format!(
146
+ "[CHECK FAIL] {} prologue: 0x{:08X} (ghidra 0x{:08X}) NOT in .text",
147
+ entry.name, addr, entry.va
148
+ ));
149
+ continue;
150
+ }
151
+ unsafe {
152
+ let first_byte = *(addr as *const u8);
153
+ if VALID_PROLOGUES.contains(&first_byte) {
154
+ pass += 1;
155
+ } else {
156
+ fail += 1;
157
+ let _ = log_line(&format!(
158
+ "[CHECK FAIL] {} prologue: 0x{:08X} first byte 0x{:02X} unexpected",
159
+ entry.name, addr, first_byte
160
+ ));
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ let total = pass + fail;
167
+ let _ = log_line(&format!("[CHECK] {pass}/{total} passed, {fail} failed"));
168
+ fail
169
+ }
crates/openwa-wormkit/src/validation/hooks/detour.rs DELETED
@@ -1,72 +0,0 @@
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_core::address::va;
9
- use openwa_core::rebase::rb;
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(
43
- this: u32,
44
- parent: u32,
45
- param3: u32,
46
- param4: u32,
47
- ) -> u32 {
48
- let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
49
- core::mem::transmute(ORIG_CGAMETASK_CONSTRUCTOR.load(Ordering::Relaxed));
50
- orig(this, parent, param3, param4)
51
- }
52
-
53
- pub fn install() -> Result<(), String> {
54
- unsafe {
55
- let trampoline = install_hook(
56
- va::CTASK_CONSTRUCTOR,
57
- hook_ctask_constructor as *const (),
58
- "CTask::Constructor",
59
- )?;
60
- ORIG_CTASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
61
-
62
- let trampoline = install_hook(
63
- va::CGAMETASK_CONSTRUCTOR,
64
- hook_cgametask_constructor as *const (),
65
- "CGameTask::Constructor",
66
- )?;
67
- ORIG_CGAMETASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
68
- }
69
-
70
- let _ = log_validation(" 2 inline hooks installed via MinHook");
71
- Ok(())
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-wormkit/src/validation/hooks/mod.rs DELETED
@@ -1,13 +0,0 @@
1
- pub mod detour;
2
- pub mod vtable;
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 DELETED
@@ -1,109 +0,0 @@
1
- //! VTable hooking — swap function pointers in vtable memory.
2
-
3
- use std::sync::atomic::{AtomicU32, Ordering};
4
-
5
- use openwa_core::address::va;
6
- use openwa_core::rebase::rb;
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!(
38
- "VirtualProtect failed for {name} at 0x{:08X}",
39
- slot_addr as u32
40
- ));
41
- }
42
-
43
- let original_fn = *slot_addr;
44
- *slot_addr = hook_fn;
45
-
46
- VirtualProtect(slot_addr as *mut _, 4, old_protect, &mut old_protect);
47
-
48
- let _ = log_validation(&format!(
49
- " [VT] {name}: slot 0x{:08X}, orig 0x{original_fn:08X} -> hook 0x{hook_fn:08X}",
50
- slot_addr as u32
51
- ));
52
-
53
- Ok(VtableHook {
54
- slot_addr,
55
- original_fn,
56
- name,
57
- })
58
- }
59
-
60
- pub fn original(&self) -> u32 {
61
- self.original_fn
62
- }
63
- }
64
-
65
- static ORIG_CTASK_PROCESS_FRAME: AtomicU32 = AtomicU32::new(0);
66
-
67
- unsafe extern "fastcall" fn hook_ctask_process_frame(this: u32, _edx: u32, flags: u32) -> u32 {
68
- let orig: unsafe extern "fastcall" fn(u32, u32, u32) -> u32 =
69
- core::mem::transmute(ORIG_CTASK_PROCESS_FRAME.load(Ordering::Relaxed));
70
- orig(this, 0, flags)
71
- }
72
-
73
- static ORIG_CTASK_HANDLE_MESSAGE: AtomicU32 = AtomicU32::new(0);
74
-
75
- unsafe extern "fastcall" fn hook_ctask_handle_message(
76
- this: u32,
77
- _edx: u32,
78
- sender: u32,
79
- msg_type: u32,
80
- size: u32,
81
- data: u32,
82
- ) -> u32 {
83
- let orig: unsafe extern "fastcall" fn(u32, u32, u32, u32, u32, u32) -> u32 =
84
- core::mem::transmute(ORIG_CTASK_HANDLE_MESSAGE.load(Ordering::Relaxed));
85
- orig(this, 0, sender, msg_type, size, data)
86
- }
87
-
88
- pub fn install() -> Result<(), String> {
89
- unsafe {
90
- let hook = VtableHook::install(
91
- va::CTASK_VTABLE,
92
- 7,
93
- hook_ctask_process_frame as *const () as u32,
94
- "CTask::ProcessFrame",
95
- )?;
96
- ORIG_CTASK_PROCESS_FRAME.store(hook.original(), Ordering::Relaxed);
97
-
98
- let hook = VtableHook::install(
99
- va::CTASK_VTABLE,
100
- 2,
101
- hook_ctask_handle_message as *const () as u32,
102
- "CTask::HandleMessage",
103
- )?;
104
- ORIG_CTASK_HANDLE_MESSAGE.store(hook.original(), Ordering::Relaxed);
105
-
106
- let _ = log_validation(" 2 vtable hooks installed");
107
- }
108
- Ok(())
109
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-wormkit/src/validation/mod.rs DELETED
@@ -1,1915 +0,0 @@
1
- //! Runtime validation of openwa-core 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 for the replay to finish (up to 120s), runs validation dumps,
6
- //! then calls ExitProcess(0) as a safety net.
7
-
8
- mod hooks;
9
-
10
- use std::sync::atomic::{AtomicU32, Ordering};
11
-
12
- use openwa_core::address::va;
13
- use openwa_core::engine::{DDGame, DDGameWrapper};
14
- use openwa_core::rebase::rb;
15
- use openwa_core::task::{
16
- CGameTask, CTask, CTaskBfsIter, CTaskMissile, CTaskTeam, CTaskTurnGame, SharedDataTable,
17
- TurnGameCtx,
18
- };
19
-
20
- static DUMP_COUNTER: AtomicU32 = AtomicU32::new(0);
21
-
22
- // ---------------------------------------------------------------------------
23
- // Logging (separate file from main OpenWA.log)
24
- // ---------------------------------------------------------------------------
25
-
26
- pub(crate) fn log_validation(msg: &str) -> std::io::Result<()> {
27
- use std::io::Write;
28
- let mut f = std::fs::OpenOptions::new()
29
- .create(true)
30
- .append(true)
31
- .open("OpenWA_validation.log")?;
32
- writeln!(f, "{}", msg)?;
33
- Ok(())
34
- }
35
-
36
- fn clear_validation_log() -> std::io::Result<()> {
37
- std::fs::write("OpenWA_validation.log", "")?;
38
- Ok(())
39
- }
40
-
41
- // ---------------------------------------------------------------------------
42
- // ValidationResult
43
- // ---------------------------------------------------------------------------
44
-
45
- struct ValidationResult {
46
- pass: u32,
47
- fail: u32,
48
- }
49
-
50
- impl ValidationResult {
51
- fn new() -> Self {
52
- Self { pass: 0, fail: 0 }
53
- }
54
-
55
- fn check(&mut self, name: &str, ok: bool, detail: &str) {
56
- if ok {
57
- self.pass += 1;
58
- let _ = log_validation(&format!("[STATIC PASS] {} - {}", name, detail));
59
- } else {
60
- self.fail += 1;
61
- let _ = log_validation(&format!("[STATIC FAIL] {} - {}", name, detail));
62
- }
63
- }
64
-
65
- fn total(&self) -> u32 {
66
- self.pass + self.fail
67
- }
68
-
69
- fn summary_line(&self) -> String {
70
- format!(
71
- "Results: {}/{} passed, {} failed",
72
- self.pass,
73
- self.total(),
74
- self.fail
75
- )
76
- }
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Memory helpers
81
- // ---------------------------------------------------------------------------
82
-
83
- #[inline]
84
- unsafe fn read_u32(addr: u32) -> u32 {
85
- *(addr as *const u32)
86
- }
87
-
88
- #[inline]
89
- unsafe fn read_u8(addr: u32) -> u8 {
90
- *(addr as *const u8)
91
- }
92
-
93
- #[inline]
94
- fn is_in_text(addr: u32) -> bool {
95
- addr >= rb(va::TEXT_START) && addr <= rb(va::TEXT_END)
96
- }
97
-
98
- #[inline]
99
- fn is_in_rdata(addr: u32) -> bool {
100
- addr >= rb(va::RDATA_START) && addr < rb(va::DATA_START)
101
- }
102
-
103
- // ---------------------------------------------------------------------------
104
- // Address validation
105
- // ---------------------------------------------------------------------------
106
-
107
- fn validate_addresses(result: &mut ValidationResult) {
108
- let _ = log_validation("");
109
- let _ = log_validation("--- Address Validation ---");
110
-
111
- // Auto-discover all registered vtables from the address registry
112
- let vtables: Vec<(&str, u32)> =
113
- openwa_core::registry::entries_by_kind(openwa_core::registry::AddrKind::Vtable)
114
- .map(|e| {
115
- let name = e.class_name.map_or(e.name, |c| c);
116
- (name, e.va)
117
- })
118
- .collect();
119
-
120
- let _ = log_validation("");
121
- let _ = log_validation(&format!(
122
- " Vtable location checks (.rdata range) — {} registered vtables:",
123
- vtables.len()
124
- ));
125
- for &(name, ghidra_addr) in &vtables {
126
- let addr = rb(ghidra_addr);
127
- let in_rdata = is_in_rdata(addr);
128
- result.check(
129
- &format!("{} location", name),
130
- in_rdata,
131
- &format!(
132
- "0x{:08X} (ghidra 0x{:08X}) {}",
133
- addr,
134
- ghidra_addr,
135
- if in_rdata {
136
- "in .rdata"
137
- } else {
138
- "NOT in .rdata"
139
- }
140
- ),
141
- );
142
- }
143
-
144
- let _ = log_validation("");
145
- let _ = log_validation(" Vtable first-entry checks (should point to .text):");
146
- for &(name, ghidra_addr) in &vtables {
147
- let addr = rb(ghidra_addr);
148
- unsafe {
149
- let first_entry = read_u32(addr);
150
- let in_text = is_in_text(first_entry);
151
- result.check(
152
- &format!("{} first entry", name),
153
- in_text,
154
- &format!(
155
- "[0x{:08X}] = 0x{:08X} {}",
156
- addr,
157
- first_entry,
158
- if in_text { "in .text" } else { "NOT in .text" }
159
- ),
160
- );
161
- }
162
- }
163
-
164
- let _ = log_validation("");
165
- let _ = log_validation(" CTask vtable method verification:");
166
- let ctask_vt_methods: &[(&str, u32, u32)] = &[
167
- ("vt0 init", 0, va::CTASK_VT0_INIT),
168
- ("vt1 Free", 4, va::CTASK_VT1_FREE),
169
- ("vt2 HandleMessage", 8, va::CTASK_VT2_HANDLE_MESSAGE),
170
- ("vt3", 12, va::CTASK_VT3),
171
- ("vt4", 16, va::CTASK_VT4),
172
- ("vt5", 20, va::CTASK_VT5),
173
- ("vt6", 24, va::CTASK_VT6),
174
- ("vt7 ProcessFrame", 28, va::CTASK_VT7_PROCESS_FRAME),
175
- ];
176
- for (name, offset, expected_ghidra) in ctask_vt_methods {
177
- unsafe {
178
- let actual = read_u32(rb(va::CTASK_VTABLE) + offset);
179
- let expected = rb(*expected_ghidra);
180
- let ok = actual == expected;
181
- result.check(
182
- &format!("CTask::{}", name),
183
- ok,
184
- &format!(
185
- "vtable+0x{:02X}: expected 0x{:08X}, got 0x{:08X}",
186
- offset, expected, actual
187
- ),
188
- );
189
- }
190
- }
191
-
192
- let _ = log_validation("");
193
- let _ = log_validation(" Function prologue checks:");
194
- let valid_prologues: &[u8] = &[
195
- 0x55, 0x53, 0x56, 0x57, 0x83, 0x8B, 0x6A, 0x81, 0xB8, 0x51, 0x52, 0x64, 0x85, 0x8D,
196
- 0xE9, // JMP — MinHook trampoline (function has been hooked)
197
- ];
198
-
199
- let functions: &[(&str, u32)] = &[
200
- ("CTask::Constructor", va::CTASK_CONSTRUCTOR),
201
- ("CGameTask::Constructor", va::CGAMETASK_CONSTRUCTOR),
202
- ("DDGameWrapper::Constructor", va::CONSTRUCT_DD_GAME_WRAPPER),
203
- ("DDGame::Constructor", va::CONSTRUCT_DD_GAME),
204
- ("CreateExplosion", va::CREATE_EXPLOSION),
205
- ("SpawnObject", va::SPAWN_OBJECT),
206
- ("WeaponRelease", va::WEAPON_RELEASE),
207
- ("FireWeapon", va::FIRE_WEAPON),
208
- ("InitWeaponTable", va::INIT_WEAPON_TABLE),
209
- ("BlitScreen", va::BLIT_SCREEN),
210
- ("RenderDrawingQueue", va::RQ_RENDER_DRAWING_QUEUE),
211
- ("DrawLandscape", va::DRAW_LANDSCAPE),
212
- ("ConstructPCLandscape", va::CONSTRUCT_PC_LANDSCAPE),
213
- ("ConstructDSSound", va::CONSTRUCT_DS_SOUND),
214
- ("ShowChatMessage", va::SHOW_CHAT_MESSAGE),
215
- ("FrontendChangeScreen", va::FRONTEND_CHANGE_SCREEN),
216
- ("WA_MallocMemset", va::WA_MALLOC_MEMSET),
217
- ("CTask::vt0_init", va::CTASK_VT0_INIT),
218
- ("CTask::vt1_Free", va::CTASK_VT1_FREE),
219
- ("CTask::vt2_HandleMessage", va::CTASK_VT2_HANDLE_MESSAGE),
220
- ("CGameTask::vt0", va::CGAMETASK_VT0),
221
- ("CGameTask::vt1_Free", va::CGAMETASK_VT1_FREE),
222
- (
223
- "CGameTask::vt2_HandleMessage",
224
- va::CGAMETASK_VT2_HANDLE_MESSAGE,
225
- ),
226
- ];
227
-
228
- for (name, ghidra_addr) in functions {
229
- let addr = rb(*ghidra_addr);
230
- let in_text = is_in_text(addr);
231
- if !in_text {
232
- result.check(
233
- &format!("{} prologue", name),
234
- false,
235
- &format!(
236
- "0x{:08X} (ghidra 0x{:08X}) not in .text range",
237
- addr, ghidra_addr
238
- ),
239
- );
240
- continue;
241
- }
242
- unsafe {
243
- let first_byte = read_u8(addr);
244
- let ok = valid_prologues.contains(&first_byte);
245
- result.check(
246
- &format!("{} prologue", name),
247
- ok,
248
- &format!(
249
- "0x{:08X}: first byte 0x{:02X} {}",
250
- addr,
251
- first_byte,
252
- if ok { "valid" } else { "UNEXPECTED" }
253
- ),
254
- );
255
- }
256
- }
257
- }
258
-
259
- // ---------------------------------------------------------------------------
260
- // Typed vtable slot validation (from #[vtable(...)] metadata)
261
- // ---------------------------------------------------------------------------
262
-
263
- fn validate_typed_vtable_slots(result: &mut ValidationResult) {
264
- let _ = log_validation("");
265
- let _ = log_validation("--- Typed Vtable Slot Validation (#[vtable(...)]) ---");
266
-
267
- let mut vtable_count = 0;
268
- let mut slot_count = 0;
269
-
270
- for info in openwa_core::registry::all_vtable_info() {
271
- if info.ghidra_va == 0 {
272
- continue;
273
- }
274
- vtable_count += 1;
275
- let vt_runtime = rb(info.ghidra_va);
276
-
277
- for slot in info.slots {
278
- slot_count += 1;
279
- unsafe {
280
- let slot_addr = vt_runtime + slot.index * 4;
281
- let actual = read_u32(slot_addr);
282
- let in_text = is_in_text(actual);
283
- result.check(
284
- &format!("{}::{} [slot {}]", info.class_name, slot.name, slot.index),
285
- in_text,
286
- &format!(
287
- "0x{:08X} {}",
288
- actual,
289
- if in_text {
290
- "in .text"
291
- } else {
292
- "NOT in .text — hooked or corrupt?"
293
- }
294
- ),
295
- );
296
- }
297
- }
298
- }
299
-
300
- let _ = log_validation(&format!(
301
- " Checked {} named slots across {} typed vtables",
302
- slot_count, vtable_count
303
- ));
304
- }
305
-
306
- // ---------------------------------------------------------------------------
307
- // Struct offset validation
308
- // ---------------------------------------------------------------------------
309
-
310
- fn validate_struct_offsets(result: &mut ValidationResult) {
311
- let _ = log_validation("");
312
- let _ = log_validation("--- Struct Offset Validation (offset_of!) ---");
313
-
314
- macro_rules! check_offset {
315
- ($result:expr, $struct:ty, $field:ident, $expected:expr) => {
316
- let actual = core::mem::offset_of!($struct, $field);
317
- $result.check(
318
- &format!("{}::{}", stringify!($struct), stringify!($field)),
319
- actual == $expected,
320
- &format!("expected 0x{:X}, got 0x{:X}", $expected, actual),
321
- );
322
- };
323
- }
324
-
325
- let _ = log_validation("");
326
- let _ = log_validation(" CTask:");
327
- check_offset!(result, CTask, vtable, 0x00);
328
- check_offset!(result, CTask, parent, 0x04);
329
- check_offset!(result, CTask, children_capacity, 0x08);
330
- check_offset!(result, CTask, children_data, 0x14);
331
- check_offset!(result, CTask, class_type, 0x20);
332
- check_offset!(result, CTask, shared_data, 0x24);
333
- check_offset!(result, CTask, owns_shared_data, 0x28);
334
- check_offset!(result, CTask, ddgame, 0x2C);
335
-
336
- let _ = log_validation("");
337
- let _ = log_validation(" CGameTask:");
338
- check_offset!(result, CGameTask, base, 0x00);
339
- check_offset!(result, CGameTask, pos_x, 0x84);
340
- check_offset!(result, CGameTask, pos_y, 0x88);
341
- check_offset!(result, CGameTask, speed_x, 0x90);
342
- check_offset!(result, CGameTask, speed_y, 0x94);
343
- check_offset!(result, CGameTask, sound_emitter, 0xE8);
344
-
345
- let _ = log_validation("");
346
- let _ = log_validation(" DDGame:");
347
- check_offset!(result, DDGame, landscape, 0x20);
348
- check_offset!(result, DDGame, game_info, 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, bit_grid, 0x380);
353
- check_offset!(result, DDGame, sprite_regions, 0x46C);
354
- check_offset!(result, DDGame, coord_list, 0x50C);
355
-
356
- let _ = log_validation("");
357
- let _ = log_validation(" DDGameWrapper:");
358
- check_offset!(result, DDGameWrapper, vtable, 0x00);
359
- check_offset!(result, DDGameWrapper, ddgame, 0x488);
360
- check_offset!(result, DDGameWrapper, primary_gfx_dir, 0x4C0);
361
- check_offset!(result, DDGameWrapper, landscape, 0x4CC);
362
- check_offset!(result, DDGameWrapper, display, 0x4D0);
363
-
364
- let _ = log_validation("");
365
- let _ = log_validation(" CTaskTeam:");
366
- check_offset!(result, CTaskTeam, team_index, 0x38);
367
- check_offset!(result, CTaskTeam, alive_worm_count, 0x48);
368
- check_offset!(result, CTaskTeam, last_launched_weapon, 0x60);
369
- check_offset!(result, CTaskTeam, worm_count, 0x218);
370
- check_offset!(result, CTaskTeam, pos_x, 0x404);
371
- check_offset!(result, CTaskTeam, pos_y, 0x408);
372
-
373
- let _ = log_validation("");
374
- let _ = log_validation(" TurnGameCtx (embedded at CTaskTurnGame+0x30):");
375
- check_offset!(result, TurnGameCtx, land_height, 0x10);
376
- check_offset!(result, TurnGameCtx, land_height_2, 0x14);
377
- check_offset!(result, TurnGameCtx, _sentinel_18, 0x18);
378
- check_offset!(result, TurnGameCtx, _sentinel_28, 0x28);
379
- check_offset!(result, TurnGameCtx, _sentinel_38, 0x38);
380
- check_offset!(result, TurnGameCtx, team_count, 0x4C);
381
- check_offset!(result, TurnGameCtx, _slot_d0, 0xA0);
382
- check_offset!(result, TurnGameCtx, _hud_textbox_a, 0xA4);
383
- check_offset!(result, TurnGameCtx, _hud_textbox_b, 0xA8);
384
-
385
- let _ = log_validation("");
386
- let _ = log_validation(" CTaskMissile:");
387
- check_offset!(result, CTaskMissile, base, 0x00);
388
- check_offset!(result, CTaskMissile, slot_id, 0x12C);
389
- check_offset!(result, CTaskMissile, spawn_params, 0x130);
390
- check_offset!(result, CTaskMissile, weapon_data, 0x15C);
391
- check_offset!(result, CTaskMissile, render_data, 0x2D4);
392
- check_offset!(result, CTaskMissile, launch_speed_raw, 0x3A0);
393
- check_offset!(result, CTaskMissile, homing_enabled, 0x3A8);
394
- check_offset!(result, CTaskMissile, direction, 0x3C8);
395
-
396
- let _ = log_validation(" CTaskTurnGame:");
397
- check_offset!(result, CTaskTurnGame, game_ctx, 0x30);
398
- check_offset!(result, CTaskTurnGame, worm_active, 0x108);
399
- check_offset!(result, CTaskTurnGame, current_team, 0x12C);
400
- check_offset!(result, CTaskTurnGame, current_worm, 0x130);
401
- check_offset!(result, CTaskTurnGame, arena_team, 0x134);
402
- check_offset!(result, CTaskTurnGame, arena_worm, 0x138);
403
- check_offset!(result, CTaskTurnGame, turn_ended, 0x150);
404
- check_offset!(result, CTaskTurnGame, no_time_limit, 0x154);
405
- check_offset!(result, CTaskTurnGame, retreat_timer, 0x178);
406
- check_offset!(result, CTaskTurnGame, retreat_time_max, 0x17C);
407
- check_offset!(result, CTaskTurnGame, idle_timer, 0x184);
408
- check_offset!(result, CTaskTurnGame, turn_timer_display, 0x188);
409
- check_offset!(result, CTaskTurnGame, turn_timer, 0x18C);
410
- check_offset!(result, CTaskTurnGame, active_worm_frames, 0x2D4);
411
- check_offset!(result, CTaskTurnGame, retreat_frames, 0x2D8);
412
- check_offset!(result, CTaskTurnGame, _timer_scale, 0x2DC);
413
- }
414
-
415
- // ---------------------------------------------------------------------------
416
- // Deferred global validation
417
- // ---------------------------------------------------------------------------
418
-
419
- fn deferred_global_validation() {
420
- let _ = log_validation("");
421
- let _ = log_validation("--- Deferred Global Validation (10s after load) ---");
422
-
423
- let mut result = ValidationResult::new();
424
-
425
- unsafe {
426
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
427
- let _ = log_validation(&format!(" g_GameSession = 0x{:08X}", session_ptr));
428
-
429
- if session_ptr == 0 {
430
- let _ = log_validation(" Game session not initialized yet — no game started?");
431
- return;
432
- }
433
-
434
- let wrapper_addr = read_u32(session_ptr + 0xA0);
435
- let _ = log_validation(&format!(" DDGameWrapper = 0x{:08X}", wrapper_addr));
436
-
437
- if wrapper_addr == 0 {
438
- let _ = log_validation(" DDGameWrapper not created — need to start a game first.");
439
- return;
440
- }
441
-
442
- let vtable_ptr = read_u32(wrapper_addr);
443
- let expected_vt = rb(va::DDGAME_WRAPPER_VTABLE);
444
- result.check(
445
- "DDGameWrapper.vtable",
446
- vtable_ptr == expected_vt,
447
- &format!("expected 0x{:08X}, got 0x{:08X}", expected_vt, vtable_ptr),
448
- );
449
-
450
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
451
- let ddgame_valid = ddgame_ptr != 0;
452
- result.check(
453
- "DDGameWrapper.ddgame != NULL",
454
- ddgame_valid,
455
- &format!("0x{:08X}", ddgame_ptr),
456
- );
457
-
458
- if ddgame_valid {
459
- let ddgame_landscape = read_u32(ddgame_ptr + 0x20);
460
- let wrapper_landscape = read_u32(wrapper_addr + 0x4CC);
461
- result.check(
462
- "DDGame.landscape == wrapper.landscape",
463
- ddgame_landscape == wrapper_landscape,
464
- &format!(
465
- "DDGame+0x20=0x{:08X}, wrapper+0x4CC=0x{:08X}",
466
- ddgame_landscape, wrapper_landscape
467
- ),
468
- );
469
-
470
- let display_gfx = read_u32(ddgame_ptr + 0x138);
471
- if display_gfx != 0 {
472
- let dgfx_vt = read_u32(display_gfx);
473
- let expected = rb(va::DISPLAY_GFX_VTABLE);
474
- result.check(
475
- "DDGame.display_gfx vtable",
476
- dgfx_vt == expected,
477
- &format!("expected 0x{:08X}, got 0x{:08X}", expected, dgfx_vt),
478
- );
479
- } else {
480
- result.check("DDGame.display_gfx", false, "NULL pointer");
481
- }
482
-
483
- let tsm = read_u32(ddgame_ptr + 0x380);
484
- if tsm != 0 {
485
- let tsm_vt = read_u32(tsm);
486
- let expected = rb(va::TASK_STATE_MACHINE_VTABLE);
487
- result.check(
488
- "DDGame.bit_grid vtable",
489
- tsm_vt == expected,
490
- &format!("expected 0x{:08X}, got 0x{:08X}", expected, tsm_vt),
491
- );
492
- } else {
493
- result.check("DDGame.bit_grid", false, "NULL pointer");
494
- }
495
- }
496
-
497
- let landscape = read_u32(wrapper_addr + 0x4CC);
498
- if landscape != 0 {
499
- let land_vt = read_u32(landscape);
500
- let expected = rb(va::PC_LANDSCAPE_VTABLE);
501
- result.check(
502
- "wrapper.landscape vtable",
503
- land_vt == expected,
504
- &format!("expected 0x{:08X}, got 0x{:08X}", expected, land_vt),
505
- );
506
- } else {
507
- result.check("wrapper.landscape", false, "NULL pointer");
508
- }
509
- }
510
-
511
- let _ = log_validation(&format!(" Deferred {}", result.summary_line()));
512
- }
513
-
514
- // ---------------------------------------------------------------------------
515
- // Team block dump
516
- // ---------------------------------------------------------------------------
517
-
518
- fn dump_team_blocks() {
519
- use openwa_core::engine::ddgame::{offsets, TeamArenaRef};
520
-
521
- let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
522
- let _ = log_validation("");
523
- let _ = log_validation(&format!("--- Team Block Dump #{} ---", dump_num));
524
-
525
- unsafe {
526
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
527
- if session_ptr == 0 {
528
- let _ = log_validation(" No game session — skipping.");
529
- return;
530
- }
531
-
532
- let wrapper_addr = read_u32(session_ptr + 0xA0);
533
- if wrapper_addr == 0 {
534
- let _ = log_validation(" No DDGameWrapper.");
535
- return;
536
- }
537
-
538
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
539
- if ddgame_ptr == 0 {
540
- let _ = log_validation(" No DDGame.");
541
- return;
542
- }
543
-
544
- let _ = log_validation(&format!(" DDGame = 0x{:08X}", ddgame_ptr));
545
-
546
- let arena_base = ddgame_ptr + offsets::TEAM_ARENA_STATE as u32;
547
- let arena = TeamArenaRef::from_raw(arena_base);
548
- let tws = arena.state();
549
- let tws_base = arena_base as *const u8;
550
- let _ = log_validation(&format!(
551
- " team_count = {} (TeamArenaState.team_count)",
552
- tws.team_count
553
- ));
554
-
555
- let blocks = arena.blocks();
556
- let blocks_addr = blocks as u32;
557
- let _ = log_validation(&format!(
558
- " blocks_base = 0x{:08X} (DDGame+0x{:X})",
559
- blocks_addr,
560
- blocks_addr - ddgame_ptr
561
- ));
562
-
563
- let mut result = ValidationResult::new();
564
-
565
- let expected_blocks = ddgame_ptr + offsets::TEAM_BLOCKS as u32;
566
- result.check(
567
- "ARENA_TO_BLOCKS derivation",
568
- blocks_addr == expected_blocks,
569
- &format!(
570
- "got 0x{:08X}, expected 0x{:08X}",
571
- blocks_addr, expected_blocks
572
- ),
573
- );
574
-
575
- let num_blocks = (tws.team_count as u32 + 1).max(3).min(7);
576
- for b in 0..num_blocks {
577
- let block = &*blocks.add(b as usize);
578
- let _ = log_validation(&format!(
579
- "\n === Block {} (0x{:08X}) ===",
580
- b,
581
- blocks_addr + b * 0x51C
582
- ));
583
-
584
- if (b + 1) < 7 {
585
- let header = &(*blocks.add(b as usize + 1)).header.team;
586
- let worm_count = header.worm_count;
587
- let eliminated = header.eliminated;
588
-
589
- let entry_ptr = tws_base.add(b as usize * 0x51C);
590
- let raw_worm_count = *(entry_ptr.sub(4) as *const i32);
591
- let raw_alliance = *(entry_ptr.add(4) as *const i32);
592
-
593
- result.check(
594
- &format!("block[{}] header.worm_count vs raw", b),
595
- worm_count == raw_worm_count,
596
- &format!("struct={}, raw={}", worm_count, raw_worm_count),
597
- );
598
-
599
- let _ = log_validation(&format!(
600
- " header: worm_count={}, eliminated={}, alliance(entry_ptr+4)={}",
601
- worm_count, eliminated, raw_alliance
602
- ));
603
-
604
- for w in 0..8usize {
605
- let worm = if w == 0 {
606
- &*block.header.worm
607
- } else {
608
- &block.worms[w - 1]
609
- };
610
- let active = worm.active_flag;
611
- let name_bytes = &worm.name;
612
- let name_len = name_bytes
613
- .iter()
614
- .position(|&c| c == 0)
615
- .unwrap_or(name_bytes.len());
616
- let name_str = core::str::from_utf8(&name_bytes[..name_len]).unwrap_or("?");
617
-
618
- if worm.state != 0 || worm.health != 0 || active != 0 || w == 0 {
619
- let _ = log_validation(&format!(
620
- " worm[{}]: state=0x{:04X} active={} max_hp={} hp={} name=\"{}\"",
621
- w, worm.state, active, worm.max_health, worm.health, name_str
622
- ));
623
- }
624
- }
625
-
626
- if worm_count > 0 && worm_count <= 8 {
627
- let struct_total: i32 = (1..=worm_count as usize)
628
- .map(|w| arena.team_worm(b as usize, w).health)
629
- .sum();
630
-
631
- let raw_health_ptr = entry_ptr.sub(0x4A0) as *const i32;
632
- let mut raw_total = 0i32;
633
- for i in 0..worm_count {
634
- raw_total += *raw_health_ptr.add(i as usize * (0x9C / 4));
635
- }
636
-
637
- result.check(
638
- &format!("block[{}] health sum (struct vs raw)", b),
639
- struct_total == raw_total,
640
- &format!("struct={}, raw={}", struct_total, raw_total),
641
- );
642
- }
643
-
644
- if worm_count > 0 {
645
- let struct_state = block.worms[0].state;
646
- let raw_state = *(entry_ptr.sub(0x598).add(0x9C) as *const u32);
647
- result.check(
648
- &format!("block[{}] worm[1].state (struct vs raw)", b),
649
- struct_state == raw_state,
650
- &format!("struct=0x{:X}, raw=0x{:X}", struct_state, raw_state),
651
- );
652
-
653
- let _ = log_validation(" CheckWormState0x64 field check (state vs health):");
654
- for w in 1..=worm_count as usize {
655
- let worm = arena.team_worm(b as usize, w);
656
- let _ = log_validation(&format!(
657
- " worm[{}]: state=0x{:04X}({}) health=0x{:04X}({})",
658
- w, worm.state, worm.state, worm.health, worm.health
659
- ));
660
- }
661
- }
662
- }
663
- }
664
-
665
- let _ = log_validation(&format!("\n Struct Validation {}", result.summary_line()));
666
-
667
- // === TeamArenaState memory layout dump ===
668
- // Dump the region around team entries to understand the actual layout.
669
- // Each "team entry" has stride 0x51C. We dump the first 16 bytes of each
670
- // slot (including slot 6 which goes "out of bounds"), plus the area
671
- // around weapon_slots start (0x1EB4) and team_count (0x1EB0).
672
- let _ = log_validation("\n === TeamArenaState Layout Dump ===");
673
- let _ = log_validation(&format!(
674
- " arena_base = 0x{:08X} (DDGame + 0x{:X})",
675
- arena_base,
676
- offsets::TEAM_ARENA_STATE
677
- ));
678
-
679
- // Dump first 16 bytes at each team_index * 0x51C stride (slots -2 to 7)
680
- for slot in -2i32..8 {
681
- let offset = slot as isize * 0x51C_isize;
682
- let ptr = tws_base.offset(offset);
683
- let mut hex = String::new();
684
- for i in 0..16usize {
685
- if i > 0 && i % 4 == 0 {
686
- hex.push(' ');
687
- }
688
- hex.push_str(&format!("{:02X}", *ptr.add(i)));
689
- }
690
- let abs_addr = (arena_base as isize + offset) as u32;
691
- let label = match slot {
692
- -2 => "team[-2]".to_string(),
693
- -1 => "team[-1]".to_string(),
694
- 0..=5 => format!("team[{}]", slot),
695
- 6 => "team[6]".to_string(),
696
- _ => "team[7]".to_string(),
697
- };
698
- let _ = log_validation(&format!(
699
- " {:+06X} (0x{:08X}) {}: {} val_at+4={}",
700
- offset,
701
- abs_addr,
702
- label,
703
- hex,
704
- *(ptr.add(4) as *const i32)
705
- ));
706
- }
707
-
708
- // Dump the region around 0x1EA0-0x1EC0 (team entries end / weapon_slots start)
709
- let _ = log_validation("\n --- Region 0x1EA0..0x1EC0 (boundary area) ---");
710
- for row in (0x1EA0..0x1EC0).step_by(16) {
711
- let ptr = tws_base.add(row);
712
- let mut hex = String::new();
713
- for i in 0..16usize {
714
- if i > 0 && i % 4 == 0 {
715
- hex.push(' ');
716
- }
717
- hex.push_str(&format!("{:02X}", *ptr.add(i)));
718
- }
719
- // Label known offsets
720
- let label = match row {
721
- 0x1EA0 => " (end of teams[5])",
722
- 0x1EB0 => " (team_count + weapon_slots[0..3])",
723
- _ => "",
724
- };
725
- let _ = log_validation(&format!(" +0x{:04X}: {}{}", row, hex, label));
726
- }
727
-
728
- // Dump team_count and first few weapon_slots
729
- let _ = log_validation(&format!(
730
- "\n team_count (+0x1EB0) = {}",
731
- *(tws_base.add(0x1EB0) as *const i32)
732
- ));
733
- let _ = log_validation(&format!(
734
- " weapon_slots[0..4] (+0x1EB4) = [{}, {}, {}, {}]",
735
- *(tws_base.add(0x1EB4) as *const i32),
736
- *(tws_base.add(0x1EB8) as *const i32),
737
- *(tws_base.add(0x1EBC) as *const i32),
738
- *(tws_base.add(0x1EC0) as *const i32)
739
- ));
740
-
741
- // Dump game_mode_flag and game_phase for context
742
- let _ = log_validation(&format!(
743
- " game_mode_flag (+0x2C0C) = {}",
744
- *(tws_base.add(0x2C0C) as *const i32)
745
- ));
746
- let _ = log_validation(&format!(
747
- " game_phase (+0x2C28) = {}",
748
- *(tws_base.add(0x2C28) as *const i32)
749
- ));
750
- }
751
- }
752
-
753
- // ---------------------------------------------------------------------------
754
- // PCLandscape dump
755
- // ---------------------------------------------------------------------------
756
-
757
- fn dump_landscape() {
758
- use openwa_core::render::PCLandscape;
759
-
760
- let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
761
- let _ = log_validation("");
762
- let _ = log_validation(&format!("--- PCLandscape Dump #{} ---", dump_num));
763
-
764
- unsafe {
765
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
766
- if session_ptr == 0 {
767
- let _ = log_validation(" No game session — skipping.");
768
- return;
769
- }
770
-
771
- let wrapper_addr = read_u32(session_ptr + 0xA0);
772
- if wrapper_addr == 0 {
773
- let _ = log_validation(" No DDGameWrapper.");
774
- return;
775
- }
776
-
777
- let landscape_ptr = read_u32(wrapper_addr + 0x4CC);
778
- if landscape_ptr == 0 {
779
- let _ = log_validation(" PCLandscape is NULL.");
780
- return;
781
- }
782
-
783
- let land = &*(landscape_ptr as *const PCLandscape);
784
- let _ = log_validation(&format!(" PCLandscape @ 0x{:08X}", landscape_ptr));
785
-
786
- let expected_vt = rb(va::PC_LANDSCAPE_VTABLE);
787
- let vt_ok = land.vtable as u32 == expected_vt;
788
- let _ = log_validation(&format!(
789
- " vtable: 0x{:08X} (expected 0x{:08X}) {}",
790
- land.vtable as u32,
791
- expected_vt,
792
- if vt_ok { "OK" } else { "MISMATCH" }
793
- ));
794
-
795
- let _ = log_validation(&format!(" ddgame: 0x{:08X}", land.ddgame as usize));
796
- let _ = log_validation(&format!(
797
- " _unknown_900: 0x{:08X}",
798
- land._unknown_900 as u32
799
- ));
800
- let _ = log_validation(&format!(
801
- " collision_bitmap: 0x{:08X}",
802
- land.collision_bitmap as u32
803
- ));
804
- let _ = log_validation(&format!(" initialized: {}", land.initialized));
805
-
806
- let primary_count = land
807
- .crater_sprites
808
- .iter()
809
- .filter(|&&p| !p.is_null())
810
- .count();
811
- let secondary_count = land
812
- .crater_sprites_secondary
813
- .iter()
814
- .filter(|&&p| !p.is_null())
815
- .count();
816
- let _ = log_validation(&format!(
817
- " crater_sprites: {}/16 non-null, secondary: {}/16 non-null",
818
- primary_count, secondary_count
819
- ));
820
-
821
- let _ = log_validation(&format!(" layer_0: 0x{:08X}", land.layer_0 as u32));
822
- let _ = log_validation(&format!(" layer_1: 0x{:08X}", land.layer_1 as u32));
823
- let _ = log_validation(&format!(
824
- " layer_terrain: 0x{:08X}",
825
- land.layer_terrain as u32
826
- ));
827
- let _ = log_validation(&format!(" layer_edges: 0x{:08X}", land.layer_edges as u32));
828
- let _ = log_validation(&format!(
829
- " layer_shadow: 0x{:08X}",
830
- land.layer_shadow as u32
831
- ));
832
- let _ = log_validation(&format!(" layer_5: 0x{:08X}", land.layer_5 as u32));
833
-
834
- if !land.layer_terrain.is_null() {
835
- let dgfx = land.layer_terrain as u32;
836
- let pixel_data = read_u32(dgfx + 0x08);
837
- let stride = read_u32(dgfx + 0x10);
838
- let width = read_u32(dgfx + 0x14);
839
- let height = read_u32(dgfx + 0x18);
840
- let _ = log_validation(&format!(
841
- " layer_terrain DisplayGfx: pixels=0x{:08X} stride={} width={} height={}",
842
- pixel_data, stride, width, height
843
- ));
844
- }
845
-
846
- let _ = log_validation(&format!(" dirty_rect_count: {}", land.dirty_rect_count));
847
- let _ = log_validation(&format!(" dirty_flag: {}", land.dirty_flag));
848
- if land.dirty_rect_count > 0 && land.dirty_rect_count <= 256 {
849
- for i in 0..land.dirty_rect_count.min(5) as usize {
850
- let r = &land.dirty_rects[i];
851
- let _ = log_validation(&format!(
852
- " rect[{}]: ({},{})..({},{})",
853
- i, r.x1, r.y1, r.x2, r.y2
854
- ));
855
- }
856
- if land.dirty_rect_count > 5 {
857
- let _ = log_validation(&format!(" ... and {} more", land.dirty_rect_count - 5));
858
- }
859
- }
860
-
861
- let _ = log_validation(&format!(" _unknown_8ec: 0x{:08X}", land._unknown_8ec));
862
- let _ = log_validation(&format!(" _unknown_8f0: 0x{:08X}", land._unknown_8f0));
863
- let _ = log_validation(&format!(" _unknown_8f4: 0x{:08X}", land._unknown_8f4));
864
- let _ = log_validation(&format!(
865
- " resource_handle: 0x{:08X}",
866
- land.resource_handle as u32
867
- ));
868
- let _ = log_validation(&format!(
869
- " level_gfx_dir: 0x{:08X}",
870
- land.level_gfx_dir as u32
871
- ));
872
- let _ = log_validation(&format!(
873
- " water_gfx_dir: 0x{:08X}",
874
- land.water_gfx_dir as u32
875
- ));
876
- let _ = log_validation(&format!(
877
- " visible_bounds: left={} top={} right={} bottom={}",
878
- land.visible_left, land.visible_top, land.visible_right, land.visible_bottom
879
- ));
880
-
881
- let level_path = std::ffi::CStr::from_ptr(land.level_dir_path.as_ptr() as *const i8);
882
- let theme_path = std::ffi::CStr::from_ptr(land.theme_dir_path.as_ptr() as *const i8);
883
- let _ = log_validation(&format!(" level_dir_path: {:?}", level_path));
884
- let _ = log_validation(&format!(" theme_dir_path: {:?}", theme_path));
885
-
886
- if !land.ddgame.is_null() {
887
- let dg = &*land.ddgame;
888
- let _ = log_validation(&format!(
889
- " DDGame level dims: {}x{} (total={})",
890
- dg.level_width, dg.level_height, dg.level_total_pixels
891
- ));
892
- }
893
-
894
- let _ = log_validation(" Vtable slots:");
895
- let vt_addr = land.vtable as u32;
896
- for slot in 0..10u32 {
897
- let entry = read_u32(vt_addr + slot * 4);
898
- let in_text = is_in_text(entry);
899
- let _ = log_validation(&format!(
900
- " [{}]: 0x{:08X} {}",
901
- slot,
902
- entry,
903
- if in_text { "" } else { "(NOT .text)" }
904
- ));
905
- }
906
-
907
- let _ = log_validation(&format!(" _unknown_b3c: 0x{:08X}", land._unknown_b3c));
908
-
909
- let _ = log_validation(" _unknown_088 (0x44 bytes):");
910
- log_hex_dump(&land._unknown_088);
911
- let _ = log_validation(" _unknown_8d9 (0x13 bytes):");
912
- log_hex_dump(&land._unknown_8d9);
913
- let _ = log_validation(" _unknown_8f8 (8 bytes):");
914
- log_hex_dump(&land._unknown_8f8);
915
- let _ = log_validation(" _unknown_918 (4 bytes):");
916
- log_hex_dump(&land._unknown_918);
917
- }
918
- }
919
-
920
- fn log_hex_dump(data: &[u8]) {
921
- for chunk in data.chunks(16) {
922
- let hex: String = chunk
923
- .iter()
924
- .map(|b| format!("{:02X}", b))
925
- .collect::<Vec<_>>()
926
- .join(" ");
927
- let ascii: String = chunk
928
- .iter()
929
- .map(|&b| {
930
- if (0x20..0x7F).contains(&b) {
931
- b as char
932
- } else {
933
- '.'
934
- }
935
- })
936
- .collect();
937
- let _ = log_validation(&format!(" {} | {}", hex, ascii));
938
- }
939
- }
940
-
941
- // ---------------------------------------------------------------------------
942
- // Entity census — enumerate all registered task types via shared_data table
943
- // ---------------------------------------------------------------------------
944
-
945
- fn dump_entity_census() {
946
- let _ = log_validation("");
947
- let _ = log_validation("--- Entity Census (SharedData hash table) ---");
948
-
949
- unsafe {
950
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
951
- if session_ptr == 0 {
952
- let _ = log_validation(" No game session — skipping.");
953
- return;
954
- }
955
- let wrapper_addr = read_u32(session_ptr + 0xA0);
956
- if wrapper_addr == 0 {
957
- let _ = log_validation(" No DDGameWrapper.");
958
- return;
959
- }
960
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
961
- if ddgame_ptr == 0 {
962
- let _ = log_validation(" No DDGame.");
963
- return;
964
- }
965
-
966
- let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
967
- if task_land_ptr == 0 {
968
- let _ = log_validation(" CTaskLand NULL — game not loaded yet.");
969
- return;
970
- }
971
-
972
- let task_land = task_land_ptr as *const CTask;
973
- let shared_data_ptr = (*task_land).shared_data;
974
- if shared_data_ptr.is_null() {
975
- let _ = log_validation(" shared_data is NULL.");
976
- return;
977
- }
978
-
979
- let table = SharedDataTable::from_ptr(shared_data_ptr);
980
-
981
- // Use the global address registry for vtable → class name lookup
982
-
983
- // Collect (ghidra_va, entity_ptr) for every node.
984
- let delta = rb(va::IMAGE_BASE).wrapping_sub(va::IMAGE_BASE);
985
- let mut entries: Vec<(u32, u32)> = Vec::new();
986
- let mut total = 0u32;
987
-
988
- for node in table.iter() {
989
- let entity = (*node).entity;
990
- if entity.is_null() {
991
- continue;
992
- }
993
- let vtable_runtime = read_u32(entity as u32);
994
- if !is_in_rdata(vtable_runtime) {
995
- continue;
996
- }
997
- let vtable_ghidra = vtable_runtime.wrapping_sub(delta);
998
- entries.push((vtable_ghidra, entity as u32));
999
- total += 1;
1000
- }
1001
-
1002
- let _ = log_validation(&format!(" Total entities: {}", total));
1003
-
1004
- // Group by vtable, sort by count descending.
1005
- let mut groups: Vec<(u32, Vec<u32>)> = Vec::new();
1006
- for (vt, ptr) in &entries {
1007
- if let Some(g) = groups.iter_mut().find(|(v, _)| *v == *vt) {
1008
- g.1.push(*ptr);
1009
- } else {
1010
- groups.push((*vt, vec![*ptr]));
1011
- }
1012
- }
1013
- groups.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
1014
-
1015
- for (vt_ghidra, ptrs) in &groups {
1016
- let name = openwa_core::registry::vtable_class_name(*vt_ghidra).unwrap_or("UNKNOWN");
1017
- let _ = log_validation(&format!(
1018
- " {:>3}x {:<20} (vtable 0x{:08X})",
1019
- ptrs.len(),
1020
- name,
1021
- vt_ghidra
1022
- ));
1023
- for &ptr in ptrs.iter().take(8) {
1024
- use openwa_core::task::CTaskWorm;
1025
- if *vt_ghidra == va::CTASK_WORM_VTABLE {
1026
- let w = &*(ptr as *const CTaskWorm);
1027
- let nlen = w
1028
- .worm_name
1029
- .iter()
1030
- .position(|&c| c == 0)
1031
- .unwrap_or(w.worm_name.len());
1032
- let wname = core::str::from_utf8(&w.worm_name[..nlen]).unwrap_or("?");
1033
- let _ = log_validation(&format!(
1034
- " @ 0x{:08X} team={} idx={} state=0x{:02X} name=\"{}\"",
1035
- ptr,
1036
- w.team_index,
1037
- w.worm_index,
1038
- w.state(),
1039
- wname
1040
- ));
1041
- } else {
1042
- let a = read_u32(ptr + 4);
1043
- let b = read_u32(ptr + 8);
1044
- let c = read_u32(ptr + 12);
1045
- let d = read_u32(ptr + 16);
1046
- let _ = log_validation(&format!(
1047
- " @ 0x{:08X} [+4:{:08X} +8:{:08X} +c:{:08X} +10:{:08X}]",
1048
- ptr, a, b, c, d
1049
- ));
1050
- }
1051
- }
1052
- }
1053
- }
1054
- }
1055
-
1056
- // ---------------------------------------------------------------------------
1057
- // CTaskWorm entity dump
1058
- // ---------------------------------------------------------------------------
1059
-
1060
- fn dump_worm_tasks() {
1061
- use openwa_core::task::CTaskWorm;
1062
-
1063
- let _ = log_validation("");
1064
- let _ = log_validation("--- CTaskWorm Entity Dump ---");
1065
-
1066
- unsafe {
1067
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1068
- if session_ptr == 0 {
1069
- let _ = log_validation(" No game session — skipping.");
1070
- return;
1071
- }
1072
-
1073
- let wrapper_addr = read_u32(session_ptr + 0xA0);
1074
- if wrapper_addr == 0 {
1075
- let _ = log_validation(" No DDGameWrapper.");
1076
- return;
1077
- }
1078
-
1079
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1080
- if ddgame_ptr == 0 {
1081
- let _ = log_validation(" No DDGame.");
1082
- return;
1083
- }
1084
-
1085
- // DDGame+0x54C = CTaskLand* — always present once map is loaded.
1086
- let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1087
- if task_land_ptr == 0 {
1088
- let _ = log_validation(" CTaskLand is NULL — game not fully loaded yet.");
1089
- return;
1090
- }
1091
- let _ = log_validation(&format!(" DDGame @ 0x{:08X}", ddgame_ptr));
1092
- let _ = log_validation(&format!(" CTaskLand @ 0x{:08X}", task_land_ptr));
1093
-
1094
- // CTask.shared_data at +0x24 — root tasks own 0x420 bytes with a
1095
- // 256-bucket hash table (FUN_005406a0). All tasks in the same game
1096
- // tree share this block via the inherited shared_data pointer.
1097
- let task_land = task_land_ptr as *const CTask;
1098
- let shared_data_ptr = (*task_land).shared_data;
1099
- if shared_data_ptr.is_null() {
1100
- let _ = log_validation(" shared_data is NULL.");
1101
- return;
1102
- }
1103
- let _ = log_validation(&format!(" shared_data @ 0x{:08X}", shared_data_ptr as u32));
1104
-
1105
- let table = SharedDataTable::from_ptr(shared_data_ptr);
1106
-
1107
- let expected_vtable = rb(va::CTASK_WORM_VTABLE) as *const u8;
1108
- let _ = log_validation(&format!(
1109
- " Expected CTaskWorm vtable: 0x{:08X}",
1110
- expected_vtable as u32
1111
- ));
1112
-
1113
- let mut worm_count = 0u32;
1114
-
1115
- for node in table.iter() {
1116
- let candidate = (*node).entity;
1117
- if candidate.is_null() {
1118
- continue;
1119
- }
1120
- // Vtable is the first pointer in the object; must be in .rdata.
1121
- let vtable = read_u32(candidate as u32) as *const u8;
1122
- if !is_in_rdata(vtable as u32) || vtable != expected_vtable {
1123
- continue;
1124
- }
1125
-
1126
- worm_count += 1;
1127
- let worm = &*(candidate as *const CTaskWorm);
1128
-
1129
- let state = worm.state();
1130
- let pos_x_f = worm.base.pos_x.0 as f32 / 65536.0;
1131
- let pos_y_f = worm.base.pos_y.0 as f32 / 65536.0;
1132
- let team_idx = worm.team_index;
1133
- let worm_idx = worm.worm_index;
1134
- let slot_id = worm.slot_id;
1135
-
1136
- let name_len = worm
1137
- .worm_name
1138
- .iter()
1139
- .position(|&c| c == 0)
1140
- .unwrap_or(worm.worm_name.len());
1141
- let name = core::str::from_utf8(&worm.worm_name[..name_len]).unwrap_or("?");
1142
-
1143
- let _ = log_validation(&format!(
1144
- " worm#{} @ 0x{:08X}: team={} worm_idx={} slot={} state=0x{:04X} pos=({:.1},{:.1}) name=\"{}\"",
1145
- worm_count, candidate as u32,
1146
- team_idx, worm_idx, slot_id,
1147
- state, pos_x_f, pos_y_f, name
1148
- ));
1149
-
1150
- // Cross-validate against WormEntry in TeamArena.
1151
- // WormEntry = DDGame+0x4090 + team*0x51C + worm_idx*0x9C
1152
- // +0x00: state, +0x78: name (17 bytes)
1153
- if team_idx < 6 && worm_idx < 8 {
1154
- let entry_addr = ddgame_ptr + 0x4090 + team_idx * 0x51C + worm_idx * 0x9C;
1155
- let entry_state = read_u32(entry_addr);
1156
- let mut entry_name = [0u8; 17];
1157
- for i in 0..17usize {
1158
- entry_name[i] = read_u8(entry_addr + 0x78 + i as u32);
1159
- if entry_name[i] == 0 {
1160
- break;
1161
- }
1162
- }
1163
- let entry_name_len = entry_name.iter().position(|&c| c == 0).unwrap_or(17);
1164
- let entry_name_str =
1165
- core::str::from_utf8(&entry_name[..entry_name_len]).unwrap_or("?");
1166
-
1167
- let state_msg = if state == entry_state {
1168
- "state:OK".to_string()
1169
- } else {
1170
- format!("state:MISMATCH(entry=0x{:04X})", entry_state)
1171
- };
1172
- let name_msg = if name == entry_name_str {
1173
- "name:OK".to_string()
1174
- } else {
1175
- format!("name:MISMATCH(entry=\"{}\")", entry_name_str)
1176
- };
1177
- let _ = log_validation(&format!(
1178
- " xcheck WormEntry[{},{}] @ 0x{:08X}: {} {}",
1179
- team_idx, worm_idx, entry_addr, state_msg, name_msg
1180
- ));
1181
- }
1182
- }
1183
-
1184
- let _ = log_validation(&format!(" Total CTaskWorm entities found: {}", worm_count));
1185
- }
1186
- }
1187
-
1188
- // ---------------------------------------------------------------------------
1189
- // CTaskTurnGame dump
1190
- // ---------------------------------------------------------------------------
1191
-
1192
- fn dump_turngame() {
1193
- use crate::replacements::debug_utils::dump_region;
1194
- use openwa_core::task::CTaskTurnGame;
1195
-
1196
- let _ = log_validation("");
1197
- let _ = log_validation("--- CTaskTurnGame Dump ---");
1198
-
1199
- unsafe {
1200
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1201
- if session_ptr == 0 {
1202
- let _ = log_validation(" No game session — skipping.");
1203
- return;
1204
- }
1205
- let wrapper_addr = read_u32(session_ptr + 0xA0);
1206
- if wrapper_addr == 0 {
1207
- let _ = log_validation(" No DDGameWrapper.");
1208
- return;
1209
- }
1210
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1211
- if ddgame_ptr == 0 {
1212
- let _ = log_validation(" No DDGame.");
1213
- return;
1214
- }
1215
-
1216
- // Find CTaskTurnGame via the shared_data entity table.
1217
- let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1218
- if task_land_ptr == 0 {
1219
- let _ = log_validation(" CTaskLand NULL — skipping.");
1220
- return;
1221
- }
1222
- let task_land = task_land_ptr as *const openwa_core::task::CTask;
1223
- let shared_data_ptr = (*task_land).shared_data;
1224
- if shared_data_ptr.is_null() {
1225
- let _ = log_validation(" shared_data NULL.");
1226
- return;
1227
- }
1228
-
1229
- let table = SharedDataTable::from_ptr(shared_data_ptr);
1230
- let expected_vt = rb(va::CTASK_TURN_GAME_VTABLE);
1231
- let mut tg_ptr: u32 = 0;
1232
- for node in table.iter() {
1233
- let entity = (*node).entity;
1234
- if entity.is_null() {
1235
- continue;
1236
- }
1237
- if !is_in_rdata(read_u32(entity as u32)) {
1238
- continue;
1239
- }
1240
- if read_u32(entity as u32) == expected_vt {
1241
- tg_ptr = entity as u32;
1242
- break;
1243
- }
1244
- }
1245
-
1246
- if tg_ptr == 0 {
1247
- let _ = log_validation(" CTaskTurnGame not found in entity table.");
1248
- return;
1249
- }
1250
-
1251
- let _ = log_validation(&format!(" CTaskTurnGame @ 0x{:08X}", tg_ptr));
1252
-
1253
- // Print known named fields for cross-validation.
1254
- let tg = &*(tg_ptr as *const CTaskTurnGame);
1255
- let ctx = &tg.game_ctx;
1256
- let _ = log_validation(" TurnGameCtx fields:");
1257
- let _ = log_validation(&format!(
1258
- " +0x40 land_height = {} ({:.4})",
1259
- ctx.land_height.0,
1260
- ctx.land_height.to_f32()
1261
- ));
1262
- let _ = log_validation(&format!(
1263
- " +0x44 land_height_2 = {} ({:.4})",
1264
- ctx.land_height_2.0,
1265
- ctx.land_height_2.to_f32()
1266
- ));
1267
- let _ = log_validation(&format!(" +0x48 _sentinel_18 = {}", ctx._sentinel_18));
1268
- let _ = log_validation(&format!(" +0x58 _sentinel_28 = {}", ctx._sentinel_28));
1269
- let _ = log_validation(&format!(" +0x68 _sentinel_38 = {}", ctx._sentinel_38));
1270
- let _ = log_validation(&format!(" +0x7C team_count = {}", ctx.team_count));
1271
- let _ = log_validation(&format!(" +0xD0 _slot_d0 = {}", ctx._slot_d0));
1272
- let _ = log_validation(&format!(
1273
- " +0xD4 _hud_textbox_a = 0x{:08X}",
1274
- ctx._hud_textbox_a
1275
- ));
1276
- let _ = log_validation(&format!(
1277
- " +0xD8 _hud_textbox_b = 0x{:08X}",
1278
- ctx._hud_textbox_b
1279
- ));
1280
- let _ = log_validation(" Known fields:");
1281
- let _ = log_validation(&format!(
1282
- " +0x108 worm_active = {}",
1283
- tg.worm_active
1284
- ));
1285
- let _ = log_validation(&format!(
1286
- " +0x12C current_team = {} (1-based, 0=none)",
1287
- tg.current_team
1288
- ));
1289
- let _ = log_validation(&format!(
1290
- " +0x130 current_worm = {} (0-based)",
1291
- tg.current_worm
1292
- ));
1293
- let _ = log_validation(&format!(
1294
- " +0x134 arena_team = {}",
1295
- tg.arena_team
1296
- ));
1297
- let _ = log_validation(&format!(
1298
- " +0x138 arena_worm = {}",
1299
- tg.arena_worm
1300
- ));
1301
- let _ = log_validation(&format!(
1302
- " +0x150 turn_ended = {}",
1303
- tg.turn_ended
1304
- ));
1305
- let _ = log_validation(&format!(
1306
- " +0x154 no_time_limit = {}",
1307
- tg.no_time_limit
1308
- ));
1309
- let _ = log_validation(&format!(
1310
- " +0x178 retreat_timer = {} ms",
1311
- tg.retreat_timer
1312
- ));
1313
- let _ = log_validation(&format!(
1314
- " +0x17C retreat_time_max = {} ms",
1315
- tg.retreat_time_max
1316
- ));
1317
- let _ = log_validation(&format!(
1318
- " +0x184 idle_timer = {} ms",
1319
- tg.idle_timer
1320
- ));
1321
- let _ = log_validation(&format!(
1322
- " +0x188 turn_timer_display = {} ms",
1323
- tg.turn_timer_display
1324
- ));
1325
- let _ = log_validation(&format!(
1326
- " +0x18C turn_timer = {} ms",
1327
- tg.turn_timer
1328
- ));
1329
- let _ = log_validation(&format!(
1330
- " +0x2D4 active_worm_frames = {}",
1331
- tg.active_worm_frames
1332
- ));
1333
- let _ = log_validation(&format!(
1334
- " +0x2D8 retreat_frames = {}",
1335
- tg.retreat_frames
1336
- ));
1337
- let _ = log_validation(&format!(
1338
- " +0x2DC _timer_scale = {}",
1339
- tg._timer_scale
1340
- ));
1341
-
1342
- // Full memory dump — classify every DWORD to discover unknown fields.
1343
- // Dump in chunks to keep log lines manageable.
1344
- let base = tg_ptr as *const u8;
1345
- dump_region(base, 0x00, 0x30, "CTaskTurnGame"); // CTask base
1346
- dump_region(base, 0x30, 0x38, "TurnGameCtx"); // 0x30..0x67 (vtable + sentinels)
1347
- dump_region(base, 0x68, 0x74, "TurnGameCtx"); // 0x68..0xDB (team_count + unknowns)
1348
- dump_region(base, 0xDC, 0x80, "CTaskTurnGame"); // 0xDC..0x15B
1349
- dump_region(base, 0x15C, 0x80, "CTaskTurnGame"); // 0x15C..0x1DB
1350
- dump_region(base, 0x1DC, 0x80, "CTaskTurnGame"); // 0x1DC..0x25B
1351
- dump_region(base, 0x25C, 0x84, "CTaskTurnGame"); // 0x25C..0x2DF
1352
- }
1353
- }
1354
-
1355
- // ---------------------------------------------------------------------------
1356
- // CTask children sub-struct live dump
1357
- // ---------------------------------------------------------------------------
1358
- //
1359
- // We need to confirm the layout of the four fields at CTask+0x08..+0x17:
1360
- // +0x08 children_capacity (u32, starts 0x10)
1361
- // +0x0C children_dirty (u32, flag)
1362
- // +0x10 children_watermark (u32, insertion counter)
1363
- // +0x14 children_data (*mut u8, array pointer)
1364
- //
1365
- // We dump the raw DWORDs at these offsets for a few representative tasks so
1366
- // we can verify against what WA actually stores at runtime.
1367
-
1368
- fn dump_ctask_children() {
1369
- use crate::replacements::debug_utils::dump_region;
1370
-
1371
- let _ = log_validation("");
1372
- let _ = log_validation("--- CTask children sub-struct dump ---");
1373
-
1374
- unsafe {
1375
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1376
- if session_ptr == 0 {
1377
- let _ = log_validation(" No game session.");
1378
- return;
1379
- }
1380
- let wrapper_addr = read_u32(session_ptr + 0xA0);
1381
- if wrapper_addr == 0 {
1382
- let _ = log_validation(" No DDGameWrapper.");
1383
- return;
1384
- }
1385
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1386
- if ddgame_ptr == 0 {
1387
- let _ = log_validation(" No DDGame.");
1388
- return;
1389
- }
1390
-
1391
- let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1392
- if task_land_ptr == 0 {
1393
- let _ = log_validation(" CTaskLand NULL.");
1394
- return;
1395
- }
1396
-
1397
- // Walk up to root (CTaskTurnGame)
1398
- let mut cursor = task_land_ptr;
1399
- for _ in 0..10 {
1400
- let parent = read_u32(cursor + 0x04);
1401
- if parent == 0 {
1402
- break;
1403
- }
1404
- cursor = parent;
1405
- }
1406
- let root = cursor;
1407
-
1408
- // Print the children sub-struct fields for root, then walk its first
1409
- // child (CTaskTeam level) and each of its children (CTaskFilter level).
1410
- let print_task_children = |label: &str, addr: u32| {
1411
- if addr == 0 {
1412
- return;
1413
- }
1414
- let cap = read_u32(addr + 0x08);
1415
- let dirty = read_u32(addr + 0x0C);
1416
- let wmark = read_u32(addr + 0x10);
1417
- let data = read_u32(addr + 0x14);
1418
- let _ = log_validation(&format!(
1419
- " {} @ 0x{:08X} cap={} dirty={} watermark={} data=0x{:08X}",
1420
- label, addr, cap, dirty, wmark, data
1421
- ));
1422
- // Raw dump of the 0x30-byte CTask base so we can cross-check all fields
1423
- dump_region(addr as *const u8, 0x00, 0x30, "CTask");
1424
- };
1425
-
1426
- print_task_children("root (CTaskTurnGame)", root);
1427
-
1428
- // First two children of root (CTaskTeam instances)
1429
- let root_watermark = read_u32(root + 0x10) as usize;
1430
- let root_data = read_u32(root + 0x14);
1431
- let mut teams_found = 0;
1432
- for i in 0..root_watermark.min(64) {
1433
- let child = read_u32(root_data + i as u32 * 4);
1434
- if child == 0 {
1435
- continue;
1436
- }
1437
- print_task_children(&format!(" child[{}] (CTaskTeam?)", i), child);
1438
- // And print the first few children of this child (CTaskFilter level)
1439
- let child_wmark = read_u32(child + 0x10) as usize;
1440
- let child_data = read_u32(child + 0x14);
1441
- for j in 0..child_wmark.min(16) {
1442
- let gc = read_u32(child_data + j as u32 * 4);
1443
- if gc == 0 {
1444
- continue;
1445
- }
1446
- print_task_children(&format!(" grandchild[{}] (CTaskFilter?)", j), gc);
1447
- }
1448
- teams_found += 1;
1449
- if teams_found >= 2 {
1450
- break;
1451
- }
1452
- }
1453
- }
1454
- }
1455
-
1456
- // ---------------------------------------------------------------------------
1457
- // CTaskTeam entity dump
1458
- // ---------------------------------------------------------------------------
1459
-
1460
- fn dump_ctaskteam_entities() {
1461
- use crate::replacements::debug_utils::dump_region;
1462
- use openwa_core::task::CTaskTeam;
1463
-
1464
- let _ = log_validation("");
1465
- let _ = log_validation("--- CTaskTeam Entity Dump ---");
1466
-
1467
- unsafe {
1468
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1469
- if session_ptr == 0 {
1470
- let _ = log_validation(" No game session — skipping.");
1471
- return;
1472
- }
1473
- let wrapper_addr = read_u32(session_ptr + 0xA0);
1474
- if wrapper_addr == 0 {
1475
- let _ = log_validation(" No DDGameWrapper.");
1476
- return;
1477
- }
1478
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1479
- if ddgame_ptr == 0 {
1480
- let _ = log_validation(" No DDGame.");
1481
- return;
1482
- }
1483
-
1484
- let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1485
- if task_land_ptr == 0 {
1486
- let _ = log_validation(" CTaskLand NULL — skipping.");
1487
- return;
1488
- }
1489
- let task_land = task_land_ptr as *const openwa_core::task::CTask;
1490
- let shared_data_ptr = (*task_land).shared_data;
1491
- if shared_data_ptr.is_null() {
1492
- let _ = log_validation(" shared_data NULL.");
1493
- return;
1494
- }
1495
-
1496
- let table = SharedDataTable::from_ptr(shared_data_ptr);
1497
- let expected_vt = rb(va::CTASK_TEAM_VTABLE);
1498
- let mut found = 0u32;
1499
-
1500
- for node in table.iter() {
1501
- let entity = (*node).entity;
1502
- if entity.is_null() {
1503
- continue;
1504
- }
1505
- if !is_in_rdata(read_u32(entity as u32)) {
1506
- continue;
1507
- }
1508
- if read_u32(entity as u32) != expected_vt {
1509
- continue;
1510
- }
1511
-
1512
- let addr = entity as u32;
1513
- let team = &*(addr as *const CTaskTeam);
1514
- let _ = log_validation(&format!(
1515
- " CTaskTeam[{}] @ 0x{:08X} team_index={} worm_count={} active={} last_weapon={} pos=({:.2},{:.2})",
1516
- found, addr, team.team_index, team.worm_count,
1517
- team.alive_worm_count, team.last_launched_weapon,
1518
- team.pos_x.to_f32(), team.pos_y.to_f32()
1519
- ));
1520
-
1521
- // Full memory dump in chunks — classify every DWORD.
1522
- let base = addr as *const u8;
1523
- dump_region(base, 0x000, 0x30, "CTaskTeam"); // CTask base
1524
- dump_region(base, 0x030, 0x58, "CTaskTeam"); // 0x30..0x87 (secondary vtable, unknowns)
1525
- dump_region(base, 0x088, 0x90, "CTaskTeam"); // 0x88..0x117 (item_slots start, worm_count region)
1526
- dump_region(base, 0x118, 0x100, "CTaskTeam"); // 0x118..0x217 (item_slots end)
1527
- dump_region(base, 0x218, 0x80, "CTaskTeam"); // 0x218..0x297 (worm_count + unknowns)
1528
- dump_region(base, 0x298, 0x80, "CTaskTeam"); // 0x298..0x317
1529
- dump_region(base, 0x318, 0x80, "CTaskTeam"); // 0x318..0x397
1530
- dump_region(base, 0x398, 0x68, "CTaskTeam"); // 0x398..0x3FF
1531
- dump_region(base, 0x400, 0x60, "CTaskTeam"); // 0x400..0x45F
1532
-
1533
- found += 1;
1534
- }
1535
-
1536
- let _ = log_validation(&format!(" Total CTaskTeam entities: {}", found));
1537
- }
1538
- }
1539
-
1540
- // ---------------------------------------------------------------------------
1541
- // CTaskMissile live dump
1542
- // ---------------------------------------------------------------------------
1543
- //
1544
- // CTaskMissile is NOT registered in the SharedData entity table — it is a
1545
- // transient child of a CTaskFilter node. We walk the full task tree from the
1546
- // root (CTaskTurnGame) up to 4 levels deep, checking every child's vtable.
1547
- // Fire a weapon, then press F8.
1548
-
1549
- /// Hex dump of one CTaskMissile to the validation log.
1550
- /// Each non-zero DWORD is printed as: +0xOFFSET: 0xVALUE
1551
- /// Sections are labelled so the output can be cross-referenced with the struct.
1552
- unsafe fn dump_missile_raw(ptr: u32) {
1553
- let base = ptr as *const u32;
1554
- let sections: &[(usize, usize, &str)] = &[
1555
- (0x000, 0x030, "CTask base"),
1556
- (0x030, 0x084, "CGameTask subclass_data"),
1557
- (0x084, 0x098, "CGameTask pos/speed (0x84–0x97)"),
1558
- (0x098, 0x0FC, "CGameTask _unknown_98"),
1559
- (0x0FC, 0x130, "CTaskMissile 0xFC–0x12F"),
1560
- (0x130, 0x15C, "spawn_params"),
1561
- (0x15C, 0x2D4, "weapon_data"),
1562
- (0x2D4, 0x37C, "render_data"),
1563
- (
1564
- 0x37C,
1565
- 0x41C,
1566
- "_unknown_37c / launch_speed / homing / direction",
1567
- ),
1568
- ];
1569
- for &(start, end, label) in sections {
1570
- let _ = log_validation(&format!(
1571
- " -- {} (0x{:03X}..0x{:03X}) --",
1572
- label, start, end
1573
- ));
1574
- let dwords = (end - start) / 4;
1575
- for i in 0..dwords {
1576
- let off = start + i * 4;
1577
- let val = *base.add(off / 4);
1578
- if val != 0 {
1579
- let _ = log_validation(&format!(" +0x{:03X}: 0x{:08X} ({})", off, val, val));
1580
- }
1581
- }
1582
- }
1583
- }
1584
-
1585
- fn dump_missile_tasks() {
1586
- let _ = log_validation("");
1587
- let _ = log_validation("--- CTaskMissile Entity Dump ---");
1588
-
1589
- unsafe {
1590
- let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1591
- if session_ptr == 0 {
1592
- let _ = log_validation(" No game session — skipping.");
1593
- return;
1594
- }
1595
- let wrapper_addr = read_u32(session_ptr + 0xA0);
1596
- if wrapper_addr == 0 {
1597
- let _ = log_validation(" No DDGameWrapper.");
1598
- return;
1599
- }
1600
- let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1601
- if ddgame_ptr == 0 {
1602
- let _ = log_validation(" No DDGame.");
1603
- return;
1604
- }
1605
-
1606
- // Walk up from CTaskLand to root (CTaskTurnGame) via parent links.
1607
- let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1608
- if task_land_ptr == 0 {
1609
- let _ = log_validation(" CTaskLand NULL — game not loaded.");
1610
- return;
1611
- }
1612
- let mut root = task_land_ptr;
1613
- for _ in 0..10 {
1614
- let parent = read_u32(root + 0x04);
1615
- if parent == 0 {
1616
- break;
1617
- }
1618
- root = parent;
1619
- }
1620
-
1621
- let expected_vt = rb(va::CTASK_MISSILE_VTABLE);
1622
- let _ = log_validation(&format!(" Root (CTaskTurnGame) @ 0x{:08X}", root));
1623
- let _ = log_validation(&format!(
1624
- " Expected CTaskMissile vtable: 0x{:08X}",
1625
- expected_vt
1626
- ));
1627
-
1628
- let mut found = 0u32;
1629
- let mut first_ptr: u32 = 0;
1630
- let mut scanned = 0usize;
1631
-
1632
- for task_ptr in CTaskBfsIter::new(root as *const CTask) {
1633
- let node = task_ptr as u32;
1634
- scanned += 1;
1635
-
1636
- let vt = read_u32(node);
1637
- if vt == expected_vt {
1638
- found += 1;
1639
- if first_ptr == 0 {
1640
- first_ptr = node;
1641
- }
1642
-
1643
- let m = &*(node as *const CTaskMissile);
1644
- let pos_x = m.base.pos_x.to_f32();
1645
- let pos_y = m.base.pos_y.to_f32();
1646
- let spd_x = m.base.speed_x.to_f32();
1647
- let spd_y = m.base.speed_y.to_f32();
1648
- let _ = log_validation(&format!(
1649
- " missile#{} @ 0x{:08X} slot={} type={:?} homing={} dir={}",
1650
- found,
1651
- node,
1652
- m.slot_id,
1653
- m.missile_type(),
1654
- m.homing_enabled,
1655
- m.direction
1656
- ));
1657
- let _ = log_validation(&format!(
1658
- " pos=({:.2},{:.2}) speed=({:.3},{:.3}) cursor=({:.1},{:.1})",
1659
- pos_x,
1660
- pos_y,
1661
- spd_x,
1662
- spd_y,
1663
- m.cursor_x().to_f32(),
1664
- m.cursor_y().to_f32()
1665
- ));
1666
- let _ = log_validation(&format!(
1667
- " spawn_params: owner={} pellet={} spawn=({:.1},{:.1})",
1668
- m.spawn_params.owner_id,
1669
- m.spawn_params.pellet_index,
1670
- m.spawn_x().to_f32(),
1671
- m.spawn_y().to_f32()
1672
- ));
1673
- let _ = log_validation(&format!(
1674
- " weapon_data[0..10]: {:?}",
1675
- &m.weapon_data[..10]
1676
- ));
1677
- let _ = log_validation(&format!(
1678
- " render_data[0x15..0x1A]: {:?} (timer={})",
1679
- &m.render_data[0x15..0x1A],
1680
- m.render_data[0x19]
1681
- ));
1682
- }
1683
- }
1684
-
1685
- let _ = log_validation(&format!(
1686
- " Total CTaskMissile found: {} (scanned {} nodes)",
1687
- found, scanned
1688
- ));
1689
-
1690
- if found == 0 {
1691
- let _ = log_validation(" No missiles in flight — fire a weapon first, then press F8.");
1692
- return;
1693
- }
1694
-
1695
- // Full raw dump of the first missile to cross-validate struct layout.
1696
- let _ = log_validation(&format!(
1697
- "\n === Raw dump: CTaskMissile @ 0x{:08X} ===",
1698
- first_ptr
1699
- ));
1700
- dump_missile_raw(first_ptr);
1701
- }
1702
- }
1703
-
1704
- // ---------------------------------------------------------------------------
1705
- // Public entry point — called from wormkit's run()
1706
- // ---------------------------------------------------------------------------
1707
-
1708
- pub fn run() -> Result<(), String> {
1709
- clear_validation_log().map_err(|e| format!("Failed to clear validation log: {e}"))?;
1710
-
1711
- log_validation("============================================").map_err(|e| e.to_string())?;
1712
- log_validation(" OpenWA Runtime Validator").map_err(|e| e.to_string())?;
1713
- log_validation(" Target: WA.exe 3.8.1 (Steam)").map_err(|e| e.to_string())?;
1714
- log_validation("============================================").map_err(|e| e.to_string())?;
1715
-
1716
- // rb(IMAGE_BASE) gives us the actual runtime base
1717
- let base = rb(va::IMAGE_BASE);
1718
- let delta = base.wrapping_sub(va::IMAGE_BASE);
1719
- let _ = log_validation(&format!(
1720
- " Module base: 0x{:08X} (Ghidra base: 0x{:08X}, delta: 0x{:08X})",
1721
- base,
1722
- va::IMAGE_BASE,
1723
- delta
1724
- ));
1725
-
1726
- let mut result = ValidationResult::new();
1727
-
1728
- validate_addresses(&mut result);
1729
- validate_typed_vtable_slots(&mut result);
1730
- validate_struct_offsets(&mut result);
1731
-
1732
- let _ = log_validation("");
1733
- let _ = log_validation("--- Static Checks (vtables, struct offsets, prologues) ---");
1734
- let _ = log_validation(&format!(" {}", result.summary_line()));
1735
-
1736
- match hooks::install_all() {
1737
- Ok(()) => {}
1738
- Err(e) => {
1739
- let _ = log_validation(&format!("[ERROR] Hook installation failed: {}", e));
1740
- }
1741
- }
1742
-
1743
- let auto_mode = std::env::var("OPENWA_REPLAY_TEST").is_ok();
1744
-
1745
- if auto_mode {
1746
- let _ = log_validation("");
1747
- let _ = log_validation("--- Auto-Capture Mode (OPENWA_REPLAY_TEST) ---");
1748
- let _ =
1749
- log_validation(" Replay fast-forward active — game will exit when replay finishes.");
1750
- let _ = log_validation(" Safety timeout: 120s (will force exit if replay hangs).");
1751
- std::thread::spawn(move || {
1752
- // Restore minimized window so the replay can start.
1753
- // WA.exe won't begin replay playback until the window is visible.
1754
- std::thread::sleep(std::time::Duration::from_secs(2));
1755
- unsafe {
1756
- let hwnd = windows_sys::Win32::UI::WindowsAndMessaging::FindWindowA(
1757
- core::ptr::null::<u8>(),
1758
- b"Worms Armageddon\0".as_ptr(),
1759
- );
1760
- if !hwnd.is_null() {
1761
- windows_sys::Win32::UI::WindowsAndMessaging::ShowWindow(hwnd, 9); // SW_RESTORE
1762
- let _ = log_validation(" Window restored (SW_RESTORE).");
1763
- } else {
1764
- let _ = log_validation(" WARNING: Could not find WA window to restore.");
1765
- }
1766
- }
1767
-
1768
- // Wait for gameplay to start, then run initial validation
1769
- std::thread::sleep(std::time::Duration::from_secs(3));
1770
- let _ = log_validation(" Running deferred global validation...");
1771
- deferred_global_validation();
1772
- let _ = log_validation(" Running entity census (5s mark)...");
1773
- dump_entity_census();
1774
- let _ = log_validation(" Running worm entity dump (5s mark)...");
1775
- dump_worm_tasks();
1776
- let _ = log_validation(" Running TurnGame dump (5s mark)...");
1777
- dump_turngame();
1778
- let _ = log_validation(" Running CTaskTeam entity dump (5s mark)...");
1779
- dump_ctaskteam_entities();
1780
- let _ = log_validation(" Running CTask children sub-struct dump (5s mark)...");
1781
- dump_ctask_children();
1782
- let _ = log_validation(" Running CTaskMissile dump (5s mark)...");
1783
- dump_missile_tasks();
1784
-
1785
- // Wait briefly for replay to start, then dump game state
1786
- // (fast-forward finishes the replay in ~10-15s total, so dump early)
1787
- std::thread::sleep(std::time::Duration::from_secs(3));
1788
- let _ = log_validation(" Running team block dump (8s mark)...");
1789
- dump_team_blocks();
1790
- let _ = log_validation(" Running landscape dump...");
1791
- dump_landscape();
1792
-
1793
- // Safety timeout: if the game hasn't exited on its own by 120s, force exit.
1794
- // The replay fast-forward should finish well before this.
1795
- std::thread::sleep(std::time::Duration::from_secs(112));
1796
- let _ = log_validation("");
1797
- let _ = log_validation("--- Safety timeout reached (120s), forcing exit ---");
1798
- unsafe {
1799
- windows_sys::Win32::System::Threading::ExitProcess(1);
1800
- }
1801
- });
1802
- } else {
1803
- let _ = log_validation("");
1804
- let _ = log_validation("--- Interactive Mode (deferred polling) ---");
1805
- std::thread::spawn(move || {
1806
- std::thread::sleep(std::time::Duration::from_secs(10));
1807
- deferred_global_validation();
1808
- });
1809
- let _ = log_validation(" Polling thread started (10s delay).");
1810
-
1811
- std::thread::spawn(move || {
1812
- std::thread::sleep(std::time::Duration::from_secs(30));
1813
- dump_team_blocks();
1814
- });
1815
- let _ = log_validation(" Team block dump thread started (30s delay).");
1816
-
1817
- std::thread::spawn(move || {
1818
- std::thread::sleep(std::time::Duration::from_secs(15));
1819
- dump_landscape();
1820
- });
1821
- let _ = log_validation(" Landscape dump thread started (15s delay).");
1822
- }
1823
-
1824
- Ok(())
1825
- }
1826
-
1827
- /// Dump DSSound channel descriptor raw bytes for field layout verification.
1828
- unsafe fn dump_dssound_channels() {
1829
- use openwa_core::address::va;
1830
- use openwa_core::engine::game_session::GameSession;
1831
- use openwa_core::rebase::rb;
1832
-
1833
- let session_ptr = rb(va::G_GAME_SESSION) as *const *const GameSession;
1834
- let session = *session_ptr;
1835
- if session.is_null() {
1836
- let _ = log_validation("[DSSound] No session");
1837
- return;
1838
- }
1839
- let sound = (*session).sound;
1840
- if sound.is_null() {
1841
- let _ = log_validation("[DSSound] No sound");
1842
- return;
1843
- }
1844
- let snd = &*sound;
1845
-
1846
- let _ = log_validation("=== DSSound Channel Descriptor Dump ===");
1847
- let _ = log_validation(&format!(
1848
- " DSSound at 0x{:08X}, vtable=0x{:08X}, volume={:?}",
1849
- sound as u32, snd.vtable as u32, snd.volume
1850
- ));
1851
-
1852
- // Dump each descriptor as raw u32s
1853
- let base = sound as *const u8;
1854
- for i in 0..8 {
1855
- let desc_offset = 0x14 + i * 0x18;
1856
- let desc_ptr = base.add(desc_offset) as *const u32;
1857
- let words: [u32; 6] = [
1858
- *desc_ptr,
1859
- *desc_ptr.add(1),
1860
- *desc_ptr.add(2),
1861
- *desc_ptr.add(3),
1862
- *desc_ptr.add(4),
1863
- *desc_ptr.add(5),
1864
- ];
1865
- let has_buffer = words[5] != 0; // ds_buffer at +0x14 = word[5]
1866
- let _ = log_validation(&format!(
1867
- " desc[{}] @+0x{:03X}: [{:08X} {:08X} {:08X} {:08X} {:08X} {:08X}]{}",
1868
- i,
1869
- desc_offset,
1870
- words[0],
1871
- words[1],
1872
- words[2],
1873
- words[3],
1874
- words[4],
1875
- words[5],
1876
- if has_buffer { " <-- has buffer" } else { "" }
1877
- ));
1878
- }
1879
-
1880
- // Also dump buffer pool state
1881
- let _ = log_validation(&format!(
1882
- " pool: free={}, used={}",
1883
- snd.buffer_pool_free_count, snd.buffer_pool_used_count
1884
- ));
1885
- }
1886
-
1887
- /// Start the debug hotkey listener thread.
1888
- ///
1889
- /// F8 = CTaskMissile dump (fire a weapon first, then press F8 while missile is in flight).
1890
- ///
1891
- /// Always available regardless of `OPENWA_VALIDATE`. Skipped in replay-test
1892
- /// mode since auto-capture handles dumps automatically.
1893
- pub fn start_hotkeys() {
1894
- if std::env::var("OPENWA_REPLAY_TEST").is_ok() {
1895
- return;
1896
- }
1897
-
1898
- std::thread::spawn(|| {
1899
- use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
1900
- const VK_F7: i32 = 0x76;
1901
- const VK_F8: i32 = 0x77;
1902
- let _ = log_validation(" Hotkeys: F7=DSSound channel dump, F8=missile dump");
1903
- loop {
1904
- std::thread::sleep(std::time::Duration::from_millis(100));
1905
- unsafe {
1906
- if GetAsyncKeyState(VK_F7) & 1 != 0 {
1907
- dump_dssound_channels();
1908
- }
1909
- if GetAsyncKeyState(VK_F8) & 1 != 0 {
1910
- dump_missile_tasks();
1911
- }
1912
- }
1913
- }
1914
- });
1915
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
replay-test.ps1 CHANGED
@@ -67,7 +67,6 @@ if ($Headless) {
67
  Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
68
  Write-Host " (Fast-forward mode: replay will be auto-advanced, game exits when done)" -ForegroundColor Yellow
69
 
70
- $env:OPENWA_VALIDATE = "1"
71
  $env:OPENWA_REPLAY_TEST = "1"
72
  $proc = Start-Process -FilePath $launcher -ArgumentList "--minimized `"$waExe`" `"$ReplayFile`"" -PassThru
73
  $timeout = 150
@@ -78,7 +77,6 @@ if (-not $proc.WaitForExit($timeout * 1000)) {
78
  $proc.Kill()
79
  $proc.WaitForExit(5000)
80
  }
81
- $env:OPENWA_VALIDATE = $null
82
  $env:OPENWA_REPLAY_TEST = $null
83
  $env:OPENWA_HEADLESS = $null
84
  $env:OPENWA_WA_PATH = $null
 
67
  Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
68
  Write-Host " (Fast-forward mode: replay will be auto-advanced, game exits when done)" -ForegroundColor Yellow
69
 
 
70
  $env:OPENWA_REPLAY_TEST = "1"
71
  $proc = Start-Process -FilePath $launcher -ArgumentList "--minimized `"$waExe`" `"$ReplayFile`"" -PassThru
72
  $timeout = 150
 
77
  $proc.Kill()
78
  $proc.WaitForExit(5000)
79
  }
 
80
  $env:OPENWA_REPLAY_TEST = $null
81
  $env:OPENWA_HEADLESS = $null
82
  $env:OPENWA_WA_PATH = $null