diff --git a/src/app.rs b/src/app.rs index 3804775..b523422 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2177,7 +2177,7 @@ fn group_thousands(digits: &str) -> String { let len = bytes.len(); let mut out = String::with_capacity(len + len / 3); for (i, b) in bytes.iter().enumerate() { - if i > 0 && (len - i) % 3 == 0 { + if i > 0 && (len - i).is_multiple_of(3) { out.push(','); } out.push(*b as char); diff --git a/src/game_launch.rs b/src/game_launch.rs index bb53eb7..a5a4a1f 100644 --- a/src/game_launch.rs +++ b/src/game_launch.rs @@ -58,6 +58,7 @@ pub fn launch( } prepare_prefix(profile, log)?; + ensure_dll_override(profile, log); ensure_license(profile, log)?; let mut cmd = Command::new(&profile.runner); @@ -95,6 +96,99 @@ pub fn launch( Ok(()) } +/// The registry key Wine reads DLL overrides from, and the one value the hook needs. +/// +/// Wine loads its own builtin `version.dll` unless an override says otherwise, so the +/// game-directory proxy is ignored by default. `WINEDLLOVERRIDES` fixes that only for +/// a process we spawn ourselves — it cannot help a player who presses Play in Steam, +/// which is why the old advice was to paste launch options by hand (see +/// `setup::STEAM_LAUNCH_OPTIONS`). Asking a player to edit launch options is exactly +/// the kind of step that makes this unusable for anyone who does not already know what +/// a DLL override is. +/// +/// Persisting the override in the prefix registry removes the manual step entirely: it +/// survives restarts and applies to every launch path, including Steam. This mirrors +/// what BepInEx documents for Proton (configure the proxy in winecfg rather than the +/// environment) and what Proton itself already does in this prefix for other titles. +const DLL_OVERRIDE_KEY: &str = r"HKCU\Software\Wine\DllOverrides"; +const HOOK_DLL_VALUE: &str = "version"; +const HOOK_DLL_OVERRIDE: &str = "native,builtin"; + +/// `reg add` argv that persists the hook's DLL override, native-first with a builtin +/// fallback. `/f` makes it idempotent, so this is safe to run on every launch and +/// repairs a prefix a player has reset or replaced. +fn dll_override_args() -> [&'static str; 10] { + [ + "reg", + "add", + DLL_OVERRIDE_KEY, + "/v", + HOOK_DLL_VALUE, + "/t", + "REG_SZ", + "/d", + HOOK_DLL_OVERRIDE, + "/f", + ] +} + +/// Persist the hook's DLL override into the prefix, so the game loads the proxy no +/// matter how it is started. +/// +/// Best-effort by design: a failure here is not fatal, because a launch we spawn also +/// carries `WINEDLLOVERRIDES`. It is reported in plain language rather than as a Wine +/// error, since the player cannot act on the latter. +fn ensure_dll_override(profile: &GameProfile, log: &Log) { + if profile.wine_prefix.trim().is_empty() { + return; + } + let mut cmd = Command::new(&profile.runner); + cmd.args(dll_override_args()) + .current_dir(&profile.game_dir) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + for (k, v) in &profile.env { + cmd.env(k, v); + } + cmd.env("WINEPREFIX", &profile.wine_prefix); + match cmd.status() { + Ok(status) if status.success() => { + say(log, "[launcher] game files ready (mod support enabled)"); + } + Ok(_) | Err(_) => say( + log, + "[launcher] could not pre-enable mod support in the game prefix; \ + launching anyway (this launch still enables it directly)", + ), + } +} + +#[cfg(test)] +mod override_tests { + use super::*; + + #[test] + fn dll_override_is_persisted_native_first_and_idempotently() { + let args = dll_override_args(); + assert_eq!(args[0], "reg"); + assert_eq!(args[1], "add"); + assert_eq!( + args[2], r"HKCU\Software\Wine\DllOverrides", + "Wine reads overrides from this key; a typo silently leaves the hook unloaded" + ); + assert_eq!(args[4], "version", "the hook ships as a version.dll proxy"); + assert_eq!( + args[8], "native,builtin", + "native first so the proxy wins, builtin as fallback so a missing proxy \ + cannot make the game unlaunchable" + ); + assert_eq!( + args[9], "/f", + "idempotent, so running it on every launch repairs a reset prefix" + ); + } +} + /// The `WINEDLLOVERRIDES` value the game must be started with. /// /// The hook ships as a `version.dll` proxy inside the game directory, and Proton diff --git a/src/setup.rs b/src/setup.rs index 0fba7cb..2b7ef9c 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -126,8 +126,14 @@ pub fn hook_dll_deployed(game_dir: &Path) -> bool { game_dir.join("version.dll").exists() } -/// The Steam launch options the user needs to paste in to enable the override. -/// Proton loads local DLLs named in WINEDLLOVERRIDES ahead of system ones. +/// Steam launch options that enable the hook's DLL override. +/// +/// Kept only as a fallback to show a user who runs the game outside this launcher on +/// a prefix we have never prepared. It is NOT the normal path any more: the launcher +/// persists the override in the prefix registry itself +/// (`game_launch::ensure_dll_override`), which applies to every launch including +/// Steam's own Play button. Telling a player to paste launch options is exactly the +/// kind of manual step this launcher exists to remove. pub const STEAM_LAUNCH_OPTIONS: &str = "WINEDLLOVERRIDES=\"version=n,b\" %command%"; // ── Game launch ───────────────────────────────────────────────────────────────