aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/installers/prefix_setup.rs
diff options
context:
space:
mode:
Diffstat (limited to 'libs/nak/src/installers/prefix_setup.rs')
-rw-r--r--libs/nak/src/installers/prefix_setup.rs778
1 files changed, 778 insertions, 0 deletions
diff --git a/libs/nak/src/installers/prefix_setup.rs b/libs/nak/src/installers/prefix_setup.rs
new file mode 100644
index 0000000..4f27606
--- /dev/null
+++ b/libs/nak/src/installers/prefix_setup.rs
@@ -0,0 +1,778 @@
+//! Unified prefix setup for MO2
+//!
+//! This module handles all the dependency installation logic.
+//!
+//! Key approach (ORDER MATTERS):
+//! 1. Install dependencies via winetricks (handles wineboot internally)
+//! 2. Install custom dotnet runtimes (dotnet9sdk, dotnetdesktop10)
+//! 3. Auto-detect installed games and apply registry entries
+//! 4. Apply Wine registry settings (LAST - after prefix is fully set up)
+
+use std::error::Error;
+use std::fs;
+use std::path::Path;
+use std::process::Child;
+
+use super::{apply_wine_registry_settings, TaskContext};
+use crate::config::AppConfig;
+use crate::deps::{install_standard_deps_cancellable, STANDARD_VERBS};
+use crate::game_finder::{detect_all_games, known_games, Game, Launcher};
+use crate::logging::{log_install, log_warning};
+use crate::runtime_wrap;
+use crate::steam::{detect_steam_path_checked, SteamProton};
+
+// =============================================================================
+// Constants
+// =============================================================================
+
+/// .NET 9 SDK download URL
+const DOTNET9_SDK_URL: &str = "https://builds.dotnet.microsoft.com/dotnet/Sdk/9.0.310/dotnet-sdk-9.0.310-win-x64.exe";
+
+/// .NET Desktop Runtime 10 download URL
+const DOTNET_DESKTOP10_URL: &str = "https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.2/windowsdesktop-runtime-10.0.2-win-x64.exe";
+
+/// Drive letters to keep in the prefix (c: is Windows root, z: maps to Linux /)
+const ALLOWED_DRIVE_LETTERS: &[&str] = &["c:", "z:"];
+
+/// Install all dependencies to a prefix.
+///
+/// Order: proton init → winetricks → custom dotnet → game detection → registry → win11 → dotnet fixes
+///
+/// # Arguments
+/// * `app_id` - Steam AppID (used for registry operations)
+pub fn install_all_dependencies(
+ prefix_root: &Path,
+ install_proton: &SteamProton,
+ ctx: &TaskContext,
+ start_progress: f32,
+ end_progress: f32,
+ app_id: u32,
+) -> Result<(), Box<dyn Error>> {
+ fs::create_dir_all(AppConfig::get_tmp_path())?;
+
+ // Progress distribution
+ let init_end = start_progress + (end_progress - start_progress) * 0.10;
+ let winetricks_end = start_progress + (end_progress - start_progress) * 0.50;
+ let dotnet_end = start_progress + (end_progress - start_progress) * 0.65;
+ let games_end = start_progress + (end_progress - start_progress) * 0.75;
+
+ // =========================================================================
+ // 0. Initialize prefix with Proton wrapper (creates proper prefix structure)
+ // =========================================================================
+ ctx.set_status("Setting up Windows compatibility layer...".to_string());
+ ctx.log("Initializing Wine prefix with Proton...".to_string());
+ log_install("Running proton wineboot to initialize prefix");
+
+ if let Err(e) = initialize_prefix_with_proton(prefix_root, install_proton, app_id, ctx) {
+ ctx.log(format!("Warning: Proton prefix init failed: {}", e));
+ log_warning(&format!("Proton prefix init failed: {}", e));
+ // Continue anyway - winetricks might still work
+ }
+
+ ctx.set_progress(init_end);
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ // =========================================================================
+ // 0.5. Clean up unwanted drive letters (keep only C: and Z:)
+ // =========================================================================
+ ctx.set_status("Optimizing prefix configuration...".to_string());
+ ctx.log("Removing unwanted drive letters (keeping C: and Z:)...".to_string());
+ log_install("Cleaning up Wine drive letters");
+
+ if let Err(e) = cleanup_wine_drives(prefix_root, install_proton) {
+ ctx.log(format!("Warning: Drive cleanup had issues: {}", e));
+ log_warning(&format!("Drive cleanup failed: {}", e));
+ }
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ // =========================================================================
+ // 1. Standard Dependencies via Winetricks
+ // =========================================================================
+ ctx.set_status("Installing required Windows components (this may take several minutes)...".to_string());
+ ctx.log(format!(
+ "Installing {} dependencies via winetricks: {}",
+ STANDARD_VERBS.len(),
+ STANDARD_VERBS.join(", ")
+ ));
+ log_install(&format!("Running winetricks with {} verbs", STANDARD_VERBS.len()));
+
+ let winetricks_log_cb = {
+ let ctx = ctx.clone();
+ move |msg: String| {
+ ctx.log(msg.clone());
+ ctx.set_status(msg);
+ }
+ };
+
+ if let Err(e) = install_standard_deps_cancellable(prefix_root, install_proton, winetricks_log_cb, &ctx.cancel_flag) {
+ let msg = format!("Winetricks installation had issues: {}", e);
+ ctx.log(format!("Warning: {}", msg));
+ log_warning(&msg);
+ }
+
+ ctx.set_progress(winetricks_end);
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ // =========================================================================
+ // 2. Custom .NET Runtimes (not in winetricks yet)
+ // =========================================================================
+ ctx.set_status("Installing .NET runtime (1 of 2)...".to_string());
+ ctx.log("Installing .NET 9 SDK...".to_string());
+
+ if let Err(e) = install_dotnet_runtime(prefix_root, install_proton, DOTNET9_SDK_URL, "dotnet-sdk-9", ctx) {
+ ctx.log(format!("Warning: .NET 9 SDK install failed: {}", e));
+ log_warning(&format!(".NET 9 SDK install failed: {}", e));
+ }
+
+ ctx.set_status("Installing .NET runtime (2 of 2)...".to_string());
+ ctx.log("Installing .NET Desktop Runtime 10...".to_string());
+
+ if let Err(e) = install_dotnet_runtime(prefix_root, install_proton, DOTNET_DESKTOP10_URL, "dotnet-desktop-10", ctx) {
+ ctx.log(format!("Warning: .NET Desktop 10 install failed: {}", e));
+ log_warning(&format!(".NET Desktop 10 install failed: {}", e));
+ }
+
+ ctx.set_progress(dotnet_end);
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ // =========================================================================
+ // 3. Auto-detect and register installed games
+ // =========================================================================
+ ctx.set_status("Detecting your installed games...".to_string());
+ ctx.log("Auto-detecting installed Steam games...".to_string());
+ log_install("Auto-detecting installed games for registry");
+
+ let game_log_cb = {
+ let ctx = ctx.clone();
+ move |msg: String| ctx.log(msg)
+ };
+ auto_apply_game_registries(prefix_root, install_proton, &game_log_cb, Some(app_id));
+
+ ctx.set_progress(games_end);
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ // =========================================================================
+ // 4. Registry Settings (after prefix is fully initialized)
+ // =========================================================================
+ ctx.set_status("Configuring Windows registry...".to_string());
+ ctx.log("Applying Wine Registry Settings...".to_string());
+ log_install("Applying Wine registry settings");
+
+ let log_cb = {
+ let ctx = ctx.clone();
+ move |msg: String| ctx.log(msg)
+ };
+ apply_wine_registry_settings(prefix_root, install_proton, &log_cb, Some(app_id))?;
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ // =========================================================================
+ // 5. Set Windows 11 Mode
+ // =========================================================================
+ ctx.set_status("Finalizing compatibility settings...".to_string());
+ ctx.log("Setting Windows 11 mode...".to_string());
+ log_install("Setting Windows 11 mode via winetricks");
+
+ if let Err(e) = set_windows_11_mode(prefix_root, install_proton, ctx) {
+ ctx.log(format!("Warning: Failed to set Windows 11 mode: {}", e));
+ log_warning(&format!("Failed to set Windows 11 mode: {}", e));
+ }
+
+ if ctx.is_cancelled() {
+ return Err("Cancelled".into());
+ }
+
+ ctx.set_progress(end_progress);
+ ctx.set_status("Dependencies installed".to_string());
+ Ok(())
+}
+
+/// Install a .NET runtime via direct exe download and wine execution
+fn install_dotnet_runtime(
+ prefix_root: &Path,
+ proton: &SteamProton,
+ url: &str,
+ name: &str,
+ ctx: &TaskContext,
+) -> Result<(), Box<dyn Error>> {
+ let cache_dir = AppConfig::get_default_cache_dir();
+ fs::create_dir_all(&cache_dir)?;
+
+ let filename = url.split('/').next_back().unwrap_or("dotnet-installer.exe");
+ let installer_path = cache_dir.join(filename);
+
+ // Download if not cached
+ if !installer_path.exists() {
+ log_install(&format!("Downloading {}...", name));
+ let response = ureq::get(url)
+ .set("User-Agent", "NaK-Rust")
+ .call()
+ .map_err(|e| format!("Failed to download {}: {}", name, e))?;
+
+ let mut file = fs::File::create(&installer_path)?;
+ std::io::copy(&mut response.into_reader(), &mut file)?;
+ }
+
+ // Run installer with wine
+ let Some(wine_bin) = proton.wine_binary() else {
+ return Err("Wine binary not found".into());
+ };
+
+ log_install(&format!("Running {} installer...", name));
+
+ let envs: Vec<(&str, String)> = vec![
+ ("WINEPREFIX", prefix_root.display().to_string()),
+ ("WINEDLLOVERRIDES", "mshtml=d".to_string()),
+ ];
+ let mut cmd = runtime_wrap::build_command(&wine_bin, &envs);
+ cmd.arg(&installer_path)
+ .arg("/install")
+ .arg("/quiet")
+ .arg("/norestart");
+
+ let status = ctx.run_cancellable(cmd)?;
+
+ if !status.success() {
+ return Err(format!("{} installer exited with code {:?}", name, status.code()).into());
+ }
+
+ log_install(&format!("{} installed successfully", name));
+ Ok(())
+}
+
+/// Initialize prefix with Proton wrapper
+///
+/// Runs `proton run wineboot -u` to properly initialize the prefix with all
+/// the Steam/Proton environment variables. This creates a proper prefix
+/// structure that Steam recognizes.
+fn initialize_prefix_with_proton(
+ prefix_root: &Path,
+ proton: &SteamProton,
+ app_id: u32,
+ ctx: &TaskContext,
+) -> Result<(), Box<dyn Error>> {
+ // Find the proton wrapper script (not the wine binary)
+ let proton_script = proton.path.join("proton");
+ if !proton_script.exists() {
+ return Err(format!("Proton wrapper script not found at {:?}", proton_script).into());
+ }
+
+ // Get Steam root path
+ let steam_root = detect_steam_path_checked()
+ .ok_or("Could not find Steam installation")?;
+
+ // The compatdata path is the PARENT of the pfx directory
+ let compat_data_path = prefix_root.parent()
+ .ok_or("Could not determine compatdata path")?;
+
+ log_install(&format!("STEAM_COMPAT_DATA_PATH={:?}", compat_data_path));
+
+ // Collect all env vars upfront so build_command can forward them
+ // via --env= flags in Flatpak mode.
+ let mut envs: Vec<(&str, String)> = vec![
+ ("STEAM_COMPAT_CLIENT_INSTALL_PATH", steam_root.clone()),
+ ("STEAM_COMPAT_DATA_PATH", compat_data_path.display().to_string()),
+ ("SteamAppId", app_id.to_string()),
+ ("SteamGameId", app_id.to_string()),
+ ("DISPLAY", String::new()), // Suppress GUI
+ ("WAYLAND_DISPLAY", String::new()), // Suppress GUI
+ ("WINEDEBUG", "-all".to_string()),
+ ("WINEDLLOVERRIDES", "msdia80.dll=n;conhost.exe=d;cmd.exe=d".to_string()),
+ ];
+
+ let (exe, args): (std::path::PathBuf, Vec<&str>) = if runtime_wrap::use_umu_for_prefix() {
+ if let Some(umu_run) = runtime_wrap::resolve_umu_run() {
+ log_install(&format!("Initializing prefix with umu-run: {:?}", umu_run));
+ envs.push(("PROTONPATH", proton.path.display().to_string()));
+ envs.push(("WINEPREFIX", prefix_root.display().to_string()));
+ envs.push(("GAMEID", app_id.to_string()));
+ (umu_run, vec!["wineboot", "-u"])
+ } else {
+ log_warning(
+ "UMU prefix mode enabled but no umu-run was found; falling back to proton wrapper",
+ );
+ (proton_script.clone(), vec!["run", "wineboot", "-u"])
+ }
+ } else {
+ log_install(&format!("Initializing prefix with proton wrapper: {:?}", proton_script));
+ (proton_script.clone(), vec!["run", "wineboot", "-u"])
+ };
+
+ let mut cmd = runtime_wrap::build_command(&exe, &envs);
+ cmd.args(&args);
+
+ let status = ctx.run_cancellable(cmd)?;
+
+ if !status.success() {
+ return Err(format!("proton wineboot failed with exit code: {:?}", status.code()).into());
+ }
+
+ // Give it a moment for files to land
+ std::thread::sleep(std::time::Duration::from_secs(2));
+
+ // Verify prefix was created
+ if prefix_root.exists() {
+ log_install("Proton prefix initialized successfully");
+ Ok(())
+ } else {
+ Err("Prefix directory not created after wineboot".into())
+ }
+}
+
+/// Clean up unwanted Wine drive letters from prefix
+///
+/// Removes both symbolic links in dosdevices/ and registry entries for
+/// drive letters other than C: and Z:. This prevents Wine from mounting
+/// other users' drives as E:, F:, G:, etc.
+fn cleanup_wine_drives(
+ prefix_root: &Path,
+ proton: &SteamProton,
+) -> Result<(), Box<dyn Error>> {
+ let dosdevices = prefix_root.join("dosdevices");
+
+ if !dosdevices.exists() {
+ log_install("dosdevices directory not found, skipping drive cleanup");
+ return Ok(());
+ }
+
+ // =========================================================================
+ // 1. Remove unwanted symlinks from dosdevices/
+ // =========================================================================
+ let mut removed_drives = Vec::new();
+
+ if let Ok(entries) = fs::read_dir(&dosdevices) {
+ for entry in entries.flatten() {
+ let name = entry.file_name().to_string_lossy().to_lowercase();
+
+ // Skip allowed drives and non-drive entries (like com1, lpt1, etc.)
+ if ALLOWED_DRIVE_LETTERS.contains(&name.as_str()) {
+ continue;
+ }
+
+ // Only process drive letters (single letter followed by colon)
+ if name.len() == 2 && name.ends_with(':') && name.chars().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
+ let path = entry.path();
+ if let Err(e) = fs::remove_file(&path) {
+ log_warning(&format!("Failed to remove drive symlink {}: {}", name, e));
+ } else {
+ removed_drives.push(name.to_uppercase());
+ }
+ }
+ }
+ }
+
+ if !removed_drives.is_empty() {
+ log_install(&format!("Removed drive symlinks: {}", removed_drives.join(", ")));
+ }
+
+ // =========================================================================
+ // 2. Clean up registry entries for removed drives
+ // =========================================================================
+ let Some(wine_bin) = proton.wine_binary() else {
+ log_warning("Wine binary not found, skipping registry cleanup");
+ return Ok(());
+ };
+
+ // Create a .reg file to remove drive type entries
+ let tmp_dir = AppConfig::get_tmp_path();
+ fs::create_dir_all(&tmp_dir)?;
+
+ let mut reg_content = String::from("Windows Registry Editor Version 5.00\n\n");
+
+ // Remove entries from HKLM\Software\Wine\Drives
+ for drive in &removed_drives {
+ // Remove both uppercase and lowercase variants
+ reg_content.push_str(&format!(
+ "[HKEY_LOCAL_MACHINE\\Software\\Wine\\Drives]\n\"{drive}\"=-\n\n"
+ ));
+ }
+
+ if !removed_drives.is_empty() {
+ let reg_file = tmp_dir.join("drive_cleanup.reg");
+ fs::write(&reg_file, &reg_content)?;
+
+ let drive_envs: Vec<(&str, String)> = vec![
+ ("WINEPREFIX", prefix_root.display().to_string()),
+ ("WINEDLLOVERRIDES", "mshtml=d".to_string()),
+ ("PROTON_USE_XALIA", "0".to_string()),
+ ];
+ let status = runtime_wrap::build_command(&wine_bin, &drive_envs)
+ .arg("regedit")
+ .arg(&reg_file)
+ .status();
+
+ let _ = fs::remove_file(&reg_file);
+
+ match status {
+ Ok(s) if s.success() => {
+ log_install("Registry drive entries cleaned up");
+ }
+ Ok(s) => {
+ log_warning(&format!("Registry cleanup may have failed (exit code: {:?})", s.code()));
+ }
+ Err(e) => {
+ log_warning(&format!("Failed to run registry cleanup: {}", e));
+ }
+ }
+ }
+
+ Ok(())
+}
+
+/// Public wrapper to clean up Wine drives on an existing prefix
+///
+/// This can be called from the UI to fix drive letter issues on existing prefixes.
+pub fn cleanup_prefix_drives(
+ prefix_root: &Path,
+ proton: &SteamProton,
+) -> Result<Vec<String>, Box<dyn Error>> {
+ let dosdevices = prefix_root.join("dosdevices");
+
+ if !dosdevices.exists() {
+ return Err("dosdevices directory not found - is this a valid Wine prefix?".into());
+ }
+
+ // Collect drives before cleanup
+ let mut removed = Vec::new();
+
+ if let Ok(entries) = fs::read_dir(&dosdevices) {
+ for entry in entries.flatten() {
+ let name = entry.file_name().to_string_lossy().to_lowercase();
+ if name.len() == 2 && name.ends_with(':') && name.chars().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false)
+ && !ALLOWED_DRIVE_LETTERS.contains(&name.as_str()) {
+ removed.push(name.to_uppercase());
+ }
+ }
+ }
+
+ // Run the actual cleanup
+ cleanup_wine_drives(prefix_root, proton)?;
+
+ Ok(removed)
+}
+
+/// Set Windows 11 mode for the prefix using winetricks
+///
+/// This should be called AFTER all components are installed.
+/// Sets the Windows version to Windows 11 which is required for MO2 to work properly.
+fn set_windows_11_mode(
+ prefix_root: &Path,
+ proton: &SteamProton,
+ ctx: &TaskContext,
+) -> Result<(), Box<dyn Error>> {
+ use crate::deps::ensure_winetricks;
+
+ let winetricks_path = ensure_winetricks()?;
+
+ let Some(wine_bin) = proton.wine_binary() else {
+ return Err("Wine binary not found".into());
+ };
+
+ let Some(wineserver_bin) = proton.wineserver_binary() else {
+ return Err("Wineserver binary not found".into());
+ };
+
+ log_install("Running winetricks win11...");
+
+ let envs: Vec<(&str, String)> = vec![
+ ("WINE", wine_bin.display().to_string()),
+ ("WINESERVER", wineserver_bin.display().to_string()),
+ ("WINEPREFIX", prefix_root.display().to_string()),
+ ];
+ let mut cmd = runtime_wrap::build_command(&winetricks_path, &envs);
+ cmd.arg("-q").arg("win11");
+
+ let status = ctx.run_cancellable(cmd)?;
+
+ if !status.success() {
+ return Err(format!("winetricks win11 failed with exit code: {:?}", status.code()).into());
+ }
+
+ log_install("Windows 11 mode set successfully");
+ Ok(())
+}
+
+// =============================================================================
+// DPI Configuration
+// =============================================================================
+
+/// Common DPI presets with their percentage labels
+pub const DPI_PRESETS: &[(u32, &str)] = &[
+ (96, "100%"),
+ (120, "125%"),
+ (144, "150%"),
+ (192, "200%"),
+];
+
+/// Apply DPI setting to a Wine prefix via registry
+pub fn apply_dpi(
+ prefix_root: &Path,
+ proton: &SteamProton,
+ dpi_value: u32,
+) -> Result<(), Box<dyn Error>> {
+ log_install(&format!("Applying DPI {} to prefix", dpi_value));
+
+ let wine_bin = proton.wine_binary().ok_or_else(|| {
+ format!("Wine binary not found for Proton '{}'", proton.name)
+ })?;
+
+ let envs: Vec<(&str, String)> = vec![
+ ("WINEPREFIX", prefix_root.display().to_string()),
+ ("PROTON_USE_XALIA", "0".to_string()),
+ ];
+ let status = runtime_wrap::build_command(&wine_bin, &envs)
+ .arg("reg")
+ .arg("add")
+ .arg(r"HKCU\Control Panel\Desktop")
+ .arg("/v")
+ .arg("LogPixels")
+ .arg("/t")
+ .arg("REG_DWORD")
+ .arg("/d")
+ .arg(dpi_value.to_string())
+ .arg("/f")
+ .status()?;
+
+ if !status.success() {
+ return Err(format!("Failed to apply DPI setting: exit code {:?}", status.code()).into());
+ }
+
+ log_install(&format!("DPI {} applied successfully", dpi_value));
+ Ok(())
+}
+
+/// Launch a test application (winecfg, regedit, notepad, control) and return its PID
+pub fn launch_dpi_test_app(
+ prefix_root: &Path,
+ proton: &SteamProton,
+ app_name: &str,
+) -> Result<Child, Box<dyn Error>> {
+ let wine_bin = proton.wine_binary().ok_or_else(|| {
+ format!("Wine binary not found for Proton '{}'", proton.name)
+ })?;
+
+ log_install(&format!(
+ "Launching {} with wine={:?} prefix={:?}",
+ app_name, wine_bin, prefix_root
+ ));
+
+ if !prefix_root.exists() {
+ return Err(format!("Prefix not found: {:?}", prefix_root).into());
+ }
+
+ let envs: Vec<(&str, String)> = vec![
+ ("WINEPREFIX", prefix_root.display().to_string()),
+ ("PROTON_USE_XALIA", "0".to_string()),
+ ];
+ let child = runtime_wrap::build_command(&wine_bin, &envs)
+ .arg(app_name)
+ .spawn()?;
+
+ Ok(child)
+}
+
+/// Kill the wineserver for a prefix (terminates all Wine processes in that prefix)
+pub fn kill_wineserver(prefix_root: &Path, proton: &SteamProton) {
+ log_install("Killing wineserver for prefix");
+
+ let Some(wineserver_bin) = proton.wineserver_binary() else {
+ log_install("Wineserver binary not found, skipping kill");
+ return;
+ };
+
+ let envs: Vec<(&str, String)> = vec![
+ ("WINEPREFIX", prefix_root.display().to_string()),
+ ];
+ let _ = runtime_wrap::build_command(&wineserver_bin, &envs)
+ .arg("-k")
+ .status();
+}
+
+// ============================================================================
+// Game Registry Detection (uses game_finder module)
+// ============================================================================
+
+/// Auto-detect installed games and apply registry entries
+///
+/// This uses the game_finder module to detect installed games across all
+/// supported launchers (Steam, Heroic, Bottles) and automatically adds
+/// the registry entries so mod managers can detect them.
+pub fn auto_apply_game_registries(
+ prefix_path: &Path,
+ proton: &SteamProton,
+ log_callback: &impl Fn(String),
+ _app_id: Option<u32>,
+) {
+ let Some(wine_bin) = proton.wine_binary() else {
+ log_warning("Wine binary not found, skipping game registry auto-detection");
+ return;
+ };
+
+ // Use the new game_finder module to detect all games
+ let scan_result = detect_all_games();
+ let mut applied_count = 0;
+
+ for game in &scan_result.games {
+ // Only process games that have registry info
+ let (Some(reg_path), Some(reg_value)) = (&game.registry_path, &game.registry_value) else {
+ continue;
+ };
+
+ // Apply registry for this game
+ if apply_game_registry(
+ prefix_path,
+ &wine_bin,
+ game,
+ reg_path,
+ reg_value,
+ log_callback,
+ ) {
+ applied_count += 1;
+ }
+ }
+
+ if applied_count > 0 {
+ log_callback(format!("Auto-configured {} game(s) in registry", applied_count));
+ log_install(&format!("Auto-applied registry for {} detected game(s)", applied_count));
+ }
+}
+
+/// Apply a game's registry entry with a custom install path.
+///
+/// Looks up the game by name in KNOWN_GAMES, then writes the registry entry
+/// pointing to `install_path`. Use this when the game is in a custom/stock
+/// folder that auto-detection won't find.
+pub fn apply_registry_for_game_path(
+ prefix_path: &Path,
+ proton: &SteamProton,
+ game_name: &str,
+ install_path: &Path,
+ log_callback: &impl Fn(String),
+) -> Result<(), String> {
+ let Some(wine_bin) = proton.wine_binary() else {
+ return Err("Wine binary not found".to_string());
+ };
+
+ let known = known_games::find_by_name(game_name);
+ let (reg_path, reg_value) = if let Some(kg) = known {
+ (kg.registry_path, kg.registry_value)
+ } else {
+ return Err(format!("Unknown game: {game_name}"));
+ };
+
+ let fake_game = Game {
+ name: game_name.to_string(),
+ install_path: install_path.to_path_buf(),
+ app_id: known.map(|k| k.steam_app_id.to_string()).unwrap_or_default(),
+ prefix_path: None,
+ launcher: Launcher::Steam { is_flatpak: false, is_snap: false },
+ my_games_folder: known.and_then(|k| k.my_games_folder.map(String::from)),
+ appdata_local_folder: known.and_then(|k| k.appdata_local_folder.map(String::from)),
+ appdata_roaming_folder: known.and_then(|k| k.appdata_roaming_folder.map(String::from)),
+ registry_path: Some(reg_path.to_string()),
+ registry_value: Some(reg_value.to_string()),
+ };
+
+ if apply_game_registry(prefix_path, &wine_bin, &fake_game, reg_path, reg_value, log_callback) {
+ Ok(())
+ } else {
+ Err(format!("Failed to apply registry for {game_name}"))
+ }
+}
+
+/// Return the list of known game names for UI display.
+pub fn known_game_names() -> Vec<&'static str> {
+ known_games::KNOWN_GAMES.iter().map(|g| g.name).collect()
+}
+
+/// Apply registry entry for a single game
+fn apply_game_registry(
+ prefix_path: &Path,
+ wine_bin: &Path,
+ game: &Game,
+ reg_path: &str,
+ reg_value: &str,
+ log_callback: &impl Fn(String),
+) -> bool {
+ log_callback(format!("Found {}, applying registry...", game.name));
+
+ // Convert Linux path to Wine Z: drive path with escaped backslashes for .reg file
+ let linux_path = game.install_path.to_string_lossy();
+ let wine_path_reg = format!("Z:{}", linux_path.replace('/', "\\\\"));
+
+ // Create .reg file content
+ let reg_content = format!(
+ r#"Windows Registry Editor Version 5.00
+
+[HKEY_LOCAL_MACHINE\{}]
+"{}"="{}"
+
+[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\{}]
+"{}"="{}"
+"#,
+ reg_path,
+ reg_value,
+ wine_path_reg,
+ reg_path.strip_prefix("Software\\").unwrap_or(reg_path),
+ reg_value,
+ wine_path_reg,
+ );
+
+ // Write temp .reg file
+ let tmp_dir = AppConfig::get_tmp_path();
+ let reg_file = tmp_dir.join(format!("game_reg_{}.reg", game.app_id));
+
+ if let Err(e) = fs::write(&reg_file, &reg_content) {
+ log_warning(&format!("Failed to write registry file for {}: {}", game.name, e));
+ return false;
+ }
+
+ // Apply registry
+ let reg_envs: Vec<(&str, String)> = vec![
+ ("WINEPREFIX", prefix_path.display().to_string()),
+ ("WINEDLLOVERRIDES", "mshtml=d".to_string()),
+ ("PROTON_USE_XALIA", "0".to_string()),
+ ];
+ let status = runtime_wrap::build_command(wine_bin, &reg_envs)
+ .arg("regedit")
+ .arg(&reg_file)
+ .status();
+
+ let _ = fs::remove_file(&reg_file);
+
+ match status {
+ Ok(s) if s.success() => {
+ log_install(&format!("Applied registry for {} -> {:?}", game.name, game.install_path));
+ true
+ }
+ Ok(s) => {
+ log_warning(&format!(
+ "Registry for {} may have failed (exit code: {:?})",
+ game.name,
+ s.code()
+ ));
+ false
+ }
+ Err(e) => {
+ log_warning(&format!("Failed to apply registry for {}: {}", game.name, e));
+ false
+ }
+ }
+}