aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/steam/paths.rs
diff options
context:
space:
mode:
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
+ }
+ }
+}