From 95538ad3a6027aa53f49ca3b2b929b9925a86ce5 Mon Sep 17 00:00:00 2001 From: Bash-09 <47521168+Bash-09@users.noreply.github.com> Date: Sat, 26 Aug 2023 15:25:10 +1000 Subject: [PATCH] Bash09/steamlocate (#47) * Refactor to use steamlocate and improve error handling when loading settings. --------- Co-authored-by: Bash <> --- Cargo.lock | 70 +++++++++++++++++++++- Cargo.toml | 1 + src/gamefinder.rs | 135 +++++++++---------------------------------- src/launchoptions.rs | 19 +++--- src/main.rs | 33 ++++++++--- src/settings.rs | 86 ++++++++++++--------------- 6 files changed, 167 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2afd265..290bbed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,7 @@ dependencies = [ "serde_json", "serde_yaml", "steamid-ng", + "steamlocate", "substring", "tappet", "thiserror", @@ -428,6 +429,27 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1024,6 +1046,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1210,6 +1238,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -1539,7 +1573,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -1829,6 +1863,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "steamlocate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec01c74611d14a808cb212d17c6e03f0e30736a15ed1d5736f8a53154cea3ae" +dependencies = [ + "dirs", + "keyvalues-parser", + "keyvalues-serde", + "serde", + "steamid-ng", + "steamy-vdf", + "winreg 0.11.0", +] + +[[package]] +name = "steamy-vdf" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533127ad49314bfe71c3d3fd36b3ebac3d24f40618092e70e1cfe8362c7fac79" +dependencies = [ + "nom", +] + [[package]] name = "strsim" version = "0.10.0" @@ -2488,3 +2546,13 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if", + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml index d38de89..0140a18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,4 @@ keyvalues-serde = "0.1.0" substring = "1.4.5" tower-http = { version = "0.4.3", features = ["cors"] } include_dir = "0.7.3" +steamlocate = { version = "1.2.1", features = ["steamid_ng"] } diff --git a/src/gamefinder.rs b/src/gamefinder.rs index 4ee1de8..19dc996 100644 --- a/src/gamefinder.rs +++ b/src/gamefinder.rs @@ -1,128 +1,45 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; +use anyhow::{anyhow, Result}; use steamid_ng::SteamID; +use steamlocate::SteamDir; -pub const TF2_GAME_ID: &str = "440"; +pub const TF2_GAME_ID: u32 = 440; -pub fn locate_steam_logged_in_users() -> Option { +pub fn locate_steam_logged_in_users() -> Result { tracing::debug!("Fetching Steam loginusers.vdf"); - let mut base_folder: PathBuf = find_base_lib(); + let mut base_folder: PathBuf = SteamDir::locate() + .ok_or(anyhow!("Failed to locate Steam directory"))? + .path; base_folder.push::("config/loginusers.vdf".into()); if base_folder.as_path().exists() { - tracing::info!("Located current Steam user data."); - Some(base_folder) + Ok(base_folder) } else { - tracing::error!("Could not locate loginusers.vdf in the Steam dir"); - None + Err(anyhow!("Could not locate loginusers.vdf in the Steam dir")) } } -pub fn locate_steam_launch_configs(steam_user: SteamID) -> Option { - tracing::debug!("Fetching Steam userdata//config/localconfig.vdf"); +pub fn locate_steam_launch_configs(steam_user: SteamID) -> Result { let a_id = steam_user.account_id(); - let mut base_folder: PathBuf = find_base_lib(); - base_folder.push::(format!("userdata/{}/config/localconfig.vdf", a_id,).into()); + let local_config_path = format!("userdata/{}/config/localconfig.vdf", a_id); + tracing::debug!("Fetching Steam {}", local_config_path); + + let steam = SteamDir::locate().ok_or(anyhow!("Failed to locate Steam directory."))?; + let mut base_folder: PathBuf = steam.path; + base_folder.push(local_config_path); if base_folder.as_path().exists() { - tracing::info!("Located local launch configs."); - Some(base_folder) + Ok(base_folder) } else { - tracing::error!("Could not find local configs (player not found)."); - None + Err(anyhow!("Could not find local configs (player not found).")) } } /// Attempts to open the TF2 directory or locate it if it's not in the expected place -pub fn locate_tf2_folder() -> Option { - tracing::debug!("Fetching TF2 Folder"); - let libs: Vec = fetch_libraryfolders(); - - for lib in libs { - let mut path = lib.to_path_buf(); - path.push::(get_rel_tf2_path().into()); - tracing::debug!("Found TF2 Folder: {:?}", path); - - if path.exists() && verify_tf_location(&path) { - tracing::info!("Using TF2 directory: {}", path.to_string_lossy()); - return Some(path); - } - } - None -} - -fn fetch_libraryfolders() -> Vec { - tracing::debug!("Attempting to open libraryfolders.vdf"); - const LIBFILE: &str = "libraryfolders.vdf"; - let libraryfolders = fs::read_to_string(Path::join(&find_default_lib(), LIBFILE)); - - match libraryfolders { - Ok(content) => { - let mut paths = Vec::new(); - let lines = content.lines(); - - for line in lines { - if line.contains("path") { - if let Some(path_str) = line.split('"').nth(3) { - paths.push(PathBuf::from(path_str)); - } - } - } - - tracing::debug!("Successfully read libraryfolders"); - paths - } - Err(err) => { - tracing::error!("Failed to read libraryfolders.vdf: {:?}", err); - Vec::new() - } - } -} - -fn find_default_lib() -> PathBuf { - let mut path: PathBuf = find_base_lib(); - path.push::("steamapps/".into()); - - path -} - -fn find_base_lib() -> PathBuf { - #[cfg(target_os = "windows")] - let default_dir = r"C:\Program Files (x86)\Steam\".into(); - - #[cfg(not(target_os = "windows"))] - let default_dir = { - use std::env::var_os; - var_os("HOME") - .map(PathBuf::from) - .unwrap_or("~".into()) - .join(".steam/steam/") - }; - - default_dir -} - -fn verify_tf_location(lib: &Path) -> bool { - tracing::debug!("Start TF2 Verification of {:?}", lib.to_string_lossy()); - let gameinfo = "tf/gameinfo.txt"; - let mut path = lib.to_path_buf(); - path.push(gameinfo); - - if path.exists() { - tracing::debug!("Passed Verification Check"); - return true; - } - tracing::debug!("Failed Verification Check"); - false -} - -#[cfg(target_os = "windows")] -fn get_rel_tf2_path() -> String { - r"steamapps\common\Team Fortress 2".to_string() -} - -#[cfg(not(target_os = "windows"))] -fn get_rel_tf2_path() -> String { - r"steamapps/common/Team Fortress 2".to_string() +pub fn locate_tf2_folder() -> Result { + Ok(SteamDir::locate() + .ok_or(anyhow!("Failed to locate Steam directory"))? + .app(&TF2_GAME_ID) + .ok_or(anyhow!("Failed to locate TF2 installation."))? + .path + .clone()) } diff --git a/src/launchoptions.rs b/src/launchoptions.rs index b0307da..5bf0cf7 100644 --- a/src/launchoptions.rs +++ b/src/launchoptions.rs @@ -23,14 +23,14 @@ pub const TF2_REQUIRED_OPTS: [&str; 4] = ["-condebug", "-conclearlog", "-usercon /// ID. /// Handles referencing the VDF store of a Steam app's launch options and provides an interface to read /// and write launch options based on a set of required options. -pub struct LaunchOptionsV2 { +pub struct LaunchOptions { local_config: PathBuf, launch_args_regex: Regex, app_data: Option, new_app_data: Option, } -impl LaunchOptionsV2 { +impl LaunchOptions { /// Get the current configured launch options for the target app under the current logged in steam user. /// /// # Errors @@ -39,7 +39,7 @@ impl LaunchOptionsV2 { /// - Could not read the `localconfig.vdf` file. (because of any non-`ErrorKind::Interrupted` during read) /// - Failed to parse the `localconfig.vdf` file. (File is corrupted/broken/incomplete) /// - Target app ID does not exist in `localconfig.vdf` file or the object is corrupted. - pub fn new(user: SteamID, target_app: String) -> Result { + pub fn new(user: SteamID, target_app: String) -> Result { let span = tracing::span!(Level::INFO, "LaunchOptions"); let _enter = span.enter(); @@ -84,7 +84,7 @@ impl LaunchOptionsV2 { let launch_options_regex = Regex::new(r#"\t{6}"LaunchOptions"\t{2}"([(\-\w)\s]*)""#) .expect("Constructing LaunchOptions regex"); - Ok(LaunchOptionsV2 { + Ok(LaunchOptions { local_config: config_path, launch_args_regex: launch_options_regex, app_data: matched_app_block, @@ -109,10 +109,13 @@ impl LaunchOptionsV2 { None => &self.app_data, }; let app_data = data_ref.clone().context("No data currently stored.")?; - let current_args = self - .launch_args_regex - .find(&app_data) - .context("Failed to find launch args object.")?; + let current_args = match self.launch_args_regex.find(&app_data) { + Some(current_args) => current_args, + None => { + missing_args.extend(TF2_REQUIRED_OPTS.iter()); + return Ok(missing_args); + } + }; let mat_str = current_args.as_str(); TF2_REQUIRED_OPTS.iter().for_each(|opt| { diff --git a/src/main.rs b/src/main.rs index f905585..e9dc662 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use tokio::sync::mpsc::UnboundedSender; use clap::{ArgAction, Parser}; use io::{Commands, IOManager}; -use launchoptions::LaunchOptionsV2; +use launchoptions::LaunchOptions; use settings::Settings; use state::State; use tracing_appender::non_blocking::WorkerGuard; @@ -47,7 +47,7 @@ pub struct Args { /// Override the configured Steam API key, #[arg(short, long)] pub api_key: Option, - /// Rewrite the user localconfig.vdf to append the corrected set of launch options if necessary. + /// Rewrite the user localconfig.vdf to append the corrected set of launch options if necessary (only works when steam is not running). #[arg(long = "rewrite_launch_opts", action=ArgAction::SetTrue, default_value_t=false)] pub rewrite_launch_options: bool, /// Do not panic on detecting missing launch options or failure to read/parse the localconfig.vdf file. @@ -76,7 +76,7 @@ async fn main() { Settings::load(&args) }; - let settings = match settings { + let mut settings = match settings { Ok(settings) => settings, Err(e) => { tracing::warn!("Failed to load settings, continuing with defaults: {:?}", e); @@ -84,8 +84,21 @@ async fn main() { } }; + // Locate TF2 directory + match gamefinder::locate_tf2_folder() { + Ok(tf2_directory) => { + settings.set_tf2_directory(tf2_directory); + } + Err(e) => { + if args.tf2_dir.is_none() { + tracing::error!("Could not locate TF2 directory: {:?}", e); + tracing::error!("If you have a valid TF2 installation you can specify it manually by appending ' --tf2_dir \"Path to Team Fortress 2 folder\"' when running the program."); + } + } + } + // Launch options and overrides - let launch_opts = match LaunchOptionsV2::new( + let launch_opts = match LaunchOptions::new( settings .get_steam_user() .expect("Failed to identify the local steam user (failed to find `loginusers.vdf`)"), @@ -129,10 +142,12 @@ async fn main() { } Err(missing_opts_err) => { - tracing::warn!("Failed to verify app launch options: {}", missing_opts_err); if !(args.ignore_launch_options) { - panic!( - "Missing required launch options in TF2 for MAC to function. Aborting..." + panic!("Failed to verify app launch options: {}", missing_opts_err); + } else { + tracing::error!( + "Failed to verify app launch options: {:?}", + missing_opts_err ); } } @@ -174,7 +189,7 @@ async fn main() { } // Check autolaunch ui setting before settings is borrowed - let autolaunch_ui = args.autolaunch_ui || (&settings).get_autolaunch_ui(); + let autolaunch_ui = args.autolaunch_ui || settings.get_autolaunch_ui(); // Initialize State State::initialize_state(State::new(settings, playerlist)); @@ -191,7 +206,7 @@ async fn main() { if let Err(e) = open::that(Path::new(&format!("http://localhost:{}", port))) { tracing::error!("Failed to open web browser: {:?}", e); } - } + } // Steam API loop let (steam_api_requester, steam_api_receiver) = tokio::sync::mpsc::unbounded_channel(); diff --git a/src/settings.rs b/src/settings.rs index 9ac617d..f27e3ee 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,13 +1,12 @@ use std::{ - fs::File, fs::OpenOptions, + io::ErrorKind, io::{self, Write}, - io::{ErrorKind, Read}, path::{Path, PathBuf}, sync::Arc, }; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use directories_next::ProjectDirs; use keyvalues_parser::Vdf; use serde::{Deserialize, Serialize}; @@ -39,6 +38,7 @@ pub struct Settings { config_path: Option, #[serde(skip)] steam_user: Option, + #[serde(skip)] tf2_directory: PathBuf, rcon_password: Arc, steam_api_key: Arc, @@ -89,8 +89,6 @@ impl Settings { settings.config_path = Some(path); - // settings.steam_user = Settings::load_current_steam_user(); - tracing::debug!("Successfully loaded settings."); settings.set_overrides(args); Ok(settings) @@ -98,29 +96,21 @@ impl Settings { /// Reads the Steam/config/loginusers.vdf file to find the currently logged in /// steam ID. - fn load_current_steam_user() -> Option { + fn load_current_steam_user() -> Result { tracing::debug!("Loading steam user login data from Steam directory"); - let steam_user_conf = gamefinder::locate_steam_logged_in_users().unwrap_or(PathBuf::new()); - let mut steam_use_conf_str: Vec = Vec::new(); - if let Ok(mut file) = File::open(steam_user_conf.as_path()) { - let _ = file - .read_to_end(&mut steam_use_conf_str) - .context("Failed reading loginusers.vdf."); - tracing::info!("Loaded steam user login data."); - } else { - tracing::error!("Could not open loginusers.vdf from Steam dir."); - } + let user_conf_path = gamefinder::locate_steam_logged_in_users() + .context("Could not locate logged in steam user.")?; + let user_conf_contents = std::fs::read(user_conf_path) + .context("Failed to read logged in user configuration.")?; - match Vdf::parse(&String::from_utf8_lossy(&steam_use_conf_str)) { + match Vdf::parse(&String::from_utf8_lossy(&user_conf_contents)) { Ok(login_vdf) => { - let users_obj = if let Some(obj) = login_vdf.value.get_obj() { - obj - } else { - tracing::error!("Failed to get user data from VDF."); - return None; - }; + let users_obj = login_vdf + .value + .get_obj() + .ok_or(anyhow!("Failed to parse loginusers.vdf"))?; let mut latest_timestamp = 0; - let mut latest_user_sid64: Option<&str> = None; + let mut latest_user_sid64: Option = None; for (user_sid64, user_data_values) in users_obj.iter() { user_data_values @@ -134,35 +124,23 @@ impl Settings { .and_then(|timestamp_str| timestamp_str.parse::().ok()) { if timestamp > latest_timestamp { - latest_timestamp = timestamp; - latest_user_sid64 = Some(user_sid64); + if let Ok(user_steamid) = + user_sid64.parse::().map(SteamID::from) + { + latest_timestamp = timestamp; + latest_user_sid64 = Some(user_steamid); + } } } }); } - let user_sid64 = if let Some(sid64) = latest_user_sid64 { - sid64 - } else { - tracing::error!("No user with a valid timestamp found."); - return None; - }; - - user_sid64.parse::().map_or_else( - |why| { - tracing::error!("Invalid SID64 found in user data: {}.", why); - None - }, - |user_int64| { - tracing::info!("Parsed most recent steam user <{}>", user_int64); - Some(SteamID::from(user_int64)) - }, - ) - } - Err(parse_err) => { - tracing::error!("Failed to parse loginusers VDF data: {}.", parse_err); - None + latest_user_sid64.ok_or(anyhow!("No user with a valid timestamp found.")) } + Err(parse_err) => Err(anyhow!( + "Failed to parse loginusers VDF data: {}.", + parse_err + )), } } @@ -308,15 +286,23 @@ impl Settings { impl Default for Settings { fn default() -> Self { - let tf2_directory = gamefinder::locate_tf2_folder().unwrap_or(PathBuf::new()); let config_path = Self::locate_config_file_path() .map_err(|e| tracing::error!("Failed to create config directory: {:?}", e)) .ok(); + let steam_user = Self::load_current_steam_user() + .map_err(|e| tracing::error!("Failed to load steam user: {:?}", e)) + .ok(); + if let Some(steam_user) = &steam_user { + tracing::info!( + "Identified current steam user as {}", + u64::from(*steam_user) + ); + } Settings { - steam_user: Self::load_current_steam_user(), + steam_user, config_path, - tf2_directory, + tf2_directory: PathBuf::default(), rcon_password: "mac_rcon".into(), steam_api_key: "YOUR_API_KEY_HERE".into(), port: 3621,