From 817e8f5cd26739c69d930d21cd9dc4c0b6e4984e Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 14 Feb 2026 02:45:12 -0600 Subject: 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 --- libs/nak/src/game_finder/mod.rs | 191 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 libs/nak/src/game_finder/mod.rs (limited to 'libs/nak/src/game_finder/mod.rs') 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, + pub launcher: Launcher, + pub my_games_folder: Option, + pub appdata_local_folder: Option, + pub appdata_roaming_folder: Option, + pub registry_path: Option, + pub registry_value: Option, +} + +impl Game { + pub fn has_prefix(&self) -> bool { + self.prefix_path.is_some() + } + + pub fn get_prefix_user_path(&self) -> Option { + 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 { + self.get_prefix_user_path().map(|p| p.join("Documents")) + } + + pub fn get_prefix_my_games_path(&self) -> Option { + 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 { + 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 { + 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, + pub steam_count: usize, + pub heroic_count: usize, + pub bottles_count: usize, +} + +impl GameScanResult { + pub fn games_with_prefixes(&self) -> impl Iterator { + 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() + } +} -- cgit v1.3.1