paavohtl Claude Sonnet 4.6 commited on
Commit
074e202
·
1 Parent(s): 229ceef

feat: replace WormKit injection with custom launcher (openwa-launcher)

Browse files

New binary crate `openwa-launcher` creates WA.exe in a suspended state,
injects openwa_wormkit.dll via CreateRemoteThread(LoadLibraryA), waits for
DllMain to complete (all hooks installed), then resumes the main thread.
This guarantees hooks are in place before a single WA instruction runs and
requires zero modifications to the game folder.

- WA.exe location: CLI arg > OPENWA_WA_PATH env var > Steam registry
- DLL location: always next to the launcher executable
- Forwards all CLI args (replay file etc.) to WA.exe
- Supports --minimized flag (used by replay-test.ps1)
- Remove deploy.ps1 / deploy-debug.ps1 (no longer needed)
- Update replay-test.ps1 to build + run via launcher; removes DLL copy step
- Update start-debug.ps1 to invoke launcher directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Cargo.lock CHANGED
@@ -1433,6 +1433,13 @@ dependencies = [
1433
  "windows-sys 0.59.0",
1434
  ]
1435
 
 
 
 
 
 
 
 
1436
  [[package]]
1437
  name = "openwa-wormkit"
1438
  version = "0.1.0"
 
1433
  "windows-sys 0.59.0",
1434
  ]
1435
 
1436
+ [[package]]
1437
+ name = "openwa-launcher"
1438
+ version = "0.1.0"
1439
+ dependencies = [
1440
+ "windows-sys 0.59.0",
1441
+ ]
1442
+
1443
  [[package]]
1444
  name = "openwa-wormkit"
1445
  version = "0.1.0"
