paavohtl Claude Opus 4.6 (1M context) commited on
Commit
cbcd5f7
·
1 Parent(s): dae59f3

fix: suppress loading dialog and focus stealing in headless mode

Browse files

Skip the "Loading, please wait..." splash dialog entirely by patching
a JNZ→JMP at 0x4E9F20 in Frontend__MainNavigationLoop, so the game
calls ReplayLoader() directly without creating windows or threads.

Hook SetForegroundWindow and SetFocus in user32 as no-ops to prevent
headless WA.exe instances from stealing keyboard focus from other
applications during parallel test runs.

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

crates/openwa-dll/src/replacements/headless.rs CHANGED
@@ -70,6 +70,16 @@ unsafe extern "system" fn hook_messagebox_w(
70
  }
71
  }
72
 
 
 
 
 
 
 
 
 
 
 
73
  pub fn install() -> Result<(), String> {
74
  if std::env::var("OPENWA_HEADLESS").is_err() {
75
  return Ok(());
@@ -77,6 +87,52 @@ pub fn install() -> Result<(), String> {
77
 
78
  let _ = log_line("[Headless] Suppressing all message boxes");
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  unsafe {
81
  let module = windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(
82
  c"user32.dll".as_ptr().cast(),
@@ -94,6 +150,11 @@ pub fn install() -> Result<(), String> {
94
  &b"MessageBoxW\0"[..],
95
  hook_messagebox_w as *mut core::ffi::c_void,
96
  ),
 
 
 
 
 
97
  ] {
98
  let proc =
99
  windows_sys::Win32::System::LibraryLoader::GetProcAddress(module, name.as_ptr());
@@ -111,7 +172,7 @@ pub fn install() -> Result<(), String> {
111
  }
112
  }
113
 
114
- let _ = log_line("[Headless] All message box paths suppressed");
115
 
116
  // Hook CreateSemaphoreA to rename the "Worms Armageddon" instance
117
  // semaphore per-PID. Without this, concurrent WA.exe instances detect
 
70
  }
71
  }
72
 
73
+ /// Suppress focus stealing — headless processes should never grab foreground.
74
+ unsafe extern "system" fn hook_set_foreground_window(_hwnd: u32) -> i32 {
75
+ 1 // pretend success
76
+ }
77
+
78
+ /// Suppress SetFocus — headless processes don't need keyboard focus.
79
+ unsafe extern "system" fn hook_set_focus(_hwnd: u32) -> u32 {
80
+ 0 // "previous focus window" = NULL
81
+ }
82
+
83
  pub fn install() -> Result<(), String> {
84
  if std::env::var("OPENWA_HEADLESS").is_err() {
85
  return Ok(());
 
87
 
88
  let _ = log_line("[Headless] Suppressing all message boxes");
89
 
90
+ // Skip the "Loading, please wait..." splash dialog entirely.
91
+ //
92
+ // In Frontend__MainNavigationLoop at VA 0x004E9F19, a flag at
93
+ // CWinApp+0x651 controls whether the loading dialog is created.
94
+ // When zero (default), the code creates a dialog (resource 0x135D),
95
+ // shows it, spawns a loading thread gated on WM_PAINT, and runs a
96
+ // message pump. When non-zero, it calls ReplayLoader() directly —
97
+ // no window, no thread, no focus steal.
98
+ //
99
+ // At VA 0x004E9F20 there's a `JNZ +0x1C7` (0F 85 C7 01 00 00) that
100
+ // skips the dialog path. Patch it to an unconditional JMP so the
101
+ // dialog is never created in headless mode.
102
+ unsafe {
103
+ use openwa_core::rebase::rb;
104
+ let patch_addr = rb(0x004E_9F20) as *mut u8;
105
+ let mut old_protect: u32 = 0;
106
+ let ok = windows_sys::Win32::System::Memory::VirtualProtect(
107
+ patch_addr as *mut core::ffi::c_void,
108
+ 6,
109
+ 0x40, // PAGE_EXECUTE_READWRITE
110
+ &mut old_protect,
111
+ );
112
+ if ok != 0 {
113
+ assert_eq!(
114
+ core::slice::from_raw_parts(patch_addr, 2),
115
+ &[0x0F, 0x85],
116
+ "expected JNZ (0F 85) at patch site"
117
+ );
118
+ // JNZ rel32 (6 bytes) → JMP rel32 (5 bytes) + NOP (1 byte)
119
+ // Offset adjusts +1 because JMP is 1 byte shorter than JNZ.
120
+ *patch_addr = 0xE9; // JMP rel32
121
+ *patch_addr.add(1) = 0xC8;
122
+ *patch_addr.add(2) = 0x01;
123
+ *patch_addr.add(3) = 0x00;
124
+ *patch_addr.add(4) = 0x00;
125
+ *patch_addr.add(5) = 0x90; // NOP
126
+ windows_sys::Win32::System::Memory::VirtualProtect(
127
+ patch_addr as *mut core::ffi::c_void,
128
+ 6,
129
+ old_protect,
130
+ &mut old_protect,
131
+ );
132
+ let _ = log_line("[Headless] Patched loading dialog: JNZ → JMP (dialog skipped)");
133
+ }
134
+ }
135
+
136
  unsafe {
137
  let module = windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(
138
  c"user32.dll".as_ptr().cast(),
 
150
  &b"MessageBoxW\0"[..],
151
  hook_messagebox_w as *mut core::ffi::c_void,
152
  ),
153
+ (
154
+ &b"SetForegroundWindow\0"[..],
155
+ hook_set_foreground_window as *mut core::ffi::c_void,
156
+ ),
157
+ (&b"SetFocus\0"[..], hook_set_focus as *mut core::ffi::c_void),
158
  ] {
159
  let proc =
160
  windows_sys::Win32::System::LibraryLoader::GetProcAddress(module, name.as_ptr());
 
172
  }
173
  }
174
 
175
+ let _ = log_line("[Headless] All message box and focus paths suppressed");
176
 
177
  // Hook CreateSemaphoreA to rename the "Worms Armageddon" instance
178
  // semaphore per-PID. Without this, concurrent WA.exe instances detect