diff options
Diffstat (limited to 'libs/nak/src/steam')
| -rw-r--r-- | libs/nak/src/steam/mod.rs | 135 | ||||
| -rw-r--r-- | libs/nak/src/steam/paths.rs | 259 | ||||
| -rw-r--r-- | libs/nak/src/steam/proton.rs | 252 |
3 files changed, 0 insertions, 646 deletions
diff --git a/libs/nak/src/steam/mod.rs b/libs/nak/src/steam/mod.rs deleted file mode 100644 index c5964aa..0000000 --- a/libs/nak/src/steam/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Steam integration module -//! -//! Handles Proton detection, Steam path detection, and mount point discovery. -//! Shortcuts and config.vdf manipulation removed (handled by C++ side). - -mod paths; -mod proton; - -// Re-export path detection utilities -pub use paths::{ - detect_steam_path_checked, find_steam_path, find_userdata_path, - get_steam_accounts, -}; - -// Re-export Proton detection -pub use proton::{find_steam_protons, SteamProton}; - -use std::fs; - -/// Kill Steam process gracefully, then force if needed -pub fn kill_steam() -> Result<(), Box<dyn std::error::Error>> { - use std::process::Command; - - // Try steam -shutdown first (graceful) - let _ = Command::new("steam") - .arg("-shutdown") - .status(); - - std::thread::sleep(std::time::Duration::from_secs(2)); - - // Then force kill if still running - let _ = Command::new("pkill") - .arg("-9") - .arg("steam") - .status(); - - // Brief wait for Steam to fully exit - std::thread::sleep(std::time::Duration::from_secs(2)); - - Ok(()) -} - -/// Start Steam in background -pub fn start_steam() -> Result<(), Box<dyn std::error::Error>> { - use std::process::{Command, Stdio}; - - Command::new("setsid") - .arg("steam") - .arg("-silent") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - Ok(()) -} - -/// Restart Steam (kill then start) -pub fn restart_steam() -> Result<(), Box<dyn std::error::Error>> { - kill_steam()?; - start_steam()?; - Ok(()) -} - -// ============================================================================ -// STEAM_COMPAT_MOUNTS Detection -// ============================================================================ - -/// Directories that pressure-vessel already exposes by default -const ALREADY_EXPOSED: &[&str] = &[ - "bin", "etc", "home", "lib", "lib32", "lib64", - "overrides", "run", "sbin", "tmp", "usr", "var", -]; - -/// System directories that shouldn't be mounted -const SYSTEM_DIRS: &[&str] = &[ - "proc", "sys", "dev", "boot", "root", "lost+found", "snap", -]; - -/// Detect directories at root that need to be added to STEAM_COMPAT_MOUNTS -pub fn detect_extra_mounts() -> Vec<String> { - let mut mounts = Vec::new(); - - let Ok(entries) = fs::read_dir("/") else { - return mounts; - }; - - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - - if ALREADY_EXPOSED.contains(&name.as_str()) { - continue; - } - - if SYSTEM_DIRS.contains(&name.as_str()) { - continue; - } - - if name.starts_with('.') { - continue; - } - - if entry.path().is_dir() { - mounts.push(format!("/{}", name)); - } - } - - mounts.sort(); - mounts -} - -/// Generate launch options string with DXVK config file and STEAM_COMPAT_MOUNTS -pub fn generate_launch_options(dxvk_conf_path: Option<&std::path::Path>, is_electron_app: bool) -> String { - let mounts = detect_extra_mounts(); - - let dxvk_part = match dxvk_conf_path { - Some(path) => format!( - "DXVK_CONFIG_FILE=\"{}\"", - crate::config::normalize_path_for_steam(&path.to_string_lossy()) - ), - None => String::new(), - }; - - let electron_flags = if is_electron_app { - " --disable-gpu --no-sandbox" - } else { - "" - }; - - match (dxvk_part.is_empty(), mounts.is_empty()) { - (true, true) => format!("%command%{}", electron_flags), - (true, false) => format!("STEAM_COMPAT_MOUNTS={} %command%{}", mounts.join(":"), electron_flags), - (false, true) => format!("{} %command%{}", dxvk_part, electron_flags), - (false, false) => format!("{} STEAM_COMPAT_MOUNTS={} %command%{}", dxvk_part, mounts.join(":"), electron_flags), - } -} diff --git a/libs/nak/src/steam/paths.rs b/libs/nak/src/steam/paths.rs deleted file mode 100644 index 8ef5013..0000000 --- a/libs/nak/src/steam/paths.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Steam path detection utilities - -use std::fs; -use std::path::PathBuf; - -use crate::logging::{log_info, log_warning}; - -// ============================================================================ -// Core Path Detection -// ============================================================================ - -/// Find the Steam installation path. -#[must_use] -pub fn find_steam_path() -> Option<PathBuf> { - let home = std::env::var("HOME").ok()?; - - let steam_paths = [ - format!("{}/.steam/steam", home), - format!("{}/.local/share/Steam", home), - format!("{}/.var/app/com.valvesoftware.Steam/.steam/steam", home), - format!("{}/snap/steam/common/.steam/steam", home), - ]; - - steam_paths - .iter() - .map(PathBuf::from) - .find(|p| p.exists()) -} - -/// Find the Steam userdata directory. -#[must_use] -pub fn find_userdata_path() -> Option<PathBuf> { - let config = crate::config::AppConfig::load(); - if !config.selected_steam_account.is_empty() { - if let Some(path) = find_userdata_path_for_account(&config.selected_steam_account) { - return Some(path); - } - } - - let steam_path = find_steam_path()?; - let userdata = steam_path.join("userdata"); - - if !userdata.exists() { - return None; - } - - let accounts = get_steam_accounts(); - - if let Some(most_recent) = accounts.iter().find(|a| a.most_recent) { - let path = userdata.join(&most_recent.account_id); - if path.exists() { - log_info(&format!( - "Using Steam account from loginusers.vdf (MostRecent): {} ({})", - most_recent.persona_name, most_recent.account_id - )); - return Some(path); - } - } - - if let Some(first_account) = accounts.first() { - let path = userdata.join(&first_account.account_id); - if path.exists() { - log_info(&format!( - "Using Steam account from loginusers.vdf (most recent timestamp): {} ({})", - first_account.persona_name, first_account.account_id - )); - return Some(path); - } - } - - log_warning("Could not determine active Steam account from loginusers.vdf, falling back to directory modification time"); - - let mut user_dirs: Vec<PathBuf> = Vec::new(); - - if let Ok(entries) = fs::read_dir(&userdata) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - if let Some(name) = path.file_name() { - let name_str = name.to_string_lossy(); - if name_str != "0" && name_str.chars().all(|c| c.is_ascii_digit()) { - user_dirs.push(path); - } - } - } - } - } - - user_dirs.sort_by(|a, b| { - let a_time = fs::metadata(a).and_then(|m| m.modified()).ok(); - let b_time = fs::metadata(b).and_then(|m| m.modified()).ok(); - b_time.cmp(&a_time) - }); - - user_dirs.into_iter().next() -} - -// ============================================================================ -// Steam Account Detection (loginusers.vdf parsing) -// ============================================================================ - -/// Information about a Steam user account -#[derive(Debug, Clone)] -pub struct SteamAccount { - pub account_id: String, - pub persona_name: String, - pub most_recent: bool, - pub timestamp: u64, -} - -/// Get all Steam accounts from loginusers.vdf with their display names -#[must_use] -pub fn get_steam_accounts() -> Vec<SteamAccount> { - let Some(steam_path) = find_steam_path() else { - return Vec::new(); - }; - - let loginusers_path = steam_path.join("config/loginusers.vdf"); - let userdata_path = steam_path.join("userdata"); - - let Ok(content) = fs::read_to_string(&loginusers_path) else { - return Vec::new(); - }; - - let mut accounts = Vec::new(); - - let mut current_steam_id: Option<String> = None; - let mut current_account: Option<SteamAccountBuilder> = None; - - for line in content.lines() { - let trimmed = line.trim(); - - if trimmed.starts_with('"') && trimmed.ends_with('"') { - let id = trimmed.trim_matches('"'); - if id.len() == 17 && id.starts_with("7656") && id.chars().all(|c| c.is_ascii_digit()) { - if let (Some(steam_id), Some(builder)) = (current_steam_id.take(), current_account.take()) { - if let Some(account) = builder.build(&steam_id, &userdata_path) { - accounts.push(account); - } - } - current_steam_id = Some(id.to_string()); - current_account = Some(SteamAccountBuilder::default()); - } - } - - if let Some(ref mut builder) = current_account { - if let Some((key, value)) = parse_vdf_kv(trimmed) { - match key.to_lowercase().as_str() { - "accountname" => builder.account_name = Some(value), - "personaname" => builder.persona_name = Some(value), - "mostrecent" => builder.most_recent = value == "1", - "timestamp" => builder.timestamp = value.parse().unwrap_or(0), - _ => {} - } - } - } - } - - if let (Some(steam_id), Some(builder)) = (current_steam_id, current_account) { - if let Some(account) = builder.build(&steam_id, &userdata_path) { - accounts.push(account); - } - } - - accounts.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - - accounts -} - -#[derive(Default)] -struct SteamAccountBuilder { - account_name: Option<String>, - persona_name: Option<String>, - most_recent: bool, - timestamp: u64, -} - -impl SteamAccountBuilder { - fn build(self, steam_id: &str, userdata_base: &std::path::Path) -> Option<SteamAccount> { - let account_name = self.account_name?; - let persona_name = self.persona_name.unwrap_or_else(|| account_name.clone()); - - let steam64: u64 = steam_id.parse().ok()?; - let account_id = (steam64 - 76561197960265728).to_string(); - - let userdata_path = userdata_base.join(&account_id); - - if !userdata_path.exists() { - return None; - } - - Some(SteamAccount { - account_id, - persona_name, - most_recent: self.most_recent, - timestamp: self.timestamp, - }) - } -} - -/// Parse a VDF key-value pair like: "Key" "Value" -fn parse_vdf_kv(line: &str) -> Option<(String, String)> { - let mut parts = Vec::new(); - let mut current = String::new(); - let mut in_quotes = false; - - for c in line.chars() { - match c { - '"' => { - if in_quotes { - parts.push(current.clone()); - current.clear(); - } - in_quotes = !in_quotes; - } - _ if in_quotes => current.push(c), - _ => {} - } - } - - if parts.len() >= 2 { - Some((parts[0].clone(), parts[1].clone())) - } else { - None - } -} - -/// Find the userdata path for a specific Steam account -#[must_use] -pub fn find_userdata_path_for_account(account_id: &str) -> Option<PathBuf> { - let steam_path = find_steam_path()?; - let userdata = steam_path.join("userdata").join(account_id); - - if userdata.exists() { - Some(userdata) - } else { - None - } -} - -// ============================================================================ -// Convenience Wrappers -// ============================================================================ - -/// Detect the Steam installation path with logging. -#[must_use] -pub fn detect_steam_path_checked() -> Option<String> { - match find_steam_path() { - Some(path) => { - let path_str = path.to_string_lossy().to_string(); - log_info(&format!("Steam detected at: {}", path_str)); - Some(path_str) - } - None => { - log_warning("Steam installation not detected! NaK requires Steam to be installed."); - None - } - } -} diff --git a/libs/nak/src/steam/proton.rs b/libs/nak/src/steam/proton.rs deleted file mode 100644 index 4059dc4..0000000 --- a/libs/nak/src/steam/proton.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Steam Proton detection and management -//! -//! Finds Protons that Steam can see and use for non-Steam games. -//! This includes Steam's built-in Protons and custom Protons in compatibilitytools.d. - -use std::fs; -use std::path::PathBuf; - -use super::find_steam_path; - -/// Information about an installed Proton version -#[derive(Debug, Clone)] -pub struct SteamProton { - /// Display name (e.g., "GE-Proton9-20", "Proton Experimental") - pub name: String, - /// Internal name used in config.vdf (e.g., "proton_experimental", "GE-Proton9-20") - pub config_name: String, - /// Full path to the Proton installation - pub path: PathBuf, - /// Whether this is a Steam-provided Proton (vs custom) - pub is_steam_proton: bool, - /// Whether this is Proton Experimental - pub is_experimental: bool, -} - -impl SteamProton { - /// Get the path to the wine binary. - pub fn wine_binary(&self) -> Option<PathBuf> { - let paths = [ - self.path.join("files/bin/wine"), - self.path.join("dist/bin/wine"), - ]; - paths.into_iter().find(|p| p.exists()) - } - - /// Get the path to the wineserver binary. - pub fn wineserver_binary(&self) -> Option<PathBuf> { - let paths = [ - self.path.join("files/bin/wineserver"), - self.path.join("dist/bin/wineserver"), - ]; - paths.into_iter().find(|p| p.exists()) - } - - /// Get the bin directory containing wine executables. - pub fn bin_dir(&self) -> Option<PathBuf> { - self.wine_binary().and_then(|p| p.parent().map(|p| p.to_path_buf())) - } -} - -/// Find all Protons that Steam can use (Proton 10+ only) -pub fn find_steam_protons() -> Vec<SteamProton> { - let mut protons = Vec::new(); - - let Some(steam_path) = find_steam_path() else { - return protons; - }; - - // 1. Steam's built-in Protons (steamapps/common/Proton*) - protons.extend(find_builtin_protons(&steam_path)); - - // 2. Custom Protons in user's compatibilitytools.d - protons.extend(find_custom_protons(&steam_path)); - - // 3. System-level Protons in /usr/share/steam/compatibilitytools.d/ - // (Arch packages Proton here; Flatpak has --filesystem=/usr/share/steam:ro) - protons.extend(find_system_protons()); - - // Filter to only include Proton 10+ (required for Steam-native integration) - protons.retain(is_proton_10_or_newer); - - // Filter to only include Protons with valid wine binaries - protons.retain(|p| { - let has_wine = p.wine_binary().is_some(); - if !has_wine { - crate::logging::log_warning(&format!( - "Skipping Proton '{}': wine binary not found at expected paths (files/bin/wine or dist/bin/wine)", - p.name - )); - } - has_wine - }); - - // Sort: Experimental first, then by name descending (newest first) - protons.sort_by(|a, b| { - if a.is_experimental != b.is_experimental { - return b.is_experimental.cmp(&a.is_experimental); - } - b.name.cmp(&a.name) - }); - - protons -} - -/// Check if a Proton version is 10 or newer -fn is_proton_10_or_newer(proton: &SteamProton) -> bool { - let name = &proton.name; - - if proton.is_experimental || name.contains("Experimental") { - return true; - } - - if name.contains("CachyOS") { - return true; - } - - if name == "LegacyRuntime" || name.contains("Runtime") { - return false; - } - - if name.starts_with("GE-Proton") { - if let Some(version_part) = name.strip_prefix("GE-Proton") { - let major: Option<u32> = version_part - .split('-') - .next() - .and_then(|s| s.parse().ok()); - return major.map(|v| v >= 10).unwrap_or(false); - } - } - - if name.starts_with("Proton ") { - if let Some(version_part) = name.strip_prefix("Proton ") { - let major: Option<u32> = version_part - .split('.') - .next() - .and_then(|s| s.parse().ok()); - return major.map(|v| v >= 10).unwrap_or(false); - } - } - - if name.starts_with("EM-") { - if let Some(version_part) = name.strip_prefix("EM-") { - let major: Option<u32> = version_part - .split('.') - .next() - .and_then(|s| s.parse().ok()); - return major.map(|v| v >= 10).unwrap_or(false); - } - } - - // Unknown format - allow it - true -} - -/// Find Steam's built-in Proton versions -fn find_builtin_protons(steam_path: &std::path::Path) -> Vec<SteamProton> { - let mut found = Vec::new(); - let common_dir = steam_path.join("steamapps/common"); - - let Ok(entries) = fs::read_dir(&common_dir) else { - return found; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let name = entry.file_name().to_string_lossy().to_string(); - - if name.starts_with("Proton") && path.join("proton").exists() { - let is_experimental = name.contains("Experimental"); - - let config_name = if is_experimental { - "proton_experimental".to_string() - } else { - let version = name.replace("Proton ", ""); - let major = version.split('.').next().unwrap_or(&version); - format!("proton_{}", major) - }; - - found.push(SteamProton { - name: name.clone(), - config_name, - path, - is_steam_proton: true, - is_experimental, - }); - } - } - - found -} - -/// Find custom Protons in compatibilitytools.d -fn find_custom_protons(steam_path: &std::path::Path) -> Vec<SteamProton> { - let mut found = Vec::new(); - let compat_dir = steam_path.join("compatibilitytools.d"); - - let Ok(entries) = fs::read_dir(&compat_dir) else { - return found; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let name = entry.file_name().to_string_lossy().to_string(); - - let has_proton = path.join("proton").exists(); - let has_vdf = path.join("compatibilitytool.vdf").exists(); - - if has_proton || has_vdf { - found.push(SteamProton { - name: name.clone(), - config_name: name.clone(), - path, - is_steam_proton: false, - is_experimental: false, - }); - } - } - - found -} - -/// Find system-level Protons in /usr/share/steam/compatibilitytools.d/ -fn find_system_protons() -> Vec<SteamProton> { - let mut found = Vec::new(); - let system_compat_dir = PathBuf::from("/usr/share/steam/compatibilitytools.d"); - - let Ok(entries) = fs::read_dir(&system_compat_dir) else { - return found; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let name = entry.file_name().to_string_lossy().to_string(); - - let has_proton = path.join("proton").exists(); - let has_vdf = path.join("compatibilitytool.vdf").exists(); - - if has_proton || has_vdf { - found.push(SteamProton { - name: name.clone(), - config_name: name.clone(), - path, - is_steam_proton: false, - is_experimental: false, - }); - } - } - - found -} |
