aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/steam/paths.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/steam/paths.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/steam/paths.rs')
-rw-r--r--libs/nak/src/steam/paths.rs259
1 files changed, 259 insertions, 0 deletions
diff --git a/libs/nak/src/steam/paths.rs b/libs/nak/src/steam/paths.rs
new file mode 100644
index 0000000..8ef5013
--- /dev/null
+++ b/libs/nak/src/steam/paths.rs
@@ -0,0 +1,259 @@
+//! 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
+ }
+ }
+}