crates/openwa-launcher/Cargo.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "openwa-launcher"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Launcher that injects openwa_wormkit.dll into WA.exe before its main thread runs"
6
+
7
+ [[bin]]
8
+ name = "openwa-launcher"
9
+ path = "src/main.rs"
10
+
11
+ [dependencies]
12
+ windows-sys = { version = "0.59", features = [
13
+ "Win32_Foundation",
14
+ "Win32_System_Threading",
15
+ "Win32_System_Memory",
16
+ "Win32_System_LibraryLoader",
17
+ "Win32_System_Registry",
18
+ "Win32_System_Diagnostics_Debug",
19
+ ] }
crates/openwa-launcher/src/inject.rs ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::ffi::CString;
2
+ use std::mem;
3
+ use std::ptr;
4
+
5
+ // Declare only what we need directly, avoiding windows-sys feature-flag issues.
6
+ #[allow(non_camel_case_types)]
7
+ type HANDLE = *mut core::ffi::c_void;
8
+ #[allow(non_camel_case_types)]
9
+ type LPVOID = *mut core::ffi::c_void;
10
+ #[allow(non_camel_case_types)]
11
+ type LPCVOID = *const core::ffi::c_void;
12
+ #[allow(non_camel_case_types)]
13
+ type SIZE_T = usize;
14
+ #[allow(non_camel_case_types)]
15
+ type DWORD = u32;
16
+ #[allow(non_camel_case_types)]
17
+ type BOOL = i32;
18
+ type FARPROC = unsafe extern "system" fn() -> isize;
19
+
20
+ extern "system" {
21
+ fn VirtualAllocEx(
22
+ hProcess: HANDLE, lpAddress: LPVOID, dwSize: SIZE_T,
23
+ flAllocationType: DWORD, flProtect: DWORD,
24
+ ) -> LPVOID;
25
+ fn VirtualFreeEx(hProcess: HANDLE, lpAddress: LPVOID, dwSize: SIZE_T, dwFreeType: DWORD) -> BOOL;
26
+ fn WriteProcessMemory(
27
+ hProcess: HANDLE, lpBaseAddress: LPVOID, lpBuffer: LPCVOID,
28
+ nSize: SIZE_T, lpNumberOfBytesWritten: *mut SIZE_T,
29
+ ) -> BOOL;
30
+ fn GetModuleHandleA(lpModuleName: *const u8) -> HANDLE;
31
+ fn GetProcAddress(hModule: HANDLE, lpProcName: *const u8) -> Option<FARPROC>;
32
+ fn CreateRemoteThread(
33
+ hProcess: HANDLE, lpThreadAttributes: LPVOID, dwStackSize: SIZE_T,
34
+ lpStartAddress: unsafe extern "system" fn(LPVOID) -> DWORD,
35
+ lpParameter: LPVOID, dwCreationFlags: DWORD, lpThreadId: *mut DWORD,
36
+ ) -> HANDLE;
37
+ fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD;
38
+ fn CloseHandle(hObject: HANDLE) -> BOOL;
39
+ }
40
+
41
+ const MEM_COMMIT: DWORD = 0x1000;
42
+ const MEM_RESERVE: DWORD = 0x2000;
43
+ const MEM_RELEASE: DWORD = 0x8000;
44
+ const PAGE_READWRITE: DWORD = 0x04;
45
+ const INFINITE: DWORD = 0xFFFF_FFFF;
46
+
47
+ /// Inject `dll_path` into `process` by creating a remote thread that calls `LoadLibraryA`.
48
+ ///
49
+ /// Blocks until `DllMain` returns (i.e. all hooks are installed) before returning,
50
+ /// so the caller can safely resume the main thread.
51
+ pub unsafe fn inject_dll(process: HANDLE, dll_path: &str) -> Result<(), String> {
52
+ let path_cstr = CString::new(dll_path)
53
+ .map_err(|e| format!("bad DLL path: {e}"))?;
54
+ let path_bytes = path_cstr.as_bytes_with_nul();
55
+
56
+ // Allocate memory in the target process for the DLL path string.
57
+ let remote_buf = VirtualAllocEx(
58
+ process, ptr::null_mut(), path_bytes.len(),
59
+ MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE,
60
+ );
61
+ if remote_buf.is_null() {
62
+ return Err("VirtualAllocEx failed".to_string());
63
+ }
64
+
65
+ // Write the path into the target process.
66
+ let ok = WriteProcessMemory(
67
+ process, remote_buf,
68
+ path_bytes.as_ptr().cast(), path_bytes.len(),
69
+ ptr::null_mut(),
70
+ );
71
+ if ok == 0 {
72
+ VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
73
+ return Err("WriteProcessMemory failed".to_string());
74
+ }
75
+
76
+ // LoadLibraryA lives at the same address in every process (kernel32 is always mapped
77
+ // at the same base across all processes on the same OS session due to ASLR sharing).
78
+ let k32 = GetModuleHandleA(b"kernel32.dll\0".as_ptr());
79
+ if k32.is_null() {
80
+ VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
81
+ return Err("GetModuleHandleA(kernel32) failed".to_string());
82
+ }
83
+ let load_library = GetProcAddress(k32, b"LoadLibraryA\0".as_ptr());
84
+ let Some(load_library) = load_library else {
85
+ VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
86
+ return Err("GetProcAddress(LoadLibraryA) failed".to_string());
87
+ };
88
+
89
+ // Spin up a remote thread that calls LoadLibraryA(dll_path).
90
+ // This triggers DllMain(DLL_PROCESS_ATTACH) which installs all our hooks.
91
+ let thread = CreateRemoteThread(
92
+ process, ptr::null_mut(), 0,
93
+ mem::transmute(load_library),
94
+ remote_buf, 0, ptr::null_mut(),
95
+ );
96
+ if thread.is_null() {
97
+ VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
98
+ return Err("CreateRemoteThread failed".to_string());
99
+ }
100
+
101
+ // Wait for DllMain to finish — hooks are installed when this returns.
102
+ WaitForSingleObject(thread, INFINITE);
103
+ CloseHandle(thread);
104
+ VirtualFreeEx(process, remote_buf, 0, MEM_RELEASE);
105
+
106
+ Ok(())
107
+ }
crates/openwa-launcher/src/main.rs ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ mod inject;
2
+
3
+ use std::ffi::CString;
4
+ use std::mem;
5
+ use std::path::{Path, PathBuf};
6
+ use std::ptr;
7
+
8
+ // Declare Win32 types and functions directly to avoid windows-sys feature-flag issues.
9
+ #[allow(non_camel_case_types)] type HANDLE = *mut core::ffi::c_void;
10
+ #[allow(non_camel_case_types)] type HKEY = *mut core::ffi::c_void;
11
+ #[allow(non_camel_case_types)] type HMODULE = *mut core::ffi::c_void;
12
+ #[allow(non_camel_case_types)] type DWORD = u32;
13
+ #[allow(non_camel_case_types)] type BOOL = i32;
14
+ #[allow(non_camel_case_types)] type LONG = i32;
15
+ #[allow(non_camel_case_types)] type LPVOID = *mut core::ffi::c_void;
16
+ #[allow(non_camel_case_types)] type LPDWORD = *mut u32;
17
+
18
+ #[repr(C)]
19
+ #[allow(non_snake_case)]
20
+ struct STARTUPINFOA {
21
+ cb: DWORD, lpReserved: *mut u8, lpDesktop: *mut u8, lpTitle: *mut u8,
22
+ dwX: DWORD, dwY: DWORD, dwXSize: DWORD, dwYSize: DWORD,
23
+ dwXCountChars: DWORD, dwYCountChars: DWORD, dwFillAttribute: DWORD,
24
+ dwFlags: DWORD, wShowWindow: u16, cbReserved2: u16,
25
+ lpReserved2: *mut u8, hStdInput: HANDLE, hStdOutput: HANDLE, hStdError: HANDLE,
26
+ }
27
+
28
+ #[repr(C)]
29
+ #[allow(non_snake_case)]
30
+ struct PROCESS_INFORMATION {
31
+ hProcess: HANDLE, hThread: HANDLE, dwProcessId: DWORD, dwThreadId: DWORD,
32
+ }
33
+
34
+ const CREATE_SUSPENDED: DWORD = 0x0000_0004;
35
+ const STARTF_USESHOWWINDOW: DWORD = 0x0000_0001;
36
+ const SW_SHOWMINIMIZED: u16 = 2;
37
+ const INFINITE: DWORD = 0xFFFF_FFFF;
38
+ const HKEY_LOCAL_MACHINE: HKEY = -2isize as HKEY;
39
+ const HKEY_CURRENT_USER: HKEY = -1isize as HKEY;
40
+ const KEY_READ: DWORD = 0x2_0019;
41
+ const REG_SZ: DWORD = 1;
42
+
43
+ #[link(name = "kernel32")]
44
+ extern "system" {
45
+ fn CreateProcessA(
46
+ lpApplicationName: *const u8, lpCommandLine: *mut u8,
47
+ lpProcessAttributes: LPVOID, lpThreadAttributes: LPVOID,
48
+ bInheritHandles: BOOL, dwCreationFlags: DWORD,
49
+ lpEnvironment: LPVOID, lpCurrentDirectory: *const u8,
50
+ lpStartupInfo: *const STARTUPINFOA,
51
+ lpProcessInformation: *mut PROCESS_INFORMATION,
52
+ ) -> BOOL;
53
+ fn ResumeThread(hThread: HANDLE) -> DWORD;
54
+ fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD;
55
+ fn GetExitCodeProcess(hProcess: HANDLE, lpExitCode: LPDWORD) -> BOOL;
56
+ fn TerminateProcess(hProcess: HANDLE, uExitCode: u32) -> BOOL;
57
+ fn CloseHandle(hObject: HANDLE) -> BOOL;
58
+ fn GetModuleFileNameA(hModule: HMODULE, lpFilename: *mut u8, nSize: DWORD) -> DWORD;
59
+ }
60
+
61
+ #[link(name = "advapi32")]
62
+ extern "system" {
63
+ fn RegOpenKeyExA(
64
+ hKey: HKEY, lpSubKey: *const u8, ulOptions: DWORD,
65
+ samDesired: DWORD, phkResult: *mut HKEY,
66
+ ) -> LONG;
67
+ fn RegQueryValueExA(
68
+ hKey: HKEY, lpValueName: *const u8, lpReserved: *const DWORD,
69
+ lpType: *mut DWORD, lpData: *mut u8, lpcbData: *mut DWORD,
70
+ ) -> LONG;
71
+ fn RegCloseKey(hKey: HKEY) -> LONG;
72
+ }
73
+
74
+ fn main() {
75
+ if let Err(e) = run() {
76
+ eprintln!("openwa-launcher: error: {e}");
77
+ std::process::exit(1);
78
+ }
79
+ }
80
+
81
+ fn run() -> Result<(), String> {
82
+ let args: Vec<String> = std::env::args().collect();
83
+
84
+ // Parse --minimized flag; collect remaining args as WA.exe args.
85
+ let mut minimized = false;
86
+ let mut wa_path_arg: Option<String> = None;
87
+ let mut wa_args: Vec<String> = Vec::new();
88
+
89
+ for arg in args.iter().skip(1) {
90
+ match arg.as_str() {
91
+ "--minimized" => minimized = true,
92
+ s if wa_path_arg.is_none() && !s.starts_with("--") => {
93
+ wa_path_arg = Some(s.to_string());
94
+ }
95
+ s => wa_args.push(s.to_string()),
96
+ }
97
+ }
98
+
99
+ // Locate WA.exe: explicit arg > env var > Steam registry.
100
+ let wa_exe = if let Some(p) = wa_path_arg {
101
+ PathBuf::from(p)
102
+ } else if let Ok(p) = std::env::var("OPENWA_WA_PATH") {
103
+ PathBuf::from(p)
104
+ } else {
105
+ find_wa_via_steam()
106
+ .ok_or("WA.exe not found. Set OPENWA_WA_PATH or pass the path as an argument.")?
107
+ };
108
+
109
+ if !wa_exe.exists() {
110
+ return Err(format!("WA.exe not found at: {}", wa_exe.display()));
111
+ }
112
+
113
+ // Locate the DLL: same directory as the launcher executable.
114
+ let dll_path = launcher_dir()?.join("openwa_wormkit.dll");
115
+ if !dll_path.exists() {
116
+ return Err(format!(
117
+ "openwa_wormkit.dll not found at: {}\nBuild with: cargo build --release -p openwa-wormkit",
118
+ dll_path.display()
119
+ ));
120
+ }
121
+
122
+ // WA.exe must run with its own folder as the working directory (relative paths for data/logs).
123
+ let wa_dir = wa_exe
124
+ .parent()
125
+ .ok_or("WA.exe path has no parent directory")?;
126
+
127
+ let cmdline = build_cmdline(&wa_exe, &wa_args);
128
+
129
+ println!(
130
+ "openwa-launcher: {} + {}",
131
+ wa_exe.display(),
132
+ dll_path.display()
133
+ );
134
+
135
+ unsafe { launch(&wa_exe, &cmdline, wa_dir, &dll_path, minimized) }
136
+ }
137
+
138
+ unsafe fn launch(
139
+ wa_exe: &Path,
140
+ cmdline: &str,
141
+ working_dir: &Path,
142
+ dll_path: &Path,
143
+ minimized: bool,
144
+ ) -> Result<(), String> {
145
+ let exe_cstr = path_to_cstring(wa_exe)?;
146
+ let mut cmdline_buf: Vec<u8> = cmdline.bytes().chain(std::iter::once(0u8)).collect();
147
+ let wd_cstr = path_to_cstring(working_dir)?;
148
+
149
+ let mut si: STARTUPINFOA = mem::zeroed();
150
+ si.cb = mem::size_of::<STARTUPINFOA>() as DWORD;
151
+ if minimized {
152
+ si.dwFlags |= STARTF_USESHOWWINDOW;
153
+ si.wShowWindow = SW_SHOWMINIMIZED;
154
+ }
155
+
156
+ let mut pi: PROCESS_INFORMATION = mem::zeroed();
157
+
158
+ let ok = CreateProcessA(
159
+ exe_cstr.as_ptr().cast(),
160
+ cmdline_buf.as_mut_ptr(),
161
+ ptr::null_mut(), ptr::null_mut(),
162
+ 0, // bInheritHandles = FALSE
163
+ CREATE_SUSPENDED,
164
+ ptr::null_mut(), // inherit environment
165
+ wd_cstr.as_ptr().cast(),
166
+ &si,
167
+ &mut pi,
168
+ );
169
+
170
+ if ok == 0 {
171
+ return Err("CreateProcessA failed — is WA.exe path correct?".to_string());
172
+ }
173
+
174
+ let dll_str = dll_path
175
+ .to_str()
176
+ .ok_or("DLL path is not valid UTF-8")?;
177
+
178
+ if let Err(e) = inject::inject_dll(pi.hProcess, dll_str) {
179
+ TerminateProcess(pi.hProcess, 1);
180
+ CloseHandle(pi.hProcess);
181
+ CloseHandle(pi.hThread);
182
+ return Err(format!("DLL injection failed: {e}"));
183
+ }
184
+
185
+ // All hooks installed — let WA.exe run.
186
+ ResumeThread(pi.hThread);
187
+ CloseHandle(pi.hThread);
188
+
189
+ // Wait for WA.exe to exit, then propagate its exit code.
190
+ WaitForSingleObject(pi.hProcess, INFINITE);
191
+ let mut exit_code: DWORD = 0;
192
+ GetExitCodeProcess(pi.hProcess, &mut exit_code);
193
+ CloseHandle(pi.hProcess);
194
+
195
+ std::process::exit(exit_code as i32);
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Helpers
200
+ // ---------------------------------------------------------------------------
201
+
202
+ fn launcher_dir() -> Result<PathBuf, String> {
203
+ let mut buf = vec![0u8; 32768];
204
+ let len = unsafe {
205
+ GetModuleFileNameA(ptr::null_mut(), buf.as_mut_ptr(), buf.len() as DWORD)
206
+ };
207
+ if len == 0 {
208
+ return Err("GetModuleFileNameA failed".to_string());
209
+ }
210
+ let path = PathBuf::from(
211
+ std::str::from_utf8(&buf[..len as usize])
212
+ .map_err(|e| format!("launcher path not UTF-8: {e}"))?,
213
+ );
214
+ path.parent()
215
+ .map(|p| p.to_path_buf())
216
+ .ok_or_else(|| "launcher path has no parent".to_string())
217
+ }
218
+
219
+ fn build_cmdline(exe: &Path, extra: &[String]) -> String {
220
+ let mut s = format!("\"{}\"", exe.display());
221
+ for arg in extra {
222
+ s.push(' ');
223
+ if arg.contains(' ') {
224
+ s.push('"');
225
+ s.push_str(arg);
226
+ s.push('"');
227
+ } else {
228
+ s.push_str(arg);
229
+ }
230
+ }
231
+ s
232
+ }
233
+
234
+ fn path_to_cstring(p: &Path) -> Result<CString, String> {
235
+ CString::new(
236
+ p.to_str()
237
+ .ok_or_else(|| format!("path not valid UTF-8: {}", p.display()))?,
238
+ )
239
+ .map_err(|e| format!("path contains nul byte: {e}"))
240
+ }
241
+
242
+ /// Try to find WA.exe via the Steam app registry entry (App ID 217200).
243
+ fn find_wa_via_steam() -> Option<PathBuf> {
244
+ let candidates: &[(HKEY, &[u8])] = &[
245
+ (
246
+ HKEY_LOCAL_MACHINE,
247
+ b"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 217200\0",
248
+ ),
249
+ (
250
+ HKEY_LOCAL_MACHINE,
251
+ b"SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 217200\0",
252
+ ),
253
+ (
254
+ HKEY_CURRENT_USER,
255
+ b"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 217200\0",
256
+ ),
257
+ ];
258
+
259
+ for &(hive, subkey) in candidates {
260
+ if let Some(install_dir) = read_reg_sz(hive, subkey, b"InstallLocation\0") {
261
+ let wa = PathBuf::from(&install_dir).join("WA.exe");
262
+ if wa.exists() {
263
+ return Some(wa);
264
+ }
265
+ }
266
+ }
267
+ None
268
+ }
269
+
270
+ fn read_reg_sz(hive: HKEY, subkey: &[u8], value: &[u8]) -> Option<String> {
271
+ unsafe {
272
+ let mut hkey: HKEY = ptr::null_mut();
273
+ let ret = RegOpenKeyExA(hive, subkey.as_ptr(), 0, KEY_READ, &mut hkey);
274
+ if ret != 0 {
275
+ return None;
276
+ }
277
+
278
+ let mut buf = vec![0u8; 4096];
279
+ let mut buf_len = buf.len() as DWORD;
280
+ let mut reg_type: DWORD = 0;
281
+
282
+ let ret = RegQueryValueExA(
283
+ hkey,
284
+ value.as_ptr(),
285
+ ptr::null(),
286
+ &mut reg_type,
287
+ buf.as_mut_ptr(),
288
+ &mut buf_len,
289
+ );
290
+ RegCloseKey(hkey);
291
+
292
+ if ret != 0 || reg_type != REG_SZ {
293
+ return None;
294
+ }
295
+
296
+ let s = std::str::from_utf8(&buf[..buf_len as usize])
297
+ .ok()?
298
+ .trim_end_matches('\0')
299
+ .to_string();
300
+ Some(s)
301
+ }
302
+ }
deploy-debug.ps1 DELETED
@@ -1,13 +0,0 @@
1
- $dest = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon"
2
-
3
- cargo build --release -p openwa-wormkit --features debug-ui
4
- if ($LASTEXITCODE -ne 0) { exit 1 }
5
-
6
- $src = "target\i686-pc-windows-msvc\release"
7
- Copy-Item "$src\openwa_wormkit.dll" "$dest\wkOpenWA.dll"
8
-
9
- Write-Host "Deployed debug-ui build to $dest"
10
- Write-Host "Launch WA.exe with OPENWA_DEBUG_UI=1 to show the debug window."
11
- Write-Host ""
12
- Write-Host "Example:"
13
- Write-Host ' $env:OPENWA_DEBUG_UI=1; & "I:\games\SteamLibrary\steamapps\common\Worms Armageddon\WA.exe"'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy.ps1 DELETED
@@ -1,9 +0,0 @@
1
- $dest = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon"
2
-
3
- cargo build --release -p openwa-wormkit
4
- if ($LASTEXITCODE -ne 0) { exit 1 }
5
-
6
- $src = "target\i686-pc-windows-msvc\release"
7
- Copy-Item "$src\openwa_wormkit.dll" "$dest\wkOpenWA.dll"
8
-
9
- Write-Host "Deployed to $dest"
 
 
 
 
 
 
 
 
 
 
replay-test.ps1 CHANGED
@@ -7,10 +7,10 @@ param(
7
 
8
  $ErrorActionPreference = "Stop"
9
 
10
- $gameDir = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon"
11
- $waExe = "$gameDir\WA.exe"
12
- $src = "target\i686-pc-windows-msvc\release"
13
- $logDir = "testdata\logs"
14
 
15
  # Resolve replay file to absolute path
16
  if (-not [System.IO.Path]::IsPathRooted($ReplayFile)) {
@@ -22,43 +22,49 @@ if (-not (Test-Path $ReplayFile)) {
22
  exit 1
23
  }
24
 
25
- # 1. Build (single unified DLL)
26
  Write-Host "Building..." -ForegroundColor Cyan
27
- cargo build --release -p openwa-wormkit
28
  if ($LASTEXITCODE -ne 0) { exit 1 }
29
 
30
- # 2. Deploy
31
- Write-Host "Deploying to $gameDir..." -ForegroundColor Cyan
32
- Copy-Item "$src\openwa_wormkit.dll" "$gameDir\wkOpenWA.dll"
33
 
34
- # Remove old validator DLL if present (now unified)
 
 
 
 
 
 
 
 
35
  if (Test-Path "$gameDir\wkOpenWAValidator.dll") {
36
  Remove-Item "$gameDir\wkOpenWAValidator.dll"
37
  }
38
 
39
- # 3. Clear old logs
40
- Remove-Item "$gameDir\OpenWA.log" -ErrorAction SilentlyContinue
41
- 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 " (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
 
59
- Write-Host "WA.exe exited with code $($proc.ExitCode)" -ForegroundColor Cyan
60
 
61
- # 5. Copy logs back
62
  if (-not (Test-Path $logDir)) {
63
  New-Item -ItemType Directory -Path $logDir | Out-Null
64
  }
@@ -79,13 +85,13 @@ if (Test-Path "$gameDir\OpenWA.log") {
79
  Write-Host "OpenWA log copied." -ForegroundColor Green
80
  }
81
 
82
- # 6. Print summary
83
  Write-Host ""
84
  Write-Host "=== Validation Summary ===" -ForegroundColor Cyan
85
  if (Test-Path "$logDir\validation_latest.log") {
86
  $content = Get-Content "$logDir\validation_latest.log" -Raw
87
  $passes = ([regex]::Matches($content, "\[PASS\]")).Count
88
- $fails = ([regex]::Matches($content, "\[FAIL\]")).Count
89
  Write-Host " PASS: $passes" -ForegroundColor Green
90
  Write-Host " FAIL: $fails" -ForegroundColor $(if ($fails -gt 0) { "Red" } else { "Green" })
91
 
 
7
 
8
  $ErrorActionPreference = "Stop"
9
 
10
+ $waExe = if ($env:OPENWA_WA_PATH) { $env:OPENWA_WA_PATH } else { "I:\games\SteamLibrary\steamapps\common\Worms Armageddon\WA.exe" }
11
+ $gameDir = Split-Path $waExe
12
+ $src = "target\i686-pc-windows-msvc\release"
13
+ $logDir = "testdata\logs"
14
 
15
  # Resolve replay file to absolute path
16
  if (-not [System.IO.Path]::IsPathRooted($ReplayFile)) {
 
22
  exit 1
23
  }
24
 
25
+ # 1. Build launcher + DLL
26
  Write-Host "Building..." -ForegroundColor Cyan
27
+ cargo build --release -p openwa-launcher -p openwa-wormkit
28
  if ($LASTEXITCODE -ne 0) { exit 1 }
29
 
30
+ $launcher = "$src\openwa-launcher.exe"
 
 
31
 
32
+ # 2. Clear old logs
33
+ Remove-Item "$gameDir\OpenWA.log" -ErrorAction SilentlyContinue
34
+ Remove-Item "$gameDir\OpenWA_validation.log" -ErrorAction SilentlyContinue
35
+
36
+ # Remove old WormKit DLL if present from a previous deployment
37
+ if (Test-Path "$gameDir\wkOpenWA.dll") {
38
+ Write-Host "Note: removing legacy $gameDir\wkOpenWA.dll" -ForegroundColor Yellow
39
+ Remove-Item "$gameDir\wkOpenWA.dll"
40
+ }
41
  if (Test-Path "$gameDir\wkOpenWAValidator.dll") {
42
  Remove-Item "$gameDir\wkOpenWAValidator.dll"
43
  }
44
 
45
+ # 3. Launch via launcher with replay, env vars set
 
 
 
 
46
  Write-Host "Launching WA.exe with replay: $ReplayFile" -ForegroundColor Cyan
47
  Write-Host " (Fast-forward mode: replay will be auto-advanced, game exits when done)" -ForegroundColor Yellow
48
 
49
+ $env:OPENWA_VALIDATE = "1"
50
  $env:OPENWA_REPLAY_TEST = "1"
51
+ $env:OPENWA_WA_PATH = $waExe
52
+
53
+ # The launcher blocks until WA.exe exits, so run it as a background job for timeout support.
54
+ $proc = Start-Process -FilePath $launcher -ArgumentList "--minimized `"$ReplayFile`"" -PassThru
55
+ $timeout = 150 # seconds
56
  if (-not $proc.WaitForExit($timeout * 1000)) {
57
+ Write-Host "WARNING: launcher did not exit within ${timeout}s, killing..." -ForegroundColor Red
58
  $proc.Kill()
59
  $proc.WaitForExit(5000)
60
  }
61
+ $env:OPENWA_VALIDATE = $null
62
  $env:OPENWA_REPLAY_TEST = $null
63
+ $env:OPENWA_WA_PATH = $null
64
 
65
+ Write-Host "Launcher exited with code $($proc.ExitCode)" -ForegroundColor Cyan
66
 
67
+ # 4. Copy logs back
68
  if (-not (Test-Path $logDir)) {
69
  New-Item -ItemType Directory -Path $logDir | Out-Null
70
  }
 
85
  Write-Host "OpenWA log copied." -ForegroundColor Green
86
  }
87
 
88
+ # 5. Print summary
89
  Write-Host ""
90
  Write-Host "=== Validation Summary ===" -ForegroundColor Cyan
91
  if (Test-Path "$logDir\validation_latest.log") {
92
  $content = Get-Content "$logDir\validation_latest.log" -Raw
93
  $passes = ([regex]::Matches($content, "\[PASS\]")).Count
94
+ $fails = ([regex]::Matches($content, "\[FAIL\]")).Count
95
  Write-Host " PASS: $passes" -ForegroundColor Green
96
  Write-Host " FAIL: $fails" -ForegroundColor $(if ($fails -gt 0) { "Red" } else { "Green" })
97
 
start-debug.ps1 CHANGED
@@ -1,4 +1,2 @@
1
- $wa = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon\WA.exe"
2
-
3
  $env:OPENWA_DEBUG_UI = "1"
4
- & $wa
 
 
 
1
  $env:OPENWA_DEBUG_UI = "1"
2
+ & "target\i686-pc-windows-msvc\release\openwa-launcher.exe"