feat: replay fast-forward via PostMessage + WndProc subclass
Browse filesAutomatically fast-forward through entire replays during testing by
posting WM_KEYDOWN/WM_KEYUP spacebar messages with 50ms gaps. Subclasses
the game's WndProc to intercept WM_ACTIVATE/WM_ACTIVATEAPP, making WA
think it always has focus so input works even when the window is in the
background.
- Add input.rs with PostMessage loop + WndProc subclass
- Update validation auto-capture: dump at 8s, 120s safety timeout
- Update replay-test.ps1 with 150s timeout
- Add DDKeyboard::PollKeyboardState address
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- .claude/skills/replay-test/SKILL.md +7 -4
- CLAUDE.md +2 -2
- crates/openwa-types/src/address.rs +7 -0
- crates/openwa-wormkit/src/replacements/input.rs +126 -0
- crates/openwa-wormkit/src/replacements/mod.rs +2 -0
- crates/openwa-wormkit/src/validation/mod.rs +14 -8
- docs/plans/2026-03-08-replay-fastforward-design.md +61 -0
- replay-test.ps1 +12 -5
.claude/skills/replay-test/SKILL.md
CHANGED
|
@@ -20,7 +20,9 @@ This will:
|
|
| 20 |
- Deploy wkOpenWA.dll to the WA game directory
|
| 21 |
- Set `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1` env vars
|
| 22 |
- Launch WA.exe with the default replay file (testdata/replays/bots.WAgame)
|
| 23 |
-
-
|
|
|
|
|
|
|
| 24 |
- Copy logs to testdata/logs/
|
| 25 |
|
| 26 |
2. Read the validation log and present results:
|
|
@@ -28,7 +30,7 @@ This will:
|
|
| 28 |
Read `testdata/logs/validation_latest.log` and summarize:
|
| 29 |
- Total PASS/FAIL counts
|
| 30 |
- Any [FAIL] lines (quote them exactly)
|
| 31 |
-
- Whether
|
| 32 |
- Key data from dumps (team blocks, landscape) if present
|
| 33 |
|
| 34 |
3. If the user also has `testdata/logs/openwa_latest.log`, read it and note any errors or interesting hook activity.
|
|
@@ -36,13 +38,14 @@ Read `testdata/logs/validation_latest.log` and summarize:
|
|
| 36 |
## Environment Variables
|
| 37 |
|
| 38 |
- `OPENWA_VALIDATE=1` — Enables the validation module (struct checks, vtable validation, memory dumps). Without this, only the replacement hooks run.
|
| 39 |
-
- `OPENWA_REPLAY_TEST=1` — Enables
|
| 40 |
|
| 41 |
Both are set automatically by `replay-test.ps1`.
|
| 42 |
|
| 43 |
## Notes
|
| 44 |
|
| 45 |
-
- The
|
|
|
|
| 46 |
- There is only one DLL now: `wkOpenWA.dll` (unified wormkit + validator)
|
| 47 |
- The old `wkOpenWAValidator.dll` is automatically removed by the script
|
| 48 |
- Replay file can be changed by editing the script invocation: `powershell -File replay-test.ps1 path\to\other.WAgame`
|
|
|
|
| 20 |
- Deploy wkOpenWA.dll to the WA game directory
|
| 21 |
- Set `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1` env vars
|
| 22 |
- Launch WA.exe with the default replay file (testdata/replays/bots.WAgame)
|
| 23 |
+
- Fast-forward through the entire replay via PostMessage (spacebar down/up with 50ms gaps)
|
| 24 |
+
- The DLL calls SetForegroundWindow to ensure WA processes keyboard messages
|
| 25 |
+
- Game exits naturally when replay finishes (150s safety timeout)
|
| 26 |
- Copy logs to testdata/logs/
|
| 27 |
|
| 28 |
2. Read the validation log and present results:
|
|
|
|
| 30 |
Read `testdata/logs/validation_latest.log` and summarize:
|
| 31 |
- Total PASS/FAIL counts
|
| 32 |
- Any [FAIL] lines (quote them exactly)
|
| 33 |
+
- Whether validation completed or safety timeout triggered
|
| 34 |
- Key data from dumps (team blocks, landscape) if present
|
| 35 |
|
| 36 |
3. If the user also has `testdata/logs/openwa_latest.log`, read it and note any errors or interesting hook activity.
|
|
|
|
| 38 |
## Environment Variables
|
| 39 |
|
| 40 |
- `OPENWA_VALIDATE=1` — Enables the validation module (struct checks, vtable validation, memory dumps). Without this, only the replacement hooks run.
|
| 41 |
+
- `OPENWA_REPLAY_TEST=1` — Enables fast-forward mode: posts WM_KEYDOWN/WM_KEYUP messages to advance the replay one turn per press. Runs validation dumps at 8s. Safety timeout at 120s forces ExitProcess(1). Without this, validation runs in interactive mode with hotkeys (F9=team blocks, F10=landscape).
|
| 42 |
|
| 43 |
Both are set automatically by `replay-test.ps1`.
|
| 44 |
|
| 45 |
## Notes
|
| 46 |
|
| 47 |
+
- The fast-forwarded replay typically completes in ~10-15s
|
| 48 |
+
- The DLL restores and focuses the game window (steals focus briefly) so PostMessage input works
|
| 49 |
- There is only one DLL now: `wkOpenWA.dll` (unified wormkit + validator)
|
| 50 |
- The old `wkOpenWAValidator.dll` is automatically removed by the script
|
| 51 |
- Replay file can be changed by editing the script invocation: `powershell -File replay-test.ps1 path\to\other.WAgame`
|
CLAUDE.md
CHANGED
|
@@ -55,7 +55,7 @@ powershell -ExecutionPolicy Bypass -File replay-test.ps1
|
|
| 55 |
|
| 56 |
How it works:
|
| 57 |
1. `replay-test.ps1` builds the unified DLL, deploys to game dir, sets `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1`, launches `WA.exe` minimized with a replay file
|
| 58 |
-
2. The DLL
|
| 59 |
3. Script copies logs to `testdata/logs/` and prints a PASS/FAIL summary
|
| 60 |
|
| 61 |
Key paths:
|
|
@@ -66,7 +66,7 @@ Key paths:
|
|
| 66 |
|
| 67 |
Environment variables:
|
| 68 |
- `OPENWA_VALIDATE=1` — Enable validation module (struct checks, vtable validation, memory dumps)
|
| 69 |
-
- `OPENWA_REPLAY_TEST=1` —
|
| 70 |
|
| 71 |
## Crate Architecture
|
| 72 |
|
|
|
|
| 55 |
|
| 56 |
How it works:
|
| 57 |
1. `replay-test.ps1` builds the unified DLL, deploys to game dir, sets `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1`, launches `WA.exe` minimized with a replay file
|
| 58 |
+
2. The DLL posts WM_KEYDOWN/WM_KEYUP spacebar messages (50ms gaps) to fast-forward the replay, calls SetForegroundWindow so input works even when launched minimized. Validation dumps run at 8s. The replay typically finishes in ~10-15s
|
| 59 |
3. Script copies logs to `testdata/logs/` and prints a PASS/FAIL summary
|
| 60 |
|
| 61 |
Key paths:
|
|
|
|
| 66 |
|
| 67 |
Environment variables:
|
| 68 |
- `OPENWA_VALIDATE=1` — Enable validation module (struct checks, vtable validation, memory dumps)
|
| 69 |
+
- `OPENWA_REPLAY_TEST=1` — Fast-forward mode: posts spacebar messages to advance replay, runs validation dumps at 8s, 120s safety timeout. Without this, validation runs interactively with hotkeys (F9=team blocks, F10=landscape)
|
| 70 |
|
| 71 |
## Crate Architecture
|
| 72 |
|
crates/openwa-types/src/address.rs
CHANGED
|
@@ -258,6 +258,13 @@ pub mod va {
|
|
| 258 |
pub const PLAY_SOUND_LOCAL: u32 = 0x004F_DFE0;
|
| 259 |
pub const PLAY_SOUND_GLOBAL: u32 = 0x0054_6E20;
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
// === Chat / UI ===
|
| 262 |
|
| 263 |
pub const SHOW_CHAT_MESSAGE: u32 = 0x0052_ACB0;
|
|
|
|
| 258 |
pub const PLAY_SOUND_LOCAL: u32 = 0x004F_DFE0;
|
| 259 |
pub const PLAY_SOUND_GLOBAL: u32 = 0x0054_6E20;
|
| 260 |
|
| 261 |
+
// === Input ===
|
| 262 |
+
|
| 263 |
+
/// DDKeyboard::PollKeyboardState — drains WM_KEY messages, calls
|
| 264 |
+
/// GetKeyboardState, normalizes to both key_state (+0x11C) and
|
| 265 |
+
/// prev_state (+0x21C) buffers. stdcall(this), RET 0x4.
|
| 266 |
+
pub const DDKEYBOARD_POLL_KEYBOARD_STATE: u32 = 0x0057_2290;
|
| 267 |
+
|
| 268 |
// === Chat / UI ===
|
| 269 |
|
| 270 |
pub const SHOW_CHAT_MESSAGE: u32 = 0x0052_ACB0;
|
crates/openwa-wormkit/src/replacements/input.rs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Input hooks for replay fast-forward.
|
| 2 |
+
//!
|
| 3 |
+
//! When `OPENWA_REPLAY_TEST=1`, posts WM_KEYDOWN/WM_KEYUP messages for
|
| 4 |
+
//! spacebar to the game's window, advancing the replay one turn per press.
|
| 5 |
+
//!
|
| 6 |
+
//! The key-down and key-up must land in SEPARATE game frames so the game
|
| 7 |
+
//! sees a stable "pressed" state for at least one frame before release.
|
| 8 |
+
//! WA runs at 50fps (~20ms/frame), so we use 50ms gaps between down/up.
|
| 9 |
+
//!
|
| 10 |
+
//! To work without stealing focus, we subclass the game's WndProc to
|
| 11 |
+
//! intercept WM_ACTIVATE/WM_ACTIVATEAPP and make WA think it always has
|
| 12 |
+
//! focus. This allows PostMessage input to work even when the window is
|
| 13 |
+
//! in the background.
|
| 14 |
+
//!
|
| 15 |
+
//! Activation is delayed ~3s after DLL load to let WA reach gameplay.
|
| 16 |
+
|
| 17 |
+
use crate::log_line;
|
| 18 |
+
|
| 19 |
+
const VK_SPACE: u32 = 0x20;
|
| 20 |
+
const WM_KEYDOWN: u32 = 0x0100;
|
| 21 |
+
const WM_KEYUP: u32 = 0x0101;
|
| 22 |
+
const WM_ACTIVATE: u32 = 0x0006;
|
| 23 |
+
const WM_ACTIVATEAPP: u32 = 0x001C;
|
| 24 |
+
const WA_ACTIVE: u32 = 1;
|
| 25 |
+
const GWL_WNDPROC: i32 = -4;
|
| 26 |
+
const SW_RESTORE: i32 = 9;
|
| 27 |
+
|
| 28 |
+
/// Original WndProc, saved when we subclass the window.
|
| 29 |
+
static mut ORIG_WNDPROC: u32 = 0;
|
| 30 |
+
|
| 31 |
+
/// Our WndProc wrapper: intercepts deactivation messages so WA thinks
|
| 32 |
+
/// it always has focus, then forwards everything to the original.
|
| 33 |
+
unsafe extern "system" fn hook_wndproc(hwnd: u32, msg: u32, wparam: u32, lparam: u32) -> u32 {
|
| 34 |
+
extern "system" {
|
| 35 |
+
fn CallWindowProcA(lpPrevWndFunc: u32, hWnd: u32, msg: u32, wParam: u32, lParam: u32) -> u32;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
match msg {
|
| 39 |
+
WM_ACTIVATE => {
|
| 40 |
+
// Always tell WA it's being activated (WA_ACTIVE=1)
|
| 41 |
+
return CallWindowProcA(ORIG_WNDPROC, hwnd, msg, WA_ACTIVE, lparam);
|
| 42 |
+
}
|
| 43 |
+
WM_ACTIVATEAPP => {
|
| 44 |
+
// Always tell WA the app is being activated (wParam=TRUE)
|
| 45 |
+
return CallWindowProcA(ORIG_WNDPROC, hwnd, msg, 1, lparam);
|
| 46 |
+
}
|
| 47 |
+
_ => {}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
CallWindowProcA(ORIG_WNDPROC, hwnd, msg, wparam, lparam)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
pub fn install() -> Result<(), String> {
|
| 54 |
+
if std::env::var("OPENWA_REPLAY_TEST").is_err() {
|
| 55 |
+
return Ok(());
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
let _ = log_line("[Input] Replay test mode — will post spacebar messages for fast-forward");
|
| 59 |
+
|
| 60 |
+
extern "system" {
|
| 61 |
+
fn FindWindowA(lpClassName: *const u8, lpWindowName: *const u8) -> u32;
|
| 62 |
+
fn PostMessageA(hWnd: u32, msg: u32, wParam: u32, lParam: u32) -> i32;
|
| 63 |
+
fn SetForegroundWindow(hWnd: u32) -> i32;
|
| 64 |
+
fn ShowWindow(hWnd: u32, nCmdShow: i32) -> i32;
|
| 65 |
+
fn SetWindowLongA(hWnd: u32, nIndex: i32, dwNewLong: u32) -> u32;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
std::thread::spawn(move || {
|
| 69 |
+
let _ = log_line("[Input] Waiting 1s before enabling fast-forward...");
|
| 70 |
+
std::thread::sleep(std::time::Duration::from_secs(1));
|
| 71 |
+
|
| 72 |
+
// Find the WA game window
|
| 73 |
+
let hwnd = unsafe { FindWindowA(std::ptr::null(), b"Worms Armageddon\0".as_ptr()) };
|
| 74 |
+
if hwnd == 0 {
|
| 75 |
+
let _ = log_line("[Input] ERROR: Could not find game window");
|
| 76 |
+
return;
|
| 77 |
+
}
|
| 78 |
+
let _ = log_line(&format!("[Input] Found game window: HWND=0x{:08X}", hwnd));
|
| 79 |
+
|
| 80 |
+
// Subclass the WndProc to intercept deactivation messages
|
| 81 |
+
unsafe {
|
| 82 |
+
ORIG_WNDPROC = SetWindowLongA(hwnd, GWL_WNDPROC, hook_wndproc as *const () as u32);
|
| 83 |
+
if ORIG_WNDPROC == 0 {
|
| 84 |
+
let _ = log_line("[Input] ERROR: SetWindowLongA failed");
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
let _ = log_line("[Input] WndProc subclassed — WA will think it always has focus");
|
| 89 |
+
|
| 90 |
+
// Restore and focus window once to kick things off
|
| 91 |
+
unsafe {
|
| 92 |
+
ShowWindow(hwnd, SW_RESTORE);
|
| 93 |
+
SetForegroundWindow(hwnd);
|
| 94 |
+
}
|
| 95 |
+
let _ = log_line("[Input] Fast-forward enabled — posting spacebar messages");
|
| 96 |
+
|
| 97 |
+
// lParam encodes scan code (0x39 for space) and repeat count
|
| 98 |
+
let down_lparam: u32 = 0x0039_0001; // scan=0x39, repeat=1, bit30=0 (first press)
|
| 99 |
+
let up_lparam: u32 = 0xC039_0001; // scan=0x39, repeat=1, bit30+31 set (release)
|
| 100 |
+
|
| 101 |
+
loop {
|
| 102 |
+
unsafe {
|
| 103 |
+
// Send key DOWN — must be processed in its own frame
|
| 104 |
+
let r = PostMessageA(hwnd, WM_KEYDOWN, VK_SPACE, down_lparam);
|
| 105 |
+
if r == 0 {
|
| 106 |
+
let _ = log_line("[Input] PostMessage failed — window closed");
|
| 107 |
+
break;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
// Wait >1 frame (50ms > 20ms/frame) so game processes key-down
|
| 111 |
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
| 112 |
+
|
| 113 |
+
unsafe {
|
| 114 |
+
// Send key UP — processed in a subsequent frame
|
| 115 |
+
let r = PostMessageA(hwnd, WM_KEYUP, VK_SPACE, up_lparam);
|
| 116 |
+
if r == 0 {
|
| 117 |
+
break;
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
// Wait for the release to be processed before next press
|
| 121 |
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
| 122 |
+
}
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
Ok(())
|
| 126 |
+
}
|
crates/openwa-wormkit/src/replacements/mod.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
mod config;
|
| 2 |
mod frontend;
|
|
|
|
| 3 |
mod scheme;
|
| 4 |
mod render;
|
| 5 |
mod sound;
|
|
@@ -14,5 +15,6 @@ pub fn install_all() -> Result<(), String> {
|
|
| 14 |
team::install()?;
|
| 15 |
render::install()?;
|
| 16 |
sound::install()?;
|
|
|
|
| 17 |
Ok(())
|
| 18 |
}
|
|
|
|
| 1 |
mod config;
|
| 2 |
mod frontend;
|
| 3 |
+
mod input;
|
| 4 |
mod scheme;
|
| 5 |
mod render;
|
| 6 |
mod sound;
|
|
|
|
| 15 |
team::install()?;
|
| 16 |
render::install()?;
|
| 17 |
sound::install()?;
|
| 18 |
+
input::install()?;
|
| 19 |
Ok(())
|
| 20 |
}
|
crates/openwa-wormkit/src/validation/mod.rs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
| 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
|
|
|
|
| 6 |
|
| 7 |
mod hooks;
|
| 8 |
|
|
@@ -748,24 +749,29 @@ pub fn run() -> Result<(), String> {
|
|
| 748 |
if auto_mode {
|
| 749 |
let _ = log_validation("");
|
| 750 |
let _ = log_validation("--- Auto-Capture Mode (OPENWA_REPLAY_TEST) ---");
|
|
|
|
|
|
|
| 751 |
std::thread::spawn(move || {
|
| 752 |
-
|
| 753 |
std::thread::sleep(std::time::Duration::from_secs(5));
|
| 754 |
-
|
| 755 |
let _ = log_validation(" Running deferred global validation...");
|
| 756 |
deferred_global_validation();
|
| 757 |
|
| 758 |
-
|
|
|
|
|
|
|
|
|
|
| 759 |
dump_team_blocks();
|
| 760 |
-
|
| 761 |
let _ = log_validation(" Running landscape dump...");
|
| 762 |
dump_landscape();
|
| 763 |
|
|
|
|
|
|
|
|
|
|
| 764 |
let _ = log_validation("");
|
| 765 |
-
let _ = log_validation("---
|
| 766 |
-
|
| 767 |
unsafe {
|
| 768 |
-
windows_sys::Win32::System::Threading::ExitProcess(
|
| 769 |
}
|
| 770 |
});
|
| 771 |
} else {
|
|
|
|
| 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 |
|
|
|
|
| 749 |
if auto_mode {
|
| 750 |
let _ = log_validation("");
|
| 751 |
let _ = log_validation("--- Auto-Capture Mode (OPENWA_REPLAY_TEST) ---");
|
| 752 |
+
let _ = log_validation(" Replay fast-forward active — game will exit when replay finishes.");
|
| 753 |
+
let _ = log_validation(" Safety timeout: 120s (will force exit if replay hangs).");
|
| 754 |
std::thread::spawn(move || {
|
| 755 |
+
// Wait for gameplay to start, then run initial validation
|
| 756 |
std::thread::sleep(std::time::Duration::from_secs(5));
|
|
|
|
| 757 |
let _ = log_validation(" Running deferred global validation...");
|
| 758 |
deferred_global_validation();
|
| 759 |
|
| 760 |
+
// Wait briefly for replay to start, then dump game state
|
| 761 |
+
// (fast-forward finishes the replay in ~10-15s total, so dump early)
|
| 762 |
+
std::thread::sleep(std::time::Duration::from_secs(3));
|
| 763 |
+
let _ = log_validation(" Running team block dump (8s mark)...");
|
| 764 |
dump_team_blocks();
|
|
|
|
| 765 |
let _ = log_validation(" Running landscape dump...");
|
| 766 |
dump_landscape();
|
| 767 |
|
| 768 |
+
// Safety timeout: if the game hasn't exited on its own by 120s, force exit.
|
| 769 |
+
// The replay fast-forward should finish well before this.
|
| 770 |
+
std::thread::sleep(std::time::Duration::from_secs(112));
|
| 771 |
let _ = log_validation("");
|
| 772 |
+
let _ = log_validation("--- Safety timeout reached (120s), forcing exit ---");
|
|
|
|
| 773 |
unsafe {
|
| 774 |
+
windows_sys::Win32::System::Threading::ExitProcess(1);
|
| 775 |
}
|
| 776 |
});
|
| 777 |
} else {
|
docs/plans/2026-03-08-replay-fastforward-design.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Replay Fast-Forward Design
|
| 2 |
+
|
| 3 |
+
## Goal
|
| 4 |
+
|
| 5 |
+
Automatically fast-forward through entire WA replays during `OPENWA_REPLAY_TEST=1` mode, enabling comprehensive testing beyond the current 5-second window.
|
| 6 |
+
|
| 7 |
+
## Background
|
| 8 |
+
|
| 9 |
+
The current replay test captures only 5 seconds of gameplay — often not enough for a single worm turn. WA advances one turn per spacebar press during replay playback. The game detects key-DOWN transitions (not held state) and has no key repeat, so spacebar must be repeatedly pressed and released.
|
| 10 |
+
|
| 11 |
+
WA.exe exits automatically when a replay finishes, so no explicit exit is needed if we can fast-forward through the whole thing.
|
| 12 |
+
|
| 13 |
+
## Approach: Hook GetAsyncKeyState
|
| 14 |
+
|
| 15 |
+
Hook `GetAsyncKeyState` from `user32.dll` via MinHook. When the game queries `VK_SPACE` and fast-forward mode is active, return simulated key-down/key-up transitions.
|
| 16 |
+
|
| 17 |
+
**Why this over keybd_event/SendInput:**
|
| 18 |
+
- Works when WA.exe is minimized (no window focus required)
|
| 19 |
+
- Intercepts at the API level regardless of how WA reads the result
|
| 20 |
+
- No timing races with the game's message pump
|
| 21 |
+
- Existing MinHook infrastructure
|
| 22 |
+
|
| 23 |
+
### Key simulation
|
| 24 |
+
|
| 25 |
+
GetAsyncKeyState returns a SHORT:
|
| 26 |
+
- Bit 15 (0x8000) = key currently down
|
| 27 |
+
- Bit 0 (0x0001) = key pressed since last call
|
| 28 |
+
|
| 29 |
+
Toggle between calls:
|
| 30 |
+
- Even: `0x8001` (down + transition)
|
| 31 |
+
- Odd: `0x0000` (up, no transition)
|
| 32 |
+
|
| 33 |
+
All other virtual keys pass through to the real function.
|
| 34 |
+
|
| 35 |
+
### Activation
|
| 36 |
+
|
| 37 |
+
1. DLL loads, detects `OPENWA_REPLAY_TEST=1`
|
| 38 |
+
2. Spawns thread that waits ~3 seconds for game to reach gameplay
|
| 39 |
+
3. Sets atomic flag to enable fast-forward
|
| 40 |
+
4. GetAsyncKeyState hook checks flag on each VK_SPACE query
|
| 41 |
+
|
| 42 |
+
### Validation integration
|
| 43 |
+
|
| 44 |
+
Current: thread waits 5s -> validate -> ExitProcess.
|
| 45 |
+
|
| 46 |
+
New: thread waits 3s -> enable fast-forward -> wait up to 120s for natural exit. Validation dumps trigger on a timeout (60s) as a safety net. Normally the game exits on its own when the replay finishes.
|
| 47 |
+
|
| 48 |
+
### Script changes
|
| 49 |
+
|
| 50 |
+
`replay-test.ps1`: add timeout to `WaitForExit()` (120s). Remove the implicit 5s assumption from comments.
|
| 51 |
+
|
| 52 |
+
## Future: Turn-Start Hook
|
| 53 |
+
|
| 54 |
+
Hooking the function that runs at each turn start would enable per-turn assertions and proper unit-test-style validation during replay playback. This is out of scope for this design but is the natural next step.
|
| 55 |
+
|
| 56 |
+
## Files
|
| 57 |
+
|
| 58 |
+
- `crates/openwa-wormkit/src/replacements/input.rs` (new) — GetAsyncKeyState hook
|
| 59 |
+
- `crates/openwa-wormkit/src/validation/mod.rs` — Update auto-capture timeout
|
| 60 |
+
- `crates/openwa-wormkit/src/lib.rs` — Register input module
|
| 61 |
+
- `replay-test.ps1` — Add WaitForExit timeout
|
replay-test.ps1
CHANGED
|
@@ -42,12 +42,17 @@ Remove-Item "$gameDir\OpenWA_validation.log" -ErrorAction SilentlyContinue
|
|
| 42 |
|
| 43 |
# 4. Launch WA.exe with replay, env vars set
|
| 44 |
Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
|
| 45 |
-
Write-Host " (
|
| 46 |
|
| 47 |
$env:OPENWA_VALIDATE = "1"
|
| 48 |
$env:OPENWA_REPLAY_TEST = "1"
|
| 49 |
$proc = Start-Process -FilePath $waExe -ArgumentList "`"$ReplayFile`"" -WindowStyle Minimized -PassThru
|
| 50 |
-
$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
$env:OPENWA_VALIDATE = $null
|
| 52 |
$env:OPENWA_REPLAY_TEST = $null
|
| 53 |
|
|
@@ -84,10 +89,12 @@ if (Test-Path "$logDir\validation_latest.log") {
|
|
| 84 |
Write-Host " PASS: $passes" -ForegroundColor Green
|
| 85 |
Write-Host " FAIL: $fails" -ForegroundColor $(if ($fails -gt 0) { "Red" } else { "Green" })
|
| 86 |
|
| 87 |
-
if ($content -match "
|
| 88 |
-
Write-Host "
|
|
|
|
|
|
|
| 89 |
} else {
|
| 90 |
-
Write-Host "
|
| 91 |
}
|
| 92 |
}
|
| 93 |
Write-Host " Logs: $logDir\" -ForegroundColor Gray
|
|
|
|
| 42 |
|
| 43 |
# 4. Launch WA.exe with replay, env vars set
|
| 44 |
Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
|
| 45 |
+
Write-Host " (Fast-forward mode: replay will be auto-advanced, game exits when done)" -ForegroundColor Yellow
|
| 46 |
|
| 47 |
$env:OPENWA_VALIDATE = "1"
|
| 48 |
$env:OPENWA_REPLAY_TEST = "1"
|
| 49 |
$proc = Start-Process -FilePath $waExe -ArgumentList "`"$ReplayFile`"" -WindowStyle Minimized -PassThru
|
| 50 |
+
$timeout = 150 # seconds - replay fast-forward + safety margin
|
| 51 |
+
if (-not $proc.WaitForExit($timeout * 1000)) {
|
| 52 |
+
Write-Host "WARNING: WA.exe did not exit within ${timeout}s, killing..." -ForegroundColor Red
|
| 53 |
+
$proc.Kill()
|
| 54 |
+
$proc.WaitForExit(5000)
|
| 55 |
+
}
|
| 56 |
$env:OPENWA_VALIDATE = $null
|
| 57 |
$env:OPENWA_REPLAY_TEST = $null
|
| 58 |
|
|
|
|
| 89 |
Write-Host " PASS: $passes" -ForegroundColor Green
|
| 90 |
Write-Host " FAIL: $fails" -ForegroundColor $(if ($fails -gt 0) { "Red" } else { "Green" })
|
| 91 |
|
| 92 |
+
if ($content -match "Safety timeout reached") {
|
| 93 |
+
Write-Host " Replay: TIMEOUT (safety timeout triggered)" -ForegroundColor Red
|
| 94 |
+
} elseif ($content -match "deferred global validation") {
|
| 95 |
+
Write-Host " Validation: completed" -ForegroundColor Green
|
| 96 |
} else {
|
| 97 |
+
Write-Host " Validation: NOT found in log (may not have reached gameplay)" -ForegroundColor Yellow
|
| 98 |
}
|
| 99 |
}
|
| 100 |
Write-Host " Logs: $logDir\" -ForegroundColor Gray
|