diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-14 02:45:12 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-14 02:45:25 -0600 |
| commit | 817e8f5cd26739c69d930d21cd9dc4c0b6e4984e (patch) | |
| tree | 52aed11d6751bb7d20b06acf197d56fac113203d /libs/nak/src/game_finder/steam.rs | |
| parent | 51a9f8f197727f00896e5de44569b098923527dd (diff) | |
Add FUSE external mapping support, BG3/Oblivion Remastered fixes, fomod-plus and NaK integration
FUSE VFS now deploys non-data-dir mod mappings (Paks, OBSE, UE4SS, etc.)
via real symlinks and injects file-level data-dir mappings (plugins.txt,
loadorder.txt) into the VFS tree. Fixes game launches for Oblivion
Remastered (Root Builder path resolution, script extender support) and
BG3 (Wine prefix documents directory, file mapper symlinks on Linux).
Vendors mo2-fomod-plus plugin and NaK crate for FOMOD installer and
game finder/runtime support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/nak/src/game_finder/steam.rs')
| -rw-r--r-- | libs/nak/src/game_finder/steam.rs | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/libs/nak/src/game_finder/steam.rs b/libs/nak/src/game_finder/steam.rs new file mode 100644 index 0000000..b88a0b0 --- /dev/null +++ b/libs/nak/src/game_finder/steam.rs @@ -0,0 +1,251 @@ +//! Steam game detection +//! +//! Detects games installed via Steam by parsing appmanifest_*.acf files. +//! Supports native, Flatpak, and Snap Steam installations. + +use std::fs; +use std::path::{Path, PathBuf}; + +use super::known_games::{find_by_steam_id, KnownGame}; +use super::vdf::{parse_library_folders, AppManifest}; +use super::{Game, Launcher}; +use crate::logging::log_info; + +/// All possible Steam installation paths to check +const STEAM_PATHS: &[&str] = &[ + ".local/share/Steam", + ".steam/debian-installation", + ".steam/steam", + ".var/app/com.valvesoftware.Steam/data/Steam", + ".var/app/com.valvesoftware.Steam/.local/share/Steam", + "snap/steam/common/.local/share/Steam", +]; + +/// Detect all Steam games across all installations +pub fn detect_steam_games() -> Vec<Game> { + let mut games = Vec::new(); + let home = match std::env::var("HOME") { + Ok(h) => h, + Err(_) => return games, + }; + + // Find all Steam installations + for steam_info in find_steam_installations(&home) { + let libraries = get_library_folders(&steam_info.path); + + for library_path in libraries { + let steamapps = library_path.join("steamapps"); + if !steamapps.exists() { + continue; + } + + // Scan for appmanifest_*.acf files + let Ok(entries) = fs::read_dir(&steamapps) else { + continue; + }; + + for entry in entries.flatten() { + let path = entry.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + + if name.starts_with("appmanifest_") && name.ends_with(".acf") { + if let Some(game) = parse_appmanifest(&path, &steamapps, &steam_info) { + games.push(game); + } + } + } + } + } + + log_info(&format!("Steam: Found {} installed games", games.len())); + games +} + +/// Information about a Steam installation +struct SteamInstallation { + path: PathBuf, + is_flatpak: bool, + is_snap: bool, +} + +/// Find all Steam installations on the system +fn find_steam_installations(home: &str) -> Vec<SteamInstallation> { + let mut installations = Vec::new(); + + for relative_path in STEAM_PATHS { + let full_path = PathBuf::from(home).join(relative_path); + + // Check if this is a valid Steam installation + if full_path.join("steamapps").exists() || full_path.join("steam.pid").exists() { + let is_flatpak = relative_path.contains(".var/app/com.valvesoftware.Steam"); + let is_snap = relative_path.contains("snap/steam"); + + // Avoid duplicates (symlinks can cause the same installation to appear twice) + let canonical = full_path.canonicalize().unwrap_or(full_path.clone()); + if !installations.iter().any(|i: &SteamInstallation| { + i.path.canonicalize().unwrap_or(i.path.clone()) == canonical + }) { + log_info(&format!( + "Found Steam installation: {} (flatpak={}, snap={})", + full_path.display(), + is_flatpak, + is_snap + )); + installations.push(SteamInstallation { + path: full_path, + is_flatpak, + is_snap, + }); + } + } + } + + installations +} + +/// Get all library folders for a Steam installation +fn get_library_folders(steam_path: &Path) -> Vec<PathBuf> { + let mut folders = Vec::new(); + + // The Steam installation directory itself is always a library + folders.push(steam_path.to_path_buf()); + + // Parse libraryfolders.vdf for additional libraries + let vdf_path = steam_path.join("steamapps/libraryfolders.vdf"); + if let Ok(content) = fs::read_to_string(&vdf_path) { + for path_str in parse_library_folders(&content) { + let path = PathBuf::from(&path_str); + if path.exists() && !folders.contains(&path) { + folders.push(path); + } + } + } + + // Also check the older config/libraryfolders.vdf location + let old_vdf_path = steam_path.join("config/libraryfolders.vdf"); + if old_vdf_path != vdf_path { + if let Ok(content) = fs::read_to_string(&old_vdf_path) { + for path_str in parse_library_folders(&content) { + let path = PathBuf::from(&path_str); + if path.exists() && !folders.contains(&path) { + folders.push(path); + } + } + } + } + + folders +} + +/// Parse an appmanifest_*.acf file and create a Game struct +fn parse_appmanifest( + manifest_path: &Path, + steamapps_path: &Path, + steam_info: &SteamInstallation, +) -> Option<Game> { + let content = fs::read_to_string(manifest_path).ok()?; + let manifest = AppManifest::from_vdf(&content)?; + + // Only consider fully installed games + if !manifest.is_installed() { + return None; + } + + // Build the install path + let install_path = steamapps_path.join("common").join(&manifest.install_dir); + if !install_path.exists() { + return None; + } + + // Build the prefix path + let prefix_path = steamapps_path + .join("compatdata") + .join(&manifest.app_id) + .join("pfx"); + + let prefix_path = if prefix_path.exists() { + Some(prefix_path) + } else { + None + }; + + // Look up known game info + let known_game = find_by_steam_id(&manifest.app_id); + + Some(Game { + name: manifest.name, + app_id: manifest.app_id, + install_path, + prefix_path, + launcher: Launcher::Steam { + is_flatpak: steam_info.is_flatpak, + is_snap: steam_info.is_snap, + }, + my_games_folder: known_game.and_then(|g| g.my_games_folder.map(String::from)), + appdata_local_folder: known_game.and_then(|g| g.appdata_local_folder.map(String::from)), + appdata_roaming_folder: known_game.and_then(|g| g.appdata_roaming_folder.map(String::from)), + registry_path: known_game.map(|g| g.registry_path.to_string()), + registry_value: known_game.map(|g| g.registry_value.to_string()), + }) +} + +/// Find the installation path for a specific Steam game by App ID +pub fn find_game_install_path(app_id: &str) -> Option<PathBuf> { + let home = std::env::var("HOME").ok()?; + + for steam_info in find_steam_installations(&home) { + let libraries = get_library_folders(&steam_info.path); + + for library_path in libraries { + let manifest_path = library_path + .join("steamapps") + .join(format!("appmanifest_{}.acf", app_id)); + + if manifest_path.exists() { + let content = fs::read_to_string(&manifest_path).ok()?; + let manifest = AppManifest::from_vdf(&content)?; + + if manifest.is_installed() { + let install_path = library_path + .join("steamapps/common") + .join(&manifest.install_dir); + + if install_path.exists() { + return Some(install_path); + } + } + } + } + } + + None +} + +/// Find the Wine prefix for a specific Steam game by App ID +pub fn find_game_prefix_path(app_id: &str) -> Option<PathBuf> { + let home = std::env::var("HOME").ok()?; + + for steam_info in find_steam_installations(&home) { + let libraries = get_library_folders(&steam_info.path); + + for library_path in libraries { + let prefix_path = library_path + .join("steamapps/compatdata") + .join(app_id) + .join("pfx"); + + if prefix_path.exists() { + return Some(prefix_path); + } + } + } + + None +} + +/// Get the known game configuration for a Steam App ID +pub fn get_known_game(app_id: &str) -> Option<&'static KnownGame> { + find_by_steam_id(app_id) +} |
