paavohtl Claude Opus 4.6 commited on
Commit
6c0f448
·
1 Parent(s): 23a00c8

feat: replace PostMessage replay fast-forward with DDGame+0x98B0 hook

Browse files

Hook 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
- - 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:
@@ -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: 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`
 
 
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 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,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: 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
 
 
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-0x98B7: 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
- pub _unknown_8154: [u8; 0x98B8 - 0x8154],
 
 
 
 
 
 
 
 
 
 
 
 
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
- //! 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> {
@@ -55,72 +69,16 @@ pub fn install() -> Result<(), String> {
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
  }
 
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(5));
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