aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/game_finder/mod.rs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-14 02:45:12 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-14 02:45:25 -0600
commit817e8f5cd26739c69d930d21cd9dc4c0b6e4984e (patch)
tree52aed11d6751bb7d20b06acf197d56fac113203d /libs/nak/src/game_finder/mod.rs
parent51a9f8f197727f00896e5de44569b098923527dd (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/mod.rs')
-rw-r--r--libs/nak/src/game_finder/mod.rs191
1 files changed, 191 insertions, 0 deletions
diff --git a/libs/nak/src/game_finder/mod.rs b/libs/nak/src/game_finder/mod.rs
new file mode 100644
index 0000000..c383c8a
--- /dev/null
+++ b/libs/nak/src/game_finder/mod.rs
@@ -0,0 +1,191 @@
+//! Game detection module
+//!
+//! Provides unified game detection across multiple launchers:
+//! - Steam (native, Flatpak, Snap)
+//! - Heroic (GOG, Epic)
+//! - Bottles
+
+// Allow unused items - this is a public API module
+#![allow(dead_code)]
+#![allow(unused_imports)]
+
+mod bottles;
+mod heroic;
+pub mod known_games;
+mod registry;
+mod steam;
+mod vdf;
+
+use std::path::PathBuf;
+
+pub use bottles::detect_bottles_games;
+pub use heroic::detect_heroic_games;
+pub use known_games::{find_by_gog_id, find_by_name, find_by_steam_id, KnownGame, KNOWN_GAMES};
+pub use registry::{read_registry_value, wine_path_to_linux};
+pub use steam::{detect_steam_games, find_game_install_path, find_game_prefix_path, get_known_game};
+
+// ============================================================================
+// Core Types
+// ============================================================================
+
+/// The launcher/store a game was installed from
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Launcher {
+ Steam { is_flatpak: bool, is_snap: bool },
+ Heroic { store: HeroicStore },
+ Bottles,
+}
+
+impl Launcher {
+ pub fn display_name(&self) -> &'static str {
+ match self {
+ Launcher::Steam { is_flatpak: true, .. } => "Steam (Flatpak)",
+ Launcher::Steam { is_snap: true, .. } => "Steam (Snap)",
+ Launcher::Steam { .. } => "Steam",
+ Launcher::Heroic { store: HeroicStore::GOG } => "Heroic (GOG)",
+ Launcher::Heroic { store: HeroicStore::Epic } => "Heroic (Epic)",
+ Launcher::Bottles => "Bottles",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum HeroicStore {
+ GOG,
+ Epic,
+}
+
+/// A detected game installation
+#[derive(Debug, Clone)]
+pub struct Game {
+ pub name: String,
+ pub app_id: String,
+ pub install_path: PathBuf,
+ pub prefix_path: Option<PathBuf>,
+ pub launcher: Launcher,
+ pub my_games_folder: Option<String>,
+ pub appdata_local_folder: Option<String>,
+ pub appdata_roaming_folder: Option<String>,
+ pub registry_path: Option<String>,
+ pub registry_value: Option<String>,
+}
+
+impl Game {
+ pub fn has_prefix(&self) -> bool {
+ self.prefix_path.is_some()
+ }
+
+ pub fn get_prefix_user_path(&self) -> Option<PathBuf> {
+ let prefix = self.prefix_path.as_ref()?;
+ let users_dir = prefix.join("drive_c/users");
+
+ if let Ok(entries) = std::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 Some(users_dir.join(name));
+ }
+ }
+ }
+
+ Some(users_dir.join("steamuser"))
+ }
+
+ pub fn get_prefix_documents_path(&self) -> Option<PathBuf> {
+ self.get_prefix_user_path().map(|p| p.join("Documents"))
+ }
+
+ pub fn get_prefix_my_games_path(&self) -> Option<PathBuf> {
+ let docs = self.get_prefix_documents_path()?;
+ let folder = self.my_games_folder.as_ref()?;
+ Some(docs.join("My Games").join(folder))
+ }
+
+ pub fn get_prefix_appdata_local_path(&self) -> Option<PathBuf> {
+ let user = self.get_prefix_user_path()?;
+ let folder = self.appdata_local_folder.as_ref()?;
+ Some(user.join("AppData/Local").join(folder))
+ }
+
+ pub fn get_prefix_appdata_roaming_path(&self) -> Option<PathBuf> {
+ let user = self.get_prefix_user_path()?;
+ let folder = self.appdata_roaming_folder.as_ref()?;
+ Some(user.join("AppData/Roaming").join(folder))
+ }
+}
+
+// ============================================================================
+// Scan Results
+// ============================================================================
+
+#[derive(Debug, Default)]
+pub struct GameScanResult {
+ pub games: Vec<Game>,
+ pub steam_count: usize,
+ pub heroic_count: usize,
+ pub bottles_count: usize,
+}
+
+impl GameScanResult {
+ pub fn games_with_prefixes(&self) -> impl Iterator<Item = &Game> {
+ self.games.iter().filter(|g| g.has_prefix())
+ }
+
+ pub fn games_by_launcher(&self, launcher_type: &str) -> Vec<&Game> {
+ self.games
+ .iter()
+ .filter(|g| {
+ matches!(
+ (&g.launcher, launcher_type),
+ (Launcher::Steam { .. }, "steam")
+ | (Launcher::Heroic { .. }, "heroic")
+ | (Launcher::Bottles, "bottles")
+ )
+ })
+ .collect()
+ }
+
+ pub fn find_by_name(&self, name: &str) -> Option<&Game> {
+ let name_lower = name.to_lowercase();
+ self.games
+ .iter()
+ .find(|g| g.name.to_lowercase() == name_lower)
+ }
+
+ pub fn find_by_app_id(&self, app_id: &str) -> Option<&Game> {
+ self.games.iter().find(|g| g.app_id == app_id)
+ }
+}
+
+// ============================================================================
+// Public API
+// ============================================================================
+
+/// Detect all installed games from all supported launchers
+pub fn detect_all_games() -> GameScanResult {
+ let mut result = GameScanResult::default();
+
+ let steam_games = detect_steam_games();
+ result.steam_count = steam_games.len();
+ result.games.extend(steam_games);
+
+ let heroic_games = detect_heroic_games();
+ result.heroic_count = heroic_games.len();
+ result.games.extend(heroic_games);
+
+ let bottles_games = detect_bottles_games();
+ result.bottles_count = bottles_games.len();
+ result.games.extend(bottles_games);
+
+ result
+}
+
+/// Detect only Steam games
+pub fn detect_steam_only() -> GameScanResult {
+ let steam_games = detect_steam_games();
+ GameScanResult {
+ steam_count: steam_games.len(),
+ games: steam_games,
+ ..Default::default()
+ }
+}