feat: replace PostMessage replay fast-forward with DDGame+0x98B0 hook
Browse filesHook TurnManager_ProcessFrame to set DDGame.fast_forward_active=1 each
frame, enabling 50x speed processing (same mechanism as spacebar).
Add fast_forward_active field to DDGame struct. Auto-restore minimized
window via FindWindowA+ShowWindow at 2s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
.claude/skills/replay-test/SKILL.md
CHANGED
|
@@ -19,10 +19,10 @@ This will:
|
|
| 19 |
- Build the unified openwa-wormkit DLL (includes validation)
|
| 20 |
- Deploy wkOpenWA.dll to the WA game directory
|
| 21 |
- Set `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1` env vars
|
| 22 |
-
- Launch WA.exe with the default replay file (testdata/replays/bots.WAgame)
|
| 23 |
-
-
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
- Copy logs to testdata/logs/
|
| 27 |
|
| 28 |
2. Read the validation log and present results:
|
|
@@ -38,14 +38,15 @@ Read `testdata/logs/validation_latest.log` and summarize:
|
|
| 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:
|
| 42 |
|
| 43 |
Both are set automatically by `replay-test.ps1`.
|
| 44 |
|
| 45 |
## Notes
|
| 46 |
|
| 47 |
-
- The fast-forwarded replay typically completes in ~
|
| 48 |
-
- The DLL restores
|
| 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
|
|
|
|
|
|
| 19 |
- Build the unified openwa-wormkit DLL (includes validation)
|
| 20 |
- Deploy wkOpenWA.dll to the WA game directory
|
| 21 |
- Set `OPENWA_VALIDATE=1` and `OPENWA_REPLAY_TEST=1` env vars
|
| 22 |
+
- Launch WA.exe minimized with the default replay file (testdata/replays/bots.WAgame)
|
| 23 |
+
- The DLL restores the window after 2s via FindWindowA + ShowWindow(SW_RESTORE)
|
| 24 |
+
- Fast-forward through the entire replay by setting DDGame+0x98B0 (50 frames per render cycle)
|
| 25 |
+
- Validation runs at the 5s mark, game exits when replay finishes (120s safety timeout)
|
| 26 |
- Copy logs to testdata/logs/
|
| 27 |
|
| 28 |
2. Read the validation log and present results:
|
|
|
|
| 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: hooks TurnManager_ProcessFrame and sets DDGame+0x98B0=1 each frame, causing the game engine to process up to 50 frames per render cycle. The DLL auto-restores the window at 2s, runs validation at 5s, and has a 120s safety timeout. 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 ~15-30s
|
| 48 |
+
- The DLL restores the minimized window automatically (steals focus briefly)
|
| 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: `powershell -File replay-test.ps1 path\to\other.WAgame`
|
| 52 |
+
- Occasional crashes may occur during fast-forward (null vtable calls from 50x speed processing); retry if this happens
|
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` — Fast-forward mode:
|
| 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 restores the window after 2s via FindWindowA + ShowWindow(SW_RESTORE), then hooks TurnManager_ProcessFrame and sets DDGame+0x98B0=1 each frame to enable 50x fast-forward (same mechanism as spacebar during replay). Validation runs at 5s. The replay typically finishes in ~15-30s
|
| 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: hooks TurnManager_ProcessFrame and sets DDGame+0x98B0=1 each frame (50x speed). Restores window at 2s, runs validation at 5s, 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
|
@@ -428,6 +428,9 @@ pub mod va {
|
|
| 428 |
|
| 429 |
pub const LOBBY_HOST_COMMANDS: u32 = 0x004B_9B00;
|
| 430 |
pub const LOBBY_CLIENT_COMMANDS: u32 = 0x004A_ABB0;
|
|
|
|
|
|
|
|
|
|
| 431 |
pub const SEND_GAME_PACKET_WRAPPED: u32 = 0x0054_1130;
|
| 432 |
pub const LOBBY_DISPLAY_MESSAGE: u32 = 0x0049_3CB0;
|
| 433 |
pub const LOBBY_SEND_GREENTEXT: u32 = 0x004A_A990;
|
|
@@ -575,12 +578,17 @@ pub mod va {
|
|
| 575 |
/// Offset to weapon panel pointer
|
| 576 |
pub const WEAPON_PANEL: u32 = 0x548;
|
| 577 |
|
| 578 |
-
// --- Replay state ---
|
| 579 |
-
|
| 580 |
/// Deferred hurry flag. Set to 1 during replay instead of sending network
|
| 581 |
/// packet. GameFrameEndProcessor (0x531960) reads this and converts it to
|
| 582 |
/// a local Hurry message (TaskMessage 0x17 = 23).
|
| 583 |
pub const DEFERRED_HURRY_FLAG: u32 = 0x7E41;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
/// Replay state flag A — checked by TurnGame_HurryHandler and FrameFinish.
|
| 585 |
/// Both DB08 and DB0A must be non-zero for replay-mode hurry path.
|
| 586 |
pub const REPLAY_STATE_FLAG_A: u32 = 0xDB08;
|
|
|
|
| 428 |
|
| 429 |
pub const LOBBY_HOST_COMMANDS: u32 = 0x004B_9B00;
|
| 430 |
pub const LOBBY_CLIENT_COMMANDS: u32 = 0x004A_ABB0;
|
| 431 |
+
/// Allocates space in a packet queue and writes the packet type + data.
|
| 432 |
+
/// __usercall: EAX = queue pointer, ECX = data size. Stack: packet_type, data_ptr.
|
| 433 |
+
/// Returns 1 on success, 0 if queue full. RET 0x8.
|
| 434 |
pub const SEND_GAME_PACKET_WRAPPED: u32 = 0x0054_1130;
|
| 435 |
pub const LOBBY_DISPLAY_MESSAGE: u32 = 0x0049_3CB0;
|
| 436 |
pub const LOBBY_SEND_GREENTEXT: u32 = 0x004A_A990;
|
|
|
|
| 578 |
/// Offset to weapon panel pointer
|
| 579 |
pub const WEAPON_PANEL: u32 = 0x548;
|
| 580 |
|
|
|
|
|
|
|
| 581 |
/// Deferred hurry flag. Set to 1 during replay instead of sending network
|
| 582 |
/// packet. GameFrameEndProcessor (0x531960) reads this and converts it to
|
| 583 |
/// a local Hurry message (TaskMessage 0x17 = 23).
|
| 584 |
pub const DEFERRED_HURRY_FLAG: u32 = 0x7E41;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
// === GameInfo struct offsets ===
|
| 588 |
+
// These are offsets from the GameInfo pointer at DDGame+0x24 (also called
|
| 589 |
+
// "game_state" in some code). Access pattern: *(DDGame+0x24) + offset.
|
| 590 |
+
|
| 591 |
+
pub mod game_info_offsets {
|
| 592 |
/// Replay state flag A — checked by TurnGame_HurryHandler and FrameFinish.
|
| 593 |
/// Both DB08 and DB0A must be non-zero for replay-mode hurry path.
|
| 594 |
pub const REPLAY_STATE_FLAG_A: u32 = 0xDB08;
|
crates/openwa-types/src/ddgame.rs
CHANGED
|
@@ -102,12 +102,24 @@ pub struct DDGame {
|
|
| 102 |
/// 0x8150: Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
|
| 103 |
pub crosshair_scale: i32,
|
| 104 |
|
| 105 |
-
/// 0x8154-
|
| 106 |
///
|
| 107 |
/// Known landmarks:
|
| 108 |
/// - 0x8CBC-0x8CF0: 4x 0x10-byte entries (zeroed at +0, +4)
|
| 109 |
/// - 0x9850-0x9884: 4x 0x10-byte entries (zeroed at +0, +4)
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
|
|
@@ -177,6 +189,10 @@ pub mod offsets {
|
|
| 177 |
/// Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
|
| 178 |
pub const CROSSHAIR_SCALE: usize = 0x8150;
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
// === Sound queue (DDGame + 0x7F00) ===
|
| 181 |
/// Sound enabled flag (i32, nonzero = enabled).
|
| 182 |
pub const SOUND_ENABLED: usize = 0x0008;
|
|
|
|
| 102 |
/// 0x8150: Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
|
| 103 |
pub crosshair_scale: i32,
|
| 104 |
|
| 105 |
+
/// 0x8154-0x98AF: Remaining fields
|
| 106 |
///
|
| 107 |
/// Known landmarks:
|
| 108 |
/// - 0x8CBC-0x8CF0: 4x 0x10-byte entries (zeroed at +0, +4)
|
| 109 |
/// - 0x9850-0x9884: 4x 0x10-byte entries (zeroed at +0, +4)
|
| 110 |
+
/// - 0x98A4: checkpoint active flag
|
| 111 |
+
/// - 0x98AC: fast-forward request flag
|
| 112 |
+
pub _unknown_8154: [u8; 0x98B0 - 0x8154],
|
| 113 |
+
|
| 114 |
+
/// 0x98B0: Fast-forward active flag.
|
| 115 |
+
/// When set to 1, FUN_005307A0 processes up to 50 game frames per render
|
| 116 |
+
/// cycle. Sound is suppressed and rendering is skipped. Cleared at turn
|
| 117 |
+
/// boundaries (FUN_00534540, FUN_0055BDD0), so must be re-set continuously.
|
| 118 |
+
/// This is the same flag toggled by spacebar (key 0x35) during replay.
|
| 119 |
+
pub fast_forward_active: u32,
|
| 120 |
+
|
| 121 |
+
/// 0x98B4-0x98B7: Unknown
|
| 122 |
+
pub _unknown_98b4: [u8; 0x98B8 - 0x98B4],
|
| 123 |
}
|
| 124 |
|
| 125 |
const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
|
|
|
|
| 189 |
/// Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
|
| 190 |
pub const CROSSHAIR_SCALE: usize = 0x8150;
|
| 191 |
|
| 192 |
+
// === Fast-forward (DDGame + 0x98B0) ===
|
| 193 |
+
/// Fast-forward active flag (u32, 1 = active).
|
| 194 |
+
pub const FAST_FORWARD_ACTIVE: usize = 0x98B0;
|
| 195 |
+
|
| 196 |
// === Sound queue (DDGame + 0x7F00) ===
|
| 197 |
/// Sound enabled flag (i32, nonzero = enabled).
|
| 198 |
pub const SOUND_ENABLED: usize = 0x0008;
|
crates/openwa-wormkit/src/replacements/input.rs
CHANGED
|
@@ -1,53 +1,67 @@
|
|
| 1 |
-
//!
|
| 2 |
//!
|
| 3 |
-
//! When `OPENWA_REPLAY_TEST=1`,
|
| 4 |
-
//!
|
| 5 |
//!
|
| 6 |
-
//!
|
| 7 |
-
//!
|
| 8 |
-
//!
|
|
|
|
| 9 |
//!
|
| 10 |
-
//!
|
| 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 |
-
|
| 29 |
-
static mut ORIG_WNDPROC: u32 = 0;
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
pub fn install() -> Result<(), String> {
|
|
@@ -55,72 +69,16 @@ pub fn install() -> Result<(), String> {
|
|
| 55 |
return Ok(());
|
| 56 |
}
|
| 57 |
|
| 58 |
-
let _ = log_line("[Input] Replay test mode —
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 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 |
}
|
|
|
|
| 1 |
+
//! Replay fast-forward via DDGame+0x98B0.
|
| 2 |
//!
|
| 3 |
+
//! When `OPENWA_REPLAY_TEST=1`, hooks TurnManager_ProcessFrame (0x55FDA0)
|
| 4 |
+
//! and sets DDGame+0x98B0 (fast-forward active flag) each frame.
|
| 5 |
//!
|
| 6 |
+
//! When this flag is set, FUN_005307A0 processes up to 50 game frames per
|
| 7 |
+
//! render cycle. Sound is suppressed (FUN_00546B50) and rendering is skipped
|
| 8 |
+
//! (FUN_00529F30). The flag gets cleared at turn boundaries (FUN_00534540,
|
| 9 |
+
//! FUN_0055BDD0), so we re-set it every frame.
|
| 10 |
//!
|
| 11 |
+
//! This is the same mechanism triggered by key 0x35 (spacebar) during replay.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
|
| 14 |
|
| 15 |
+
use crate::hook;
|
| 16 |
+
use crate::log_line;
|
| 17 |
+
use openwa_lib::rebase::rb;
|
| 18 |
+
use openwa_types::address::va;
|
| 19 |
+
use openwa_types::ddgame::DDGame;
|
| 20 |
+
|
| 21 |
+
/// Trampoline to the original TurnManager_ProcessFrame.
|
| 22 |
+
static ORIG_TURN_MANAGER: AtomicU32 = AtomicU32::new(0);
|
| 23 |
+
|
| 24 |
+
/// Get the DDGame pointer (session+0xA0 → DDGameWrapper+0x488).
|
| 25 |
+
#[inline]
|
| 26 |
+
unsafe fn get_ddgame() -> *mut DDGame {
|
| 27 |
+
let session = *(rb(va::G_GAME_SESSION) as *const u32);
|
| 28 |
+
if session == 0 {
|
| 29 |
+
return core::ptr::null_mut();
|
| 30 |
+
}
|
| 31 |
+
let wrapper = *((session + 0xA0) as *const u32);
|
| 32 |
+
if wrapper == 0 {
|
| 33 |
+
return core::ptr::null_mut();
|
| 34 |
}
|
| 35 |
+
*((wrapper + 0x488) as *const *mut DDGame)
|
| 36 |
+
}
|
| 37 |
|
| 38 |
+
/// Hook for TurnManager_ProcessFrame (stdcall, 1 param = TurnGame*).
|
| 39 |
+
///
|
| 40 |
+
/// Called every frame from TurnGame_HandleMessage case 2 (FrameFinish).
|
| 41 |
+
/// Sets DDGame.fast_forward_active = 1 to enable multi-frame processing.
|
| 42 |
+
unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
|
| 43 |
+
static DIAG_COUNT: AtomicU32 = AtomicU32::new(0);
|
| 44 |
+
|
| 45 |
+
// Call original first
|
| 46 |
+
let orig: unsafe extern "stdcall" fn(u32) =
|
| 47 |
+
core::mem::transmute(ORIG_TURN_MANAGER.load(Ordering::Relaxed));
|
| 48 |
+
orig(turngame);
|
| 49 |
+
|
| 50 |
+
let ddgame = get_ddgame();
|
| 51 |
+
if ddgame.is_null() {
|
| 52 |
+
return;
|
| 53 |
}
|
| 54 |
|
| 55 |
+
// Set fast-forward active flag
|
| 56 |
+
(*ddgame).fast_forward_active = 1;
|
| 57 |
+
|
| 58 |
+
let diag = DIAG_COUNT.fetch_add(1, Ordering::Relaxed);
|
| 59 |
+
if diag == 0 {
|
| 60 |
+
let _ = log_line(&format!(
|
| 61 |
+
"[Input] Fast-forward active (DDGame=0x{:08X})",
|
| 62 |
+
ddgame as u32
|
| 63 |
+
));
|
| 64 |
+
}
|
| 65 |
}
|
| 66 |
|
| 67 |
pub fn install() -> Result<(), String> {
|
|
|
|
| 69 |
return Ok(());
|
| 70 |
}
|
| 71 |
|
| 72 |
+
let _ = log_line("[Input] Replay test mode — hooking TurnManager_ProcessFrame for fast-forward");
|
| 73 |
|
| 74 |
+
unsafe {
|
| 75 |
+
let trampoline = hook::install(
|
| 76 |
+
"TurnManager_ProcessFrame",
|
| 77 |
+
va::TURN_MANAGER_PROCESS_FRAME,
|
| 78 |
+
hook_turn_manager as *const (),
|
| 79 |
+
)?;
|
| 80 |
+
ORIG_TURN_MANAGER.store(trampoline as u32, Ordering::Relaxed);
|
| 81 |
}
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
Ok(())
|
| 84 |
}
|
crates/openwa-wormkit/src/validation/mod.rs
CHANGED
|
@@ -752,8 +752,24 @@ pub fn run() -> Result<(), String> {
|
|
| 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(
|
| 757 |
let _ = log_validation(" Running deferred global validation...");
|
| 758 |
deferred_global_validation();
|
| 759 |
|
|
|
|
| 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 |
+
// Restore minimized window so the replay can start.
|
| 756 |
+
// WA.exe won't begin replay playback until the window is visible.
|
| 757 |
+
std::thread::sleep(std::time::Duration::from_secs(2));
|
| 758 |
+
unsafe {
|
| 759 |
+
let hwnd = windows_sys::Win32::UI::WindowsAndMessaging::FindWindowA(
|
| 760 |
+
core::ptr::null::<u8>(),
|
| 761 |
+
b"Worms Armageddon\0".as_ptr(),
|
| 762 |
+
);
|
| 763 |
+
if !hwnd.is_null() {
|
| 764 |
+
windows_sys::Win32::UI::WindowsAndMessaging::ShowWindow(hwnd, 9); // SW_RESTORE
|
| 765 |
+
let _ = log_validation(" Window restored (SW_RESTORE).");
|
| 766 |
+
} else {
|
| 767 |
+
let _ = log_validation(" WARNING: Could not find WA window to restore.");
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
// Wait for gameplay to start, then run initial validation
|
| 772 |
+
std::thread::sleep(std::time::Duration::from_secs(3));
|
| 773 |
let _ = log_validation(" Running deferred global validation...");
|
| 774 |
deferred_global_validation();
|
| 775 |
|