feat: add openwa-config crate with WA installation locator
Browse filesCentralizes game path discovery into a new openwa-config crate.
find_wa_dir() checks OPENWA_WA_PATH env var then falls back to
HKCU\Software\Team17SoftwareLTD\WormsArmageddon (PATH value).
Removes all hardcoded Steam library paths from launcher, test runner,
and scheme_parse integration test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cargo.lock +12 -0
- crates/openwa-config/Cargo.toml +11 -0
- crates/openwa-config/src/lib.rs +83 -0
- crates/openwa-core/Cargo.toml +3 -0
- crates/openwa-core/tests/scheme_parse.rs +5 -2
- crates/openwa-launcher/Cargo.toml +1 -0
- crates/openwa-launcher/src/main.rs +3 -15
- crates/openwa-test-runner/Cargo.toml +1 -0
- crates/openwa-test-runner/src/main.rs +1 -21
Cargo.lock
CHANGED
|
@@ -1469,6 +1469,13 @@ version = "1.21.3"
|
|
| 1469 |
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1470 |
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
| 1471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1472 |
[[package]]
|
| 1473 |
name = "openwa-core"
|
| 1474 |
version = "0.1.0"
|
|
@@ -1476,6 +1483,7 @@ dependencies = [
|
|
| 1476 |
"heapless",
|
| 1477 |
"hound",
|
| 1478 |
"inventory",
|
|
|
|
| 1479 |
"openwa-debug-proto",
|
| 1480 |
"openwa-derive",
|
| 1481 |
"windows",
|
|
@@ -1518,12 +1526,16 @@ dependencies = [
|
|
| 1518 |
[[package]]
|
| 1519 |
name = "openwa-launcher"
|
| 1520 |
version = "0.1.0"
|
|
|
|
|
|
|
|
|
|
| 1521 |
|
| 1522 |
[[package]]
|
| 1523 |
name = "openwa-test-runner"
|
| 1524 |
version = "0.1.0"
|
| 1525 |
dependencies = [
|
| 1526 |
"chrono",
|
|
|
|
| 1527 |
]
|
| 1528 |
|
| 1529 |
[[package]]
|
|
|
|
| 1469 |
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1470 |
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
| 1471 |
|
| 1472 |
+
[[package]]
|
| 1473 |
+
name = "openwa-config"
|
| 1474 |
+
version = "0.1.0"
|
| 1475 |
+
dependencies = [
|
| 1476 |
+
"windows-sys 0.59.0",
|
| 1477 |
+
]
|
| 1478 |
+
|
| 1479 |
[[package]]
|
| 1480 |
name = "openwa-core"
|
| 1481 |
version = "0.1.0"
|
|
|
|
| 1483 |
"heapless",
|
| 1484 |
"hound",
|
| 1485 |
"inventory",
|
| 1486 |
+
"openwa-config",
|
| 1487 |
"openwa-debug-proto",
|
| 1488 |
"openwa-derive",
|
| 1489 |
"windows",
|
|
|
|
| 1526 |
[[package]]
|
| 1527 |
name = "openwa-launcher"
|
| 1528 |
version = "0.1.0"
|
| 1529 |
+
dependencies = [
|
| 1530 |
+
"openwa-config",
|
| 1531 |
+
]
|
| 1532 |
|
| 1533 |
[[package]]
|
| 1534 |
name = "openwa-test-runner"
|
| 1535 |
version = "0.1.0"
|
| 1536 |
dependencies = [
|
| 1537 |
"chrono",
|
| 1538 |
+
"openwa-config",
|
| 1539 |
]
|
| 1540 |
|
| 1541 |
[[package]]
|
crates/openwa-config/Cargo.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "openwa-config"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
edition = "2021"
|
| 5 |
+
description = "Configuration and installation discovery for OpenWA tools"
|
| 6 |
+
|
| 7 |
+
[dependencies]
|
| 8 |
+
windows-sys = { version = "0.59", features = [
|
| 9 |
+
"Win32_Foundation",
|
| 10 |
+
"Win32_System_Registry",
|
| 11 |
+
] }
|
crates/openwa-config/src/lib.rs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Configuration and installation discovery for OpenWA tools.
|
| 2 |
+
|
| 3 |
+
use std::path::PathBuf;
|
| 4 |
+
|
| 5 |
+
use windows_sys::Win32::System::Registry::*;
|
| 6 |
+
|
| 7 |
+
const WA_REGISTRY_KEY: &[u8] = b"Software\\Team17SoftwareLTD\\WormsArmageddon\0";
|
| 8 |
+
const WA_PATH_VALUE: &[u8] = b"PATH\0";
|
| 9 |
+
|
| 10 |
+
/// Locate the Worms Armageddon installation directory.
|
| 11 |
+
///
|
| 12 |
+
/// Search order:
|
| 13 |
+
/// 1. `OPENWA_WA_PATH` environment variable β may be either the WA.exe path or
|
| 14 |
+
/// the installation directory. If it ends with `.exe`, the parent directory
|
| 15 |
+
/// is returned.
|
| 16 |
+
/// 2. Windows registry: `HKEY_CURRENT_USER\Software\Team17SoftwareLTD\WormsArmageddon`,
|
| 17 |
+
/// value `PATH`.
|
| 18 |
+
///
|
| 19 |
+
/// Returns `None` if the installation cannot be located.
|
| 20 |
+
pub fn find_wa_dir() -> Option<PathBuf> {
|
| 21 |
+
if let Some(dir) = from_env() {
|
| 22 |
+
return Some(dir);
|
| 23 |
+
}
|
| 24 |
+
from_registry()
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
fn from_env() -> Option<PathBuf> {
|
| 28 |
+
let val = std::env::var_os("OPENWA_WA_PATH")?;
|
| 29 |
+
let path = PathBuf::from(val);
|
| 30 |
+
// Accept either a WA.exe path or a directory.
|
| 31 |
+
if path.extension().map(|e| e.eq_ignore_ascii_case("exe")).unwrap_or(false) {
|
| 32 |
+
path.parent().map(|p| p.to_path_buf())
|
| 33 |
+
} else {
|
| 34 |
+
Some(path)
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
fn from_registry() -> Option<PathBuf> {
|
| 39 |
+
let mut buf = [0u8; 512];
|
| 40 |
+
let len = read_registry_sz(WA_REGISTRY_KEY, WA_PATH_VALUE, &mut buf)?;
|
| 41 |
+
let s = std::str::from_utf8(&buf[..len]).ok()?;
|
| 42 |
+
if s.is_empty() {
|
| 43 |
+
return None;
|
| 44 |
+
}
|
| 45 |
+
Some(PathBuf::from(s))
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// Read a REG_SZ value from `HKCU\<key>`. Returns the number of bytes written
|
| 49 |
+
/// to `buf` (excluding null terminator), or `None` on any failure.
|
| 50 |
+
fn read_registry_sz(key: &[u8], value: &[u8], buf: &mut [u8]) -> Option<usize> {
|
| 51 |
+
unsafe {
|
| 52 |
+
let mut hkey: HKEY = std::ptr::null_mut();
|
| 53 |
+
let status = RegOpenKeyExA(
|
| 54 |
+
HKEY_CURRENT_USER,
|
| 55 |
+
key.as_ptr(),
|
| 56 |
+
0,
|
| 57 |
+
KEY_READ,
|
| 58 |
+
&mut hkey,
|
| 59 |
+
);
|
| 60 |
+
if status != 0 {
|
| 61 |
+
return None;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
let mut data_size = buf.len() as u32;
|
| 65 |
+
let mut value_type: u32 = 0;
|
| 66 |
+
let status = RegQueryValueExA(
|
| 67 |
+
hkey,
|
| 68 |
+
value.as_ptr(),
|
| 69 |
+
std::ptr::null_mut(),
|
| 70 |
+
&mut value_type,
|
| 71 |
+
buf.as_mut_ptr(),
|
| 72 |
+
&mut data_size,
|
| 73 |
+
);
|
| 74 |
+
RegCloseKey(hkey);
|
| 75 |
+
|
| 76 |
+
if status != 0 || value_type != REG_SZ || data_size == 0 {
|
| 77 |
+
return None;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// data_size includes the null terminator
|
| 81 |
+
Some((data_size - 1) as usize)
|
| 82 |
+
}
|
| 83 |
+
}
|
crates/openwa-core/Cargo.toml
CHANGED
|
@@ -11,6 +11,9 @@ inventory = "0.3.22"
|
|
| 11 |
openwa-derive = { path = "../openwa-derive" }
|
| 12 |
openwa-debug-proto = { path = "../openwa-debug-proto" }
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 15 |
windows = { version = "0.62", features = [
|
| 16 |
"Win32_Media_Audio_DirectSound",
|
|
|
|
| 11 |
openwa-derive = { path = "../openwa-derive" }
|
| 12 |
openwa-debug-proto = { path = "../openwa-debug-proto" }
|
| 13 |
|
| 14 |
+
[dev-dependencies]
|
| 15 |
+
openwa-config = { path = "../openwa-config" }
|
| 16 |
+
|
| 17 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 18 |
windows = { version = "0.62", features = [
|
| 19 |
"Win32_Media_Audio_DirectSound",
|
crates/openwa-core/tests/scheme_parse.rs
CHANGED
|
@@ -181,8 +181,11 @@ fn v2_super_weapons_zeroed_region() {
|
|
| 181 |
/// Parse all .wsc files from the game directory if available.
|
| 182 |
#[test]
|
| 183 |
fn parse_all_game_schemes() {
|
| 184 |
-
let
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
| 186 |
if !schemes_dir.exists() {
|
| 187 |
eprintln!("Skipping: game schemes directory not found");
|
| 188 |
return;
|
|
|
|
| 181 |
/// Parse all .wsc files from the game directory if available.
|
| 182 |
#[test]
|
| 183 |
fn parse_all_game_schemes() {
|
| 184 |
+
let Some(wa_dir) = openwa_config::find_wa_dir() else {
|
| 185 |
+
eprintln!("Skipping: WA installation not found");
|
| 186 |
+
return;
|
| 187 |
+
};
|
| 188 |
+
let schemes_dir = wa_dir.join("User/Schemes");
|
| 189 |
if !schemes_dir.exists() {
|
| 190 |
eprintln!("Skipping: game schemes directory not found");
|
| 191 |
return;
|
crates/openwa-launcher/Cargo.toml
CHANGED
|
@@ -9,3 +9,4 @@ name = "openwa-launcher"
|
|
| 9 |
path = "src/main.rs"
|
| 10 |
|
| 11 |
[dependencies]
|
|
|
|
|
|
| 9 |
path = "src/main.rs"
|
| 10 |
|
| 11 |
[dependencies]
|
| 12 |
+
openwa-config = { path = "../openwa-config" }
|
crates/openwa-launcher/src/main.rs
CHANGED
|
@@ -111,13 +111,12 @@ fn run() -> Result<(), String> {
|
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
-
// Locate WA.exe: explicit arg > env var
|
| 115 |
let wa_exe = if let Some(p) = wa_path_arg {
|
| 116 |
PathBuf::from(p)
|
| 117 |
-
} else if let Ok(p) = std::env::var("OPENWA_WA_PATH") {
|
| 118 |
-
PathBuf::from(p)
|
| 119 |
} else {
|
| 120 |
-
|
|
|
|
| 121 |
.ok_or("WA.exe not found. Set OPENWA_WA_PATH or pass the path as an argument.")?
|
| 122 |
};
|
| 123 |
|
|
@@ -296,14 +295,3 @@ fn path_to_cstring(p: &Path) -> Result<CString, String> {
|
|
| 296 |
.map_err(|e| format!("path contains nul byte: {e}"))
|
| 297 |
}
|
| 298 |
|
| 299 |
-
/// Try a few well-known default Steam install locations for WA.exe.
|
| 300 |
-
/// Users with non-standard library locations should set OPENWA_WA_PATH instead.
|
| 301 |
-
fn find_wa_hardcoded() -> Option<PathBuf> {
|
| 302 |
-
const CANDIDATES: &[&str] = &[
|
| 303 |
-
r"I:\games\SteamLibrary\steamapps\common\Worms Armageddon\WA.exe",
|
| 304 |
-
r"C:\Program Files (x86)\Steam\steamapps\common\Worms Armageddon\WA.exe",
|
| 305 |
-
r"C:\Program Files\Steam\steamapps\common\Worms Armageddon\WA.exe",
|
| 306 |
-
];
|
| 307 |
-
|
| 308 |
-
CANDIDATES.iter().map(PathBuf::from).find(|p| p.exists())
|
| 309 |
-
}
|
|
|
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
+
// Locate WA.exe: explicit arg > env var / registry (via openwa-config).
|
| 115 |
let wa_exe = if let Some(p) = wa_path_arg {
|
| 116 |
PathBuf::from(p)
|
|
|
|
|
|
|
| 117 |
} else {
|
| 118 |
+
openwa_config::find_wa_dir()
|
| 119 |
+
.map(|d| d.join("WA.exe"))
|
| 120 |
.ok_or("WA.exe not found. Set OPENWA_WA_PATH or pass the path as an argument.")?
|
| 121 |
};
|
| 122 |
|
|
|
|
| 295 |
.map_err(|e| format!("path contains nul byte: {e}"))
|
| 296 |
}
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
crates/openwa-test-runner/Cargo.toml
CHANGED
|
@@ -10,3 +10,4 @@ path = "src/main.rs"
|
|
| 10 |
|
| 11 |
[dependencies]
|
| 12 |
chrono = "0.4"
|
|
|
|
|
|
| 10 |
|
| 11 |
[dependencies]
|
| 12 |
chrono = "0.4"
|
| 13 |
+
openwa-config = { path = "../openwa-config" }
|
crates/openwa-test-runner/src/main.rs
CHANGED
|
@@ -24,11 +24,6 @@ const TIMEOUT_SECS: u64 = 120;
|
|
| 24 |
const REPLAYS_DIR: &str = "testdata/replays";
|
| 25 |
const RUNS_DIR: &str = "testdata/runs";
|
| 26 |
|
| 27 |
-
/// Known WA.exe locations to try if not specified.
|
| 28 |
-
const WA_CANDIDATES: &[&str] = &[
|
| 29 |
-
"I:/games/SteamLibrary/steamapps/common/Worms Armageddon/WA.exe",
|
| 30 |
-
"C:/Program Files (x86)/Steam/steamapps/common/Worms Armageddon/WA.exe",
|
| 31 |
-
];
|
| 32 |
|
| 33 |
// βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
|
|
@@ -167,22 +162,7 @@ fn find_wa_exe(override_path: Option<&Path>) -> Option<PathBuf> {
|
|
| 167 |
return Some(p.to_path_buf());
|
| 168 |
}
|
| 169 |
}
|
| 170 |
-
|
| 171 |
-
if let Ok(p) = env::var("OPENWA_WA_PATH") {
|
| 172 |
-
let path = PathBuf::from(p);
|
| 173 |
-
if path.exists() {
|
| 174 |
-
return Some(path);
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
for candidate in WA_CANDIDATES {
|
| 179 |
-
let path = PathBuf::from(candidate);
|
| 180 |
-
if path.exists() {
|
| 181 |
-
return Some(path);
|
| 182 |
-
}
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
None
|
| 186 |
}
|
| 187 |
|
| 188 |
fn find_launcher() -> Option<PathBuf> {
|
|
|
|
| 24 |
const REPLAYS_DIR: &str = "testdata/replays";
|
| 25 |
const RUNS_DIR: &str = "testdata/runs";
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
// βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
|
|
|
|
| 162 |
return Some(p.to_path_buf());
|
| 163 |
}
|
| 164 |
}
|
| 165 |
+
openwa_config::find_wa_dir().map(|d| d.join("WA.exe"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
fn find_launcher() -> Option<PathBuf> {
|