aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/game_finder/steam.rs
diff options
context:
space:
mode:
Diffstat (limited to 'libs/nak/src/game_finder/steam.rs')
-rw-r--r--libs/nak/src/game_finder/steam.rs251
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)
+}