feat: replace WormKit injection with custom launcher (openwa-launcher)
Browse filesNew 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 +7 -0
- crates/openwa-launcher/Cargo.toml +19 -0
- crates/openwa-launcher/src/inject.rs +107 -0
- crates/openwa-launcher/src/main.rs +302 -0
- deploy-debug.ps1 +0 -13
- deploy.ps1 +0 -9
- replay-test.ps1 +30 -24
- start-debug.ps1 +1 -3
|
@@ -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"
|
|
@@ -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 |
+
] }
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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"'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -7,10 +7,10 @@ param(
|
|
| 7 |
|
| 8 |
$ErrorActionPreference = "Stop"
|
| 9 |
|
| 10 |
-
$
|
| 11 |
-
$
|
| 12 |
-
$src
|
| 13 |
-
$logDir
|
| 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
|
| 26 |
Write-Host "Building..." -ForegroundColor Cyan
|
| 27 |
-
cargo build --release -p openwa-wormkit
|
| 28 |
if ($LASTEXITCODE -ne 0) { exit 1 }
|
| 29 |
|
| 30 |
-
|
| 31 |
-
Write-Host "Deploying to $gameDir..." -ForegroundColor Cyan
|
| 32 |
-
Copy-Item "$src\openwa_wormkit.dll" "$gameDir\wkOpenWA.dll"
|
| 33 |
|
| 34 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
if (Test-Path "$gameDir\wkOpenWAValidator.dll") {
|
| 36 |
Remove-Item "$gameDir\wkOpenWAValidator.dll"
|
| 37 |
}
|
| 38 |
|
| 39 |
-
# 3.
|
| 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
|
| 48 |
$env:OPENWA_REPLAY_TEST = "1"
|
| 49 |
-
$
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
if (-not $proc.WaitForExit($timeout * 1000)) {
|
| 52 |
-
Write-Host "WARNING:
|
| 53 |
$proc.Kill()
|
| 54 |
$proc.WaitForExit(5000)
|
| 55 |
}
|
| 56 |
-
$env:OPENWA_VALIDATE
|
| 57 |
$env:OPENWA_REPLAY_TEST = $null
|
|
|
|
| 58 |
|
| 59 |
-
Write-Host "
|
| 60 |
|
| 61 |
-
#
|
| 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 |
-
#
|
| 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
|
| 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 |
|
|
@@ -1,4 +1,2 @@
|
|
| 1 |
-
$wa = "I:\games\SteamLibrary\steamapps\common\Worms Armageddon\WA.exe"
|
| 2 |
-
|
| 3 |
$env:OPENWA_DEBUG_UI = "1"
|
| 4 |
-
&
|
|
|
|
|
|
|
|
|
|
| 1 |
$env:OPENWA_DEBUG_UI = "1"
|
| 2 |
+
& "target\i686-pc-windows-msvc\release\openwa-launcher.exe"
|