diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-08 03:30:04 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-08 03:30:04 -0500 |
| commit | 3de4056df4ea5d0afbcea388ad8d681daea1aac3 (patch) | |
| tree | fc5978451f9f51b84268f45bd2fa6f5d4a17baaf /libs/nak/src/installers | |
| parent | 5ba23a6d7a40052d9e1d4bd0cf018cfe7814d03a (diff) | |
Remove NaK/Rust dependency, port remaining functionality to native C++
Replace NaK Rust crate and nak_ffi with native C++ implementations:
- Game detection (Steam, Heroic, Bottles), VDF parser, icon extraction
- Prefix symlinks, SLR manager, Steam path detection
- Known games database
Add FUSE VFS optimizations: zero-copy reads via fuse_reply_data,
default_permissions mount option, FUSE_CAP_ASYNC_DIO, FUSE_CAP_EXPIRE_ONLY,
lookup cache parent index, cached mode bits, increased I/O buffer sizes.
Update build system, prefix setup, and various fixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/nak/src/installers')
| -rw-r--r-- | libs/nak/src/installers/mod.rs | 334 | ||||
| -rw-r--r-- | libs/nak/src/installers/prefix_setup.rs | 764 | ||||
| -rw-r--r-- | libs/nak/src/installers/symlinks.rs | 377 |
3 files changed, 0 insertions, 1475 deletions
diff --git a/libs/nak/src/installers/mod.rs b/libs/nak/src/installers/mod.rs deleted file mode 100644 index cf3eb85..0000000 --- a/libs/nak/src/installers/mod.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! Mod manager installation logic -//! -//! Stripped for Fluorine: no common.rs, mo2.rs, plugin.rs, compatdata_scanner.rs. - -pub mod symlinks; - -mod prefix_setup; - -pub use prefix_setup::{ - apply_dpi, apply_registry_for_game_path, auto_apply_game_registries, cleanup_prefix_drives, - install_all_dependencies, kill_wineserver, known_game_names, launch_dpi_test_app, DPI_PRESETS, -}; - -use std::error::Error; -use std::fs; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use crate::logging::log_install; -use crate::steam::SteamProton; - -// ============================================================================ -// Shared Types -// ============================================================================ - -/// Context for background installation tasks -#[derive(Clone)] -pub struct TaskContext { - pub status_callback: Arc<dyn Fn(String) + Send + Sync>, - pub log_callback: Arc<dyn Fn(String) + Send + Sync>, - pub progress_callback: Arc<dyn Fn(f32) + Send + Sync>, - pub cancel_flag: Arc<AtomicBool>, -} - -impl TaskContext { - pub fn new( - status: impl Fn(String) + Send + Sync + 'static, - log: impl Fn(String) + Send + Sync + 'static, - progress: impl Fn(f32) + Send + Sync + 'static, - cancel: Arc<AtomicBool>, - ) -> Self { - Self { - status_callback: Arc::new(status), - log_callback: Arc::new(log), - progress_callback: Arc::new(progress), - cancel_flag: cancel, - } - } - - pub fn set_status(&self, msg: String) { - (self.status_callback)(msg); - } - - pub fn log(&self, msg: String) { - (self.log_callback)(msg); - } - - pub fn set_progress(&self, p: f32) { - (self.progress_callback)(p); - } - - pub fn is_cancelled(&self) -> bool { - self.cancel_flag.load(std::sync::atomic::Ordering::Relaxed) - } - - /// Run a command that can be killed if the user cancels. - pub fn run_cancellable(&self, mut cmd: std::process::Command) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> { - let mut child = cmd.spawn()?; - - loop { - match child.try_wait()? { - Some(status) => return Ok(status), - None => { - if self.is_cancelled() { - let _ = child.kill(); - let _ = child.wait(); - return Err("Cancelled".into()); - } - std::thread::sleep(std::time::Duration::from_millis(250)); - } - } - } - } -} - -// ============================================================================ -// Shared Wine Registry Settings -// ============================================================================ - -/// Wine registry settings -pub const WINE_SETTINGS_REG: &str = r#"Windows Registry Editor Version 5.00 - -[HKEY_CURRENT_USER\Software\Wine\DllOverrides] -"dwrite.dll"="native,builtin" -"dwrite"="native,builtin" -"winmm.dll"="native,builtin" -"winmm"="native,builtin" -"version.dll"="native,builtin" -"version"="native,builtin" -"ArchiveXL.dll"="native,builtin" -"ArchiveXL"="native,builtin" -"Codeware.dll"="native,builtin" -"Codeware"="native,builtin" -"TweakXL.dll"="native,builtin" -"TweakXL"="native,builtin" -"input_loader.dll"="native,builtin" -"input_loader"="native,builtin" -"RED4ext.dll"="native,builtin" -"RED4ext"="native,builtin" -"mod_settings.dll"="native,builtin" -"mod_settings"="native,builtin" -"scc_lib.dll"="native,builtin" -"scc_lib"="native,builtin" -"dxgi.dll"="native,builtin" -"dxgi"="native,builtin" -"dbghelp.dll"="native,builtin" -"dbghelp"="native,builtin" -"d3d12.dll"="native,builtin" -"d3d12"="native,builtin" -"wininet.dll"="native,builtin" -"wininet"="native,builtin" -"winhttp.dll"="native,builtin" -"winhttp"="native,builtin" -"dinput.dll"="native,builtin" -"dinput8"="native,builtin" -"dinput8.dll"="native,builtin" - -[HKEY_CURRENT_USER\Software\Wine] -"ShowDotFiles"="Y" - -[HKEY_CURRENT_USER\Control Panel\Desktop] -"FontSmoothing"="2" -"FontSmoothingGamma"=dword:00000578 -"FontSmoothingOrientation"=dword:00000001 -"FontSmoothingType"=dword:00000002 - -[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers] -@="~ HIGHDPIAWARE" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Pandora Behaviour Engine+.exe\X11 Driver] -"Decorated"="N" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Vortex.exe\X11 Driver] -"Decorated"="N" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SSEEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SSEEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO4Edit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO4Edit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\TES4Edit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\TES4Edit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SF1Edit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FNVEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FNVEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xFOEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xFOEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xSFEEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xSFEEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xTESEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xTESEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO3Edit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO3Edit64.exe] -"Version"="winxp" - -; ============================================================================= -; Native file browser integration (opens folders in native file manager) -; ============================================================================= -[HKEY_CLASSES_ROOT\Folder\shell\explore\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -[HKEY_CLASSES_ROOT\Directory\shell\explore\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -[HKEY_CLASSES_ROOT\Folder\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -[HKEY_CLASSES_ROOT\Directory\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -; ============================================================================= -; Native text editor integration (opens text files in native editor) -; ============================================================================= -[HKEY_CLASSES_ROOT\txtfile\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe \"%1\"" - -[HKEY_CLASSES_ROOT\inifile\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe \"%1\"" - -[HKEY_CLASSES_ROOT\.txt] -@="txtfile" - -[HKEY_CLASSES_ROOT\.ini] -@="inifile" - -[HKEY_CLASSES_ROOT\.cfg] -@="txtfile" - -[HKEY_CLASSES_ROOT\.log] -@="txtfile" - -[HKEY_CLASSES_ROOT\.xml] -@="txtfile" - -[HKEY_CLASSES_ROOT\.json] -@="txtfile" - -[HKEY_CLASSES_ROOT\.yml] -@="txtfile" - -[HKEY_CLASSES_ROOT\.yaml] -@="txtfile" -"#; - -// ============================================================================ -// Shared Functions -// ============================================================================ - -/// Apply Wine registry settings to a prefix -pub fn apply_wine_registry_settings( - prefix_path: &std::path::Path, - proton: &SteamProton, - log_callback: &impl Fn(String), - _app_id: Option<u32>, -) -> Result<(), Box<dyn Error>> { - use std::io::Write; - use crate::config::AppConfig; - use crate::logging::{log_error, log_warning}; - use crate::runtime_wrap; - - let tmp_dir = AppConfig::get_tmp_path(); - fs::create_dir_all(&tmp_dir)?; - let reg_file = tmp_dir.join("wine_settings.reg"); - - let mut file = fs::File::create(®_file)?; - file.write_all(WINE_SETTINGS_REG.as_bytes())?; - - let wine_bin = proton.wine_binary().ok_or_else(|| { - let err_msg = format!( - "Wine binary not found for Proton '{}' (checked files/bin/wine and dist/bin/wine)", - proton.name - ); - log_callback(format!("Error: {}", err_msg)); - err_msg - })?; - - let wineserver_bin = proton.wineserver_binary().unwrap_or_else(|| { - wine_bin.with_file_name("wineserver") - }); - - let bin_dir = proton.bin_dir().ok_or_else(|| { - let err_msg = "Could not determine Proton bin directory"; - log_callback(format!("Error: {}", err_msg)); - err_msg - })?; - - let path_env = format!( - "{}:{}", - bin_dir.to_string_lossy(), - std::env::var("PATH").unwrap_or_default() - ); - - log_callback("Applying Wine registry settings...".to_string()); - log_install("Running wine regedit..."); - - let reg_envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_path.display().to_string()), - ("WINE", wine_bin.display().to_string()), - ("WINESERVER", wineserver_bin.display().to_string()), - ("PATH", path_env), - ("WINEDLLOVERRIDES", "mshtml=d".to_string()), - ("PROTON_USE_XALIA", "0".to_string()), - ]; - let regedit_status = runtime_wrap::build_command(&wine_bin, ®_envs) - .arg("regedit") - .arg(®_file) - .status(); - - match regedit_status { - Ok(status) => { - if status.success() { - log_callback("Registry settings applied successfully".to_string()); - log_install("Wine registry settings applied successfully"); - } else { - let msg = format!("regedit exited with code {:?}", status.code()); - log_callback(format!("Warning: {}", msg)); - log_warning(&msg); - } - } - Err(e) => { - let msg = format!("Failed to run regedit: {}", e); - log_callback(format!("Error: {}", msg)); - log_error(&msg); - return Err(msg.into()); - } - } - - let _ = fs::remove_file(®_file); - Ok(()) -} diff --git a/libs/nak/src/installers/prefix_setup.rs b/libs/nak/src/installers/prefix_setup.rs deleted file mode 100644 index 6c48b09..0000000 --- a/libs/nak/src/installers/prefix_setup.rs +++ /dev/null @@ -1,764 +0,0 @@ -//! 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 set them on the - // process (inherited by SLR/pressure-vessel into the container). - let 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()), - ]; - - log_install(&format!("Initializing prefix with proton wrapper: {:?}", proton_script)); - let (exe, args): (std::path::PathBuf, Vec<&str>) = - (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(®_file, ®_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(®_file) - .status(); - - let _ = fs::remove_file(®_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(®_file, ®_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, ®_envs) - .arg("regedit") - .arg(®_file) - .status(); - - let _ = fs::remove_file(®_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 - } - } -} diff --git a/libs/nak/src/installers/symlinks.rs b/libs/nak/src/installers/symlinks.rs deleted file mode 100644 index e8e477b..0000000 --- a/libs/nak/src/installers/symlinks.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Symlink management for NaK prefixes -//! -//! Creates symlinks FROM NaK prefix TO game prefixes. -//! Data stays in the game prefix, NaK just has links pointing to it. -//! -//! This inverted approach means: -//! - Game saves remain in their original location (Steam cloud sync works) -//! - NaK prefix provides unified access to all game data -//! - No data duplication or sync issues -//! -//! NaK Tools folder contains convenience symlinks pointing INTO the prefix -//! for easy access to Documents, AppData, etc. - -// Allow unused items - some functions are public API for future use -#![allow(dead_code)] - -use std::fs; -use std::path::Path; - -use crate::game_finder::{detect_all_games, Game, GameScanResult}; -use crate::logging::{log_info, log_warning}; - -// ============================================================================ -// Public API -// ============================================================================ - -/// Create symlinks from NaK prefix to game prefixes for all detected games -/// -/// This creates symlinks in the NaK prefix pointing to the actual save/config -/// folders in each game's own prefix. The symlink direction is: -/// -/// NaK/Documents/My Games/Skyrim -> game_prefix/Documents/My Games/Skyrim -/// NaK/AppData/Local/Skyrim -> game_prefix/AppData/Local/Skyrim -/// -/// Data stays in the game prefix (preserving Steam Cloud sync), while NaK -/// provides unified access through symlinks. -pub fn create_game_symlinks(nak_prefix: &Path, games: &[Game]) { - let users_dir = nak_prefix.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let user_dir = users_dir.join(&username); - - let documents = user_dir.join("Documents"); - let my_games = documents.join("My Games"); - let appdata_local = user_dir.join("AppData/Local"); - let appdata_roaming = user_dir.join("AppData/Roaming"); - - // Ensure base directories exist (real folders in NaK prefix) - let _ = fs::create_dir_all(&my_games); - let _ = fs::create_dir_all(&appdata_local); - let _ = fs::create_dir_all(&appdata_roaming); - - let mut linked_count = 0; - - for game in games { - // Skip games without prefixes - let Some(game_prefix) = &game.prefix_path else { - log_info(&format!( - "Symlink skip '{}' (app_id {}): no prefix_path", - game.name, game.app_id - )); - continue; - }; - - // Discover the game prefix's user directory - let game_users_dir = game_prefix.join("drive_c/users"); - let game_username = find_prefix_username(&game_users_dir); - let game_user_dir = game_users_dir.join(&game_username); - - let my_games_dir = game_user_dir.join("Documents/My Games"); - log_info(&format!( - "Symlink checking '{}' (app_id {}): prefix={}, user='{}', my_games_exists={}", - game.name, game.app_id, game_prefix.display(), game_username, my_games_dir.is_dir() - )); - - // Scan and symlink ALL folders in the game prefix's Documents/My Games/ - linked_count += scan_and_link_all( - &my_games, - &game_user_dir.join("Documents/My Games"), - "Documents/My Games", - &game.name, - game_prefix, - ); - - // Also link the Documents folder itself for non-My Games entries - // (some games put saves directly in Documents/<GameName>) - linked_count += scan_and_link_all( - &documents, - &game_user_dir.join("Documents"), - "Documents", - &game.name, - game_prefix, - ); - - // Scan and symlink ALL folders in AppData/Local/ - linked_count += scan_and_link_all( - &appdata_local, - &game_user_dir.join("AppData/Local"), - "AppData/Local", - &game.name, - game_prefix, - ); - - // Scan and symlink ALL folders in AppData/Roaming/ - linked_count += scan_and_link_all( - &appdata_roaming, - &game_user_dir.join("AppData/Roaming"), - "AppData/Roaming", - &game.name, - game_prefix, - ); - } - - if linked_count > 0 { - log_info(&format!( - "Created {} symlinks to game prefixes", - linked_count - )); - } - - // Create "My Documents" symlink for compatibility - let my_documents = user_dir.join("My Documents"); - if !my_documents.exists() && fs::symlink_metadata(&my_documents).is_err() { - if let Err(e) = std::os::unix::fs::symlink("Documents", &my_documents) { - log_warning(&format!("Failed to create My Documents symlink: {}", e)); - } - } -} - -/// Create NaK Tools convenience symlinks pointing INTO the prefix -/// -/// Creates symlinks in NaK Tools folder for easy access: -/// - NaK Tools/Prefix Documents -> prefix/drive_c/users/<user>/Documents -/// - NaK Tools/Prefix AppData Local -> prefix/drive_c/users/<user>/AppData/Local -/// - NaK Tools/Prefix AppData Roaming -> prefix/drive_c/users/<user>/AppData/Roaming -pub fn create_nak_tools_symlinks(tools_dir: &Path, prefix_path: &Path) { - let users_dir = prefix_path.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let user_dir = users_dir.join(&username); - - // Symlink: NaK Tools/Prefix Documents -> prefix Documents - let documents_link = tools_dir.join("Prefix Documents"); - let documents_target = user_dir.join("Documents"); - create_or_update_symlink(&documents_link, &documents_target, "Prefix Documents"); - - // Symlink: NaK Tools/Prefix AppData Local -> prefix AppData/Local - let appdata_local_link = tools_dir.join("Prefix AppData Local"); - let appdata_local_target = user_dir.join("AppData/Local"); - create_or_update_symlink(&appdata_local_link, &appdata_local_target, "Prefix AppData Local"); - - // Symlink: NaK Tools/Prefix AppData Roaming -> prefix AppData/Roaming - let appdata_roaming_link = tools_dir.join("Prefix AppData Roaming"); - let appdata_roaming_target = user_dir.join("AppData/Roaming"); - create_or_update_symlink(&appdata_roaming_link, &appdata_roaming_target, "Prefix AppData Roaming"); - - log_info("Created NaK Tools convenience symlinks to prefix folders"); -} - -/// Create or update a symlink -fn create_or_update_symlink(link_path: &Path, target: &Path, name: &str) { - // Remove existing symlink or file - if link_path.exists() || fs::symlink_metadata(link_path).is_ok() { - let _ = fs::remove_file(link_path); - let _ = fs::remove_dir_all(link_path); - } - - // Create symlink - if let Err(e) = std::os::unix::fs::symlink(target, link_path) { - log_warning(&format!("Failed to create {} symlink: {}", name, e)); - } -} - -/// Create symlinks for all detected games -/// -/// Convenience function that detects games and creates symlinks in one call. -pub fn create_game_symlinks_auto(nak_prefix: &Path) -> GameScanResult { - let result = detect_all_games(); - create_game_symlinks(nak_prefix, &result.games); - result -} - -/// Ensure only the Temp directory exists in AppData/Local -/// -/// MO2 and other tools require AppData/Local/Temp to exist. -/// We create only this essential directory, leaving other game-specific -/// folders to be symlinked from game prefixes. -pub fn ensure_temp_directory(prefix_path: &Path) { - let users_dir = prefix_path.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let user_dir = users_dir.join(&username); - - let temp_dir = user_dir.join("AppData/Local/Temp"); - if let Err(e) = fs::create_dir_all(&temp_dir) { - log_warning(&format!("Failed to create Temp directory: {}", e)); - } else { - log_info("Ensured AppData/Local/Temp directory exists"); - } -} - -// ============================================================================ -// Internal Functions -// ============================================================================ - -/// Directories to skip when scanning prefix folders for symlinking. -/// These are Wine/Proton internal or system dirs, not game data. -const SKIP_DIRS: &[&str] = &[ - "Temp", "Microsoft", "wine", "Public", "root", - "Application Data", "Cookies", "Local Settings", - "NetHood", "PrintHood", "Recent", "SendTo", - "Start Menu", "Templates", "My Documents", "My Music", - "My Pictures", "My Videos", "Desktop", "Downloads", - "Favorites", "Links", "Searches", - "Contacts", "3D Objects", -]; - -/// Scan all subdirectories in a game prefix folder and create symlinks -/// for each one in the corresponding NaK prefix folder. -/// -/// Returns the number of symlinks created. -fn scan_and_link_all( - nak_base: &Path, - game_base: &Path, - label: &str, - game_name: &str, - _game_prefix: &Path, -) -> usize { - if !game_base.is_dir() { - log_info(&format!( - " scan_and_link_all: {} not a dir for '{}', skipping", - game_base.display(), game_name - )); - return 0; - } - - let Ok(entries) = fs::read_dir(game_base) else { - log_info(&format!( - " scan_and_link_all: cannot read {} for '{}'", - game_base.display(), game_name - )); - return 0; - }; - - let mut count = 0; - for entry in entries.flatten() { - // Only symlink directories (game folders), not loose files - if !entry.path().is_dir() { - continue; - } - - let folder_name = entry.file_name().to_string_lossy().to_string(); - - // Skip Wine/system internal directories - if SKIP_DIRS.iter().any(|&s| s.eq_ignore_ascii_case(&folder_name)) { - continue; - } - - // Skip if it's "My Games" and we're scanning Documents (handled separately) - if label == "Documents" && folder_name == "My Games" { - continue; - } - - let nak_path = nak_base.join(&folder_name); - let source_path = entry.path(); - - if create_symlink_if_needed(&nak_path, &source_path, game_name, label, &folder_name) { - count += 1; - } - } - - count -} - -/// Create a symlink if the target doesn't already exist or is already correct. -/// -/// Returns true if a symlink was created (or already existed correctly). -fn create_symlink_if_needed( - nak_path: &Path, - source_path: &Path, - game_name: &str, - label: &str, - folder_name: &str, -) -> bool { - // Check if target already exists - if nak_path.exists() || fs::symlink_metadata(nak_path).is_ok() { - // Check if it's already a symlink to the correct location - if let Ok(target) = fs::read_link(nak_path) { - if target == source_path { - return true; // Already correctly linked - } - } - // Something else exists here, don't overwrite - return false; - } - - // Ensure parent directory exists - if let Some(parent) = nak_path.parent() { - let _ = fs::create_dir_all(parent); - } - - // Create the symlink - match std::os::unix::fs::symlink(source_path, nak_path) { - Ok(()) => { - log_info(&format!( - "Linked {}/{} -> {} ({})", - label, - folder_name, - source_path.display(), - game_name, - )); - true - } - Err(e) => { - log_warning(&format!( - "Failed to create symlink for {} ({}/{}): {}", - game_name, label, folder_name, e - )); - false - } - } -} - -/// Find the username from a Wine prefix users directory -fn find_prefix_username(users_dir: &Path) -> String { - if let Ok(entries) = fs::read_dir(users_dir) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - if name != "Public" && name != "root" { - return name; - } - } - } - "steamuser".to_string() -} - -// ============================================================================ -// Oblivion Lowercase INI Symlinks -// ============================================================================ - -/// Create lowercase INI symlinks for Oblivion (some tools expect lowercase) -pub fn create_oblivion_ini_symlinks(prefix_path: &Path) { - let users_dir = prefix_path.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let oblivion_dir = users_dir - .join(&username) - .join("Documents/My Games/Oblivion"); - - if !oblivion_dir.exists() { - return; - } - - create_lowercase_ini_symlink(&oblivion_dir, "Oblivion.ini", "oblivion.ini"); - create_lowercase_ini_symlink(&oblivion_dir, "OblivionPrefs.ini", "oblivionprefs.ini"); -} - -/// Create a lowercase symlink for an INI file -fn create_lowercase_ini_symlink(dir: &Path, original: &str, lowercase: &str) { - let original_path = dir.join(original); - let lowercase_path = dir.join(lowercase); - - // Only create if original exists and lowercase doesn't - if original_path.exists() - && !lowercase_path.exists() - && fs::symlink_metadata(&lowercase_path).is_err() - { - // Create relative symlink (just the filename) - if let Err(e) = std::os::unix::fs::symlink(original, &lowercase_path) { - log_warning(&format!( - "Failed to create lowercase symlink {} -> {}: {}", - lowercase, original, e - )); - } else { - log_info(&format!( - "Created lowercase INI symlink: {} -> {}", - lowercase, original - )); - } - } -} |
