refactor: replace validation module with registry-driven startup checks
Browse filesDelete 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 +0 -1
- CLAUDE.md +3 -5
- crates/openwa-test-runner/src/main.rs +43 -0
- crates/openwa-wormkit/src/debug_watchpoint.rs +1 -1
- crates/openwa-wormkit/src/lib.rs +3 -15
- crates/openwa-wormkit/src/replacements/replay_test.rs +11 -12
- crates/openwa-wormkit/src/startup_checks.rs +169 -0
- crates/openwa-wormkit/src/validation/hooks/detour.rs +0 -72
- crates/openwa-wormkit/src/validation/hooks/mod.rs +0 -13
- crates/openwa-wormkit/src/validation/hooks/vtable.rs +0 -109
- crates/openwa-wormkit/src/validation/mod.rs +0 -1915
- replay-test.ps1 +0 -2
.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
|
| 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)
|
| 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
|
| 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 |
-
//!
|
| 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
|
| 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
|
| 101 |
-
|
| 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
|
| 116 |
///
|
| 117 |
/// Called from DLL_PROCESS_DETACH to provide a final summary of game progress.
|
| 118 |
pub fn write_gameplay_report() {
|
| 119 |
-
use crate::
|
| 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 _ =
|
| 126 |
-
let _ = log_validation("--- Gameplay Checks ---");
|
| 127 |
|
| 128 |
if frames > 0 {
|
| 129 |
-
let _ =
|
| 130 |
"[GAMEPLAY PASS] Game initialized - {} frames processed",
|
| 131 |
frames
|
| 132 |
));
|
| 133 |
} else {
|
| 134 |
-
let _ =
|
| 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 _ =
|
| 142 |
"[GAMEPLAY PASS] Match started - {} teams with alive worms detected",
|
| 143 |
teams
|
| 144 |
));
|
| 145 |
} else if frames > 0 {
|
| 146 |
let _ =
|
| 147 |
-
|
| 148 |
} else {
|
| 149 |
-
let _ =
|
| 150 |
}
|
| 151 |
|
| 152 |
if completed {
|
|
@@ -157,15 +156,15 @@ pub fn write_gameplay_report() {
|
|
| 157 |
} else {
|
| 158 |
"draw (all eliminated)"
|
| 159 |
};
|
| 160 |
-
let _ =
|
| 161 |
"[GAMEPLAY PASS] Match completed - {} at frame {}",
|
| 162 |
outcome, end_frame
|
| 163 |
));
|
| 164 |
} else if started {
|
| 165 |
let _ =
|
| 166 |
-
|
| 167 |
} else {
|
| 168 |
-
let _ =
|
| 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
|