diff --git a/src/main.rs b/src/main.rs index ea5f962..3ae042c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,21 @@ use core::panic; use std::fs; use std::env; -use std::path::{PathBuf, Path}; -use regex::Regex; +use std::path::{PathBuf}; use std::io::{BufRead, BufReader}; use std::fs::File; use std::process::{Command, exit}; -use std::collections::HashMap; -use log::{warn, debug, error, info, LevelFilter}; +use log::{debug, info, LevelFilter}; use chrono::Local; -fn main() { +mod platform; +mod shared; +#[cfg(target_os = "windows")] +use platform::windows as platform_os; +#[cfg(target_os = "linux")] +use platform::linux as platform_os; +fn main() { //grab original command without self let mut args = env::args().skip(1); @@ -19,111 +23,35 @@ fn main() { let Some(command) = args.next() else { panic!("No command to run. Was Steam launch option set to `%command%`?") }; + + //setup before so the logging starts as soon as possible + let good_config_paths: PathBuf = platform_os::get_good_config_paths(); //Os Independent Vars - let home_dir = home::home_dir().expect("Could not determine home directory"); - let mut good_config_paths = home_dir.join("Documents").join("game_configs"); let paths_file = env::current_exe().unwrap().parent().unwrap().to_path_buf().join("paths.txt"); let steam_app_id = env::var("SteamAppId").expect("Not running under steam"); let steam_user = env::var("SteamUser").expect("Steam User not found!"); + + let log_file_path = good_config_paths.join(format!("{}_config_workaround.log", steam_app_id)); - let steam_id: String; - let steam_id_3: String; - const STEAM_ID_OFFSET: u64 = 76561197960265728; - - //Setup logging + setup_logging(log_file_path); - //Hook panic and expect - //Panic and expect are not the best ideas but for a program that needs everything else before to continue running its fine - std::panic::set_hook(Box::new(|info| { - log::error!("Panic: {}", info); - })); - //Loglevel depending on build - let log_level = if cfg!(debug_assertions) { - LevelFilter::Debug // Debug build - } else { - LevelFilter::Info // Release build - }; - //Setup fern - fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "[{} {} {}]", - Local::now().format("%Y-%m-%d %H:%M:%S"), - record.level(), - message - )) - }) - .level(log_level) - .chain(std::io::stdout()) - .chain(fern::log_file(log_file_path).expect("Unable to access log file")) - .apply().expect("Unable to create logger"); - //Finish logging setup - - // variables needed depending on os - //Linux only - const WINE_USER_PATH: &str = "pfx/drive_c/users/steamuser"; - let config_vdf_linux = home_dir.join(".local").join("share").join("Steam").join("config").join("config.vdf"); - let mut proton_prefix: Option = None; - - //Windows only - let mut custom_env: HashMap = HashMap::new(); - //Assume default steam install path for now - let config_vdf_windows: PathBuf = PathBuf::from("C:/Program Files (x86)/Steam/config/config.vdf"); - - //Set needed variables depending on OS - match std::env::consts::OS { - //Set variables if under windows - "windows" => { - steam_id = env::var("STEAMID").unwrap_or_else(|_| { - warn!("SteamID not set via env vars on windows. Falling back to config parsing"); - return get_steamid_from_config(&config_vdf_windows, &steam_user); - }); - let steam_id_3_u64: u64 = steam_id.parse::().expect("msg") - STEAM_ID_OFFSET; - steam_id_3 = steam_id_3_u64.to_string(); - //Get document path from registry - let custom_documents_path = get_documents_path(); - //Override default good config path with the one from registry - good_config_paths = custom_documents_path.join("game_configs"); + //INIT Main OS Specific + let (steam_id, steam_id_3) = platform_os::init(&steam_user); + - //Create custom env for shell expanding later - custom_env.insert("STEAMID".to_string(), steam_id.to_string()); - custom_env.insert("SteamID3".to_string(), steam_id_3.to_string()); - custom_env.insert("DOCUMENTS".to_string(), custom_documents_path.to_string_lossy().to_string()); - } - //Set variables if under linux - "linux" => { - proton_prefix = Some(PathBuf::from(env::var("STEAM_COMPAT_DATA_PATH").expect("No proton compat data folder found. This tool does not support linux native games!"))); - //.local/share/Steam/config/config.vdf - steam_id = get_steamid_from_config(&config_vdf_linux, &steam_user); - let steam_id_3_u64: u64 = steam_id.parse::().expect("SteamID is not a number!") - STEAM_ID_OFFSET; - steam_id_3 = steam_id_3_u64.to_string(); - } - _ => { - panic!("Unsupported OS at this stage???") - } - } //Startup finished. Log debug info debug!("Startup finished. Logging debug info"); debug!("Operating System: {}", std::env::consts::OS ); debug!("Steam App ID: {}", steam_app_id ); - debug!("Home Directory: {}", home_dir.to_string_lossy()); + debug!("Home Directory: {}", home::home_dir().unwrap().to_string_lossy()); debug!("Good Config Directory: {}", good_config_paths.to_string_lossy()); debug!("Paths file: {}", paths_file.to_string_lossy()); debug!("SteamID64: {}", steam_id); debug!("SteamID3: {}", steam_id_3); - match std::env::consts::OS { - "windows" => { - for (k, v) in &custom_env { - debug!("{} = {}", k, v); - } - } - "linux" => { - debug!("Proton Prefix: {}", proton_prefix.as_ref().unwrap().to_string_lossy()) - } - _ => {} - } + platform_os::print_debug(); //Finish debug logging + //Create Config Dir if not exists fs::create_dir_all(&good_config_paths).expect("Could not create config dir"); @@ -149,7 +77,7 @@ fn main() { } else { info!("Restoring configs to game: {}", steam_app_id); //Copy configs to game - process_configs(true, &matching_lines, &steam_id, &steam_id_3, &steam_app_id, proton_prefix.as_deref(), &custom_env, &WINE_USER_PATH, good_config_paths.as_path()); + platform_os::process_configs(true, &matching_lines, &steam_id, &steam_id_3, &steam_app_id, good_config_paths.as_path()); info!("Finished restoring to game: {}, launching...", steam_app_id); //Launch Game Command::new(&command) @@ -157,131 +85,37 @@ fn main() { .status().expect("Failed to launch original game"); // Wait for game to finish //Copy configs to good folder info!("Game: {} exited. Backing up config files.", steam_app_id); - process_configs(false, &matching_lines, &steam_id, &steam_id_3, &steam_app_id, proton_prefix.as_deref(), &custom_env, &WINE_USER_PATH, good_config_paths.as_path()); + platform_os::process_configs(false, &matching_lines, &steam_id, &steam_id_3, &steam_app_id, good_config_paths.as_path()); info!("Finished backing up config files. for game: {}. Exiting.", steam_app_id); exit(1) } } -fn process_configs( - to_game: bool, - matching_lines: &[String], - steam_id: &str, - steam_id_3: &str, - steam_app_id: &str, - proton_prefix: Option<&Path>, - custom_envs: &HashMap, - win_user_path: &str, - good_config_paths: &Path -) { - for raw_config in matching_lines { - let mut split = raw_config.split(';'); - split.next(); // Skip app id - let mut config_path = split.next().unwrap().to_string(); - let config = split.next().unwrap(); - - let game_config_path: PathBuf; - - match std::env::consts::OS { - "linux" => { - config_path = config_path.replace("%APPDATA%", "Application Data") - .replace("%DOCUMENTS%", "Documents") - .replace("%USERPROFILE%", "") - .replace("%LOCALAPPDATA%", "AppData/Local") - .replace("%STEAMID%", steam_id) - .replace("%SteamID3%", steam_id_3); - game_config_path = match proton_prefix { - Some(prefix) => prefix.join(win_user_path).join(&config_path), - None => { - panic!("Error: Proton prefix not set on Linux."); - } - } - } - "windows" => { - //config_path = config_path.replace("%APPDATA%", "AppData"); - //config_path = config_path.replace("%DOCUMENTS%", "Documents"); - //config_path = config_path.replace("%USERPROFILE%", ""); - //config_path = config_path.replace("%LOCALAPPDATA%", "AppData/Local"); - //config_path = config_path.replace("%STEAMID%", steam_id.as_str()); - //config_path = config_path.replace("%SteamID3%", steam_id_3.as_str()); - //Use shell expansion to replace the variables - config_path = expand_windows_env_vars(&config_path, Some(&custom_envs)); - game_config_path = PathBuf::from(config_path); - } - _ => { - panic!("OS Not Supported!") - } - } - if to_game { - copy_configs( - &good_config_paths.join(steam_app_id).join(config), - &game_config_path.join(config) - ); - } else { - copy_configs( - &game_config_path.join(config), - &good_config_paths.join(steam_app_id).join(config) - ); - } - } -} - -fn copy_configs(from: &Path, to: &Path){ - if let Err(e) = fs::copy(&from, &to){ - error!("Failed to Copy {} to {}", from.to_string_lossy(), to.to_string_lossy()); - error!("Error: {}", e); - } else { - info!("Copied {} to {}", from.to_string_lossy(), to.to_string_lossy()); - } -} - - -fn get_steamid_from_config(config_path: &Path, steam_user: &str) -> String{ - //STEAMID=$(grep -Pzo '"'${SteamUser}'"\s+{\s+"SteamID"\s+"[0-9]+"' /home/${USER}/.local/share/Steam/config/config.vdf | grep --text -oP '(?<=\s")[0-9]+') - let contents = fs::read_to_string(&config_path).expect("Failed to read config.vdf"); - let block_re = Regex::new(&format!(r#""{}"\s*\{{[^{{}}]*?"SteamID"\s+"([0-9]+)""#,regex::escape(&steam_user))).expect("Regex invalid"); - if let Some(caps) = block_re.captures(&contents) { - let steam_id: String = caps[1].to_string(); - return steam_id; +fn setup_logging(log_file_path: PathBuf){ + //Hook panic and expect + //Panic and expect are not the best ideas but for a program that needs everything else before to continue running its fine + std::panic::set_hook(Box::new(|info| { + log::error!("Panic: {}", info); + })); + //Loglevel depending on build + let log_level = if cfg!(debug_assertions) { + LevelFilter::Debug // Debug build } else { - panic!("No steamid found in config.vdf. How is this possible?") - } -} - -#[cfg(windows)] -fn get_documents_path() -> PathBuf { - use winreg::enums::*; - use winreg::RegKey; - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let key = hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders") - .expect("Could not open registry key for Documents folder"); - - let documents: String = key.get_value("Personal") - .expect("Could not read 'Personal' (Documents) path from registry"); - - // Expand environment variables like %USERPROFILE% - let expanded = expand_windows_env_vars(&documents, None); - - PathBuf::from(expanded.to_string()) -} - -#[cfg(not(windows))] -fn get_documents_path() -> PathBuf { - panic!("get_documents_path() should only be called on Windows"); -} - -// Expand %% windows vars with vars from the hashmap is exists otherwise from environment -fn expand_windows_env_vars(input: &str, overrides: Option<&HashMap>) -> String { - let re = Regex::new(r"%([^%]+)%").unwrap(); - re.replace_all(input, |caps: ®ex::Captures| { - let key = &caps[1]; - overrides - .and_then(|map| map.get(key).cloned()) - .or_else(|| std::env::var(key).ok()) - .unwrap_or_default() - }).to_string() -} - - + LevelFilter::Info // Release build + }; + //Setup fern + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{} {} {}]", + Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + message + )) + }) + .level(log_level) + .chain(std::io::stdout()) + .chain(fern::log_file(log_file_path).expect("Unable to access log file")) + .apply().expect("Unable to create logger"); +} \ No newline at end of file diff --git a/src/platform/linux.rs b/src/platform/linux.rs new file mode 100644 index 0000000..1826d4b --- /dev/null +++ b/src/platform/linux.rs @@ -0,0 +1,67 @@ +use winreg::{RegKey, types::FromRegValue, HKEY}; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; +use regex::Regex; +use std::collections::HashMap; +use std::env; +use log::{warn, debug, error, info, LevelFilter}; +use crate::shared; + +//Linux only +const WINE_USER_PATH: &str = "pfx/drive_c/users/steamuser"; +static PROTON_PREFIX: OnceLock = OnceLock::new(); + +pub fn get_good_config_paths() -> PathBuf{ + return home::home_dir().unwrap().join("Documents").join("game_configs"); +} + +pub fn init(steam_user: &str) -> (String, String) { + PROTON_PREFIX.set(PathBuf::from(env::var("STEAM_COMPAT_DATA_PATH").expect("No proton compat data folder found. This tool does not support linux native games!"))); + //.local/share/Steam/config/config.vdf + let config_vdf_linux = home::home_dir().unwrap().join(".local").join("share").join("Steam").join("config").join("config.vdf"); + let steam_id = shared::get_steamid_from_config(config_vdf_linux, &steam_user); + let steam_id_3_u64: u64 = steam_id.parse::().expect("SteamID is not a number!") - shared::STEAM_ID_OFFSET; + let steam_id_3 = steam_id_3_u64.to_string(); + return (steam_id, steam_id_3); +} + +pub fn print_debug(){ + debug!("Proton Prefix: {}", PROTON_PREFIX.get().unwrap().to_string_lossy().to_string()); +} + +pub fn process_configs( + to_game: bool, + matching_lines: &Vec, + _steam_id: &str, + _steam_id_3: &str, + steam_app_id: &str, + good_config_paths: &Path +) { + for raw_config in matching_lines { + + let (config_path, config) = shared::process_raw_config_line(raw_config); + + let game_config_path: PathBuf; + + let config_path = config_path.replace("%APPDATA%", "Application Data") + .replace("%DOCUMENTS%", "Documents") + .replace("%USERPROFILE%", "") + .replace("%LOCALAPPDATA%", "AppData/Local") + .replace("%STEAMID%", _steam_id) + .replace("%SteamID3%", _steam_id_3); + + game_config_path = PROTON_PREFIX.get().unwrap().join(WINE_USER_PATH).join(config_path); + + if to_game { + shared::copy_configs( + &good_config_paths.join(steam_app_id).join(config), + &game_config_path.join(config) + ); + } else { + shared::copy_configs( + &game_config_path.join(config), + &good_config_paths.join(steam_app_id).join(config) + ); + } + } +} \ No newline at end of file diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..04b7260 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,5 @@ +#[cfg(target_os = "windows")] +pub mod windows; + +#[cfg(target_os = "linux")] +pub mod linux; \ No newline at end of file diff --git a/src/platform/windows.rs b/src/platform/windows.rs new file mode 100644 index 0000000..5e1ad90 --- /dev/null +++ b/src/platform/windows.rs @@ -0,0 +1,142 @@ +use winreg::enums::*; +use winreg::{RegKey, types::FromRegValue, HKEY}; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; +use regex::Regex; +use std::collections::HashMap; +use std::env; +use log::{warn, debug}; +use crate::shared; + +const DOCUMENTS_REGKEY_PATH: &str = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"; +const DOCUMENTS_REGKEY: &str = "Personal"; +const STEAM_REGKEY_PATH: &str = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Valve\\Steam"; +const STEAM_REGKEY: &str = "InstallPath"; + +static CUSTOM_ENV: OnceLock> = OnceLock::new(); +//const default_config_vdf_windows: PathBuf = PathBuf::from("C:/Program Files (x86)/Steam/config/config.vdf"); +static DOCUMENTS_PATH: OnceLock = OnceLock::new(); + +//static STEAM_PATH: OnceLock = OnceLock::new(); +const DEFAULT_STEAM_PATH: &str = "C:\\Program Files (x86)\\Steam\\"; + +pub fn get_good_config_paths() -> PathBuf{ + if !DOCUMENTS_PATH.get().is_some(){ + set_documents_path(); + } + return DOCUMENTS_PATH.get().unwrap().join("game_configs") +} + +fn set_documents_path(){ + let documents_path: PathBuf = get_regkey_value::(DOCUMENTS_REGKEY_PATH, DOCUMENTS_REGKEY) + .map(PathBuf::from) + .unwrap_or_else(|_| { + warn!("No Documents Path set in registry, falling back to default path"); + home::home_dir().expect("No Home Directory Found!").join("Documents") + }); + DOCUMENTS_PATH.set(documents_path).expect("Documents path already set? How is this possible!"); +} + +//Main entrypoint MUST BE CALLED! +pub fn init(steam_user: &str) -> (String, String) { + let steam_path: PathBuf = get_regkey_value::(STEAM_REGKEY_PATH, STEAM_REGKEY) + .map(PathBuf::from) + .unwrap_or_else(|_| { + warn!("No steam path found in registry, falling back to default path"); + return PathBuf::from(DEFAULT_STEAM_PATH); + }); + + let steam_id = env::var("STEAMID").unwrap_or_else(|_| { + warn!("SteamID not set via env vars on windows. Falling back to config parsing"); + return shared::get_steamid_from_config(steam_path.join("config").join("config.vdf"), &steam_user); + }); + let steam_id_3_u64: u64 = steam_id.parse::().expect("msg") - shared::STEAM_ID_OFFSET; + let steam_id_3 = steam_id_3_u64.to_string(); + + let mut custom_env: HashMap = HashMap::new(); + //Create custom env for shell expanding later + custom_env.insert("STEAMID".to_string(), steam_id.to_string()); + custom_env.insert("SteamID3".to_string(), steam_id_3.to_string()); + custom_env.insert("DOCUMENTS".to_string(), DOCUMENTS_PATH.get().unwrap().to_string_lossy().to_string()); + CUSTOM_ENV.set(custom_env).expect("Custom env already set? How is this possible!"); + return (steam_id, steam_id_3) +} + +pub fn print_debug() { + for (k, v) in CUSTOM_ENV.get().unwrap() { + debug!("{} = {}", k, v); + } +} + +fn get_regkey_value(key_path: &str, key_name: &str) -> Result +where + T: FromRegValue, +{ + let predef_index = key_path.find('\\').expect("Wrong Path given for registry"); + let (predef_path, subkey_path) = key_path.split_at(predef_index); + let subkey_path = &subkey_path[1..]; + let hkcu = RegKey::predef(predef_from_str(predef_path)?); + let key = hkcu.open_subkey(subkey_path)?; + return key.get_value::(key_name) +} + +// Expand %% windows vars with vars from the hashmap is exists otherwise from environment +fn expand_windows_env_vars(input: &str, overrides: Option<&HashMap>) -> String { + let re = Regex::new(r"%([^%]+)%").unwrap(); + re.replace_all(input, |caps: ®ex::Captures| { + let key = &caps[1]; + overrides + .and_then(|map| map.get(key).cloned()) + .or_else(|| std::env::var(key).ok()) + .unwrap_or_default() + }).to_string() +} + + +fn predef_from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "HKEY_CLASSES_ROOT" => Ok(HKEY_CLASSES_ROOT), + "HKEY_CURRENT_USER" => Ok(HKEY_CURRENT_USER), + "HKEY_LOCAL_MACHINE" => Ok(HKEY_LOCAL_MACHINE), + "HKEY_USERS" => Ok(HKEY_USERS), + "HKEY_CURRENT_CONFIG" => Ok(HKEY_CURRENT_CONFIG), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Unknown registry root", + )), + } +} + +pub fn process_configs( + to_game: bool, + matching_lines: &Vec, + _steam_id: &str, + _steam_id_3: &str, + steam_app_id: &str, + good_config_paths: &Path + //proton_prefix: Option<&Path>, + //custom_envs: &HashMap, + //win_user_path: &str, +) { + for raw_config in matching_lines { + + let (config_path, config) = shared::process_raw_config_line(raw_config); + + let game_config_path: PathBuf; + + let config_path = expand_windows_env_vars(config_path, Some(CUSTOM_ENV.get().unwrap())); + game_config_path = PathBuf::from(config_path); + + if to_game { + shared::copy_configs( + &good_config_paths.join(steam_app_id).join(config), + &game_config_path.join(config) + ); + } else { + shared::copy_configs( + &game_config_path.join(config), + &good_config_paths.join(steam_app_id).join(config) + ); + } + } +} diff --git a/src/shared.rs b/src/shared.rs new file mode 100644 index 0000000..36fee66 --- /dev/null +++ b/src/shared.rs @@ -0,0 +1,39 @@ +use core::panic; +use std::fs; +use std::path::{Path, PathBuf}; +use regex::Regex; +use log::{error, info}; + + +// Contains code used by both platform modules + +pub const STEAM_ID_OFFSET: u64 = 76561197960265728; + +pub fn get_steamid_from_config(config_path: PathBuf, steam_user: &str) -> String{ + //STEAMID=$(grep -Pzo '"'${SteamUser}'"\s+{\s+"SteamID"\s+"[0-9]+"' /home/${USER}/.local/share/Steam/config/config.vdf | grep --text -oP '(?<=\s")[0-9]+') + let contents = fs::read_to_string(&config_path).expect("Failed to read config.vdf"); + let block_re = Regex::new(&format!(r#""{}"\s*\{{[^{{}}]*?"SteamID"\s+"([0-9]+)""#,regex::escape(&steam_user))).expect("Regex invalid"); + if let Some(caps) = block_re.captures(&contents) { + let steam_id: String = caps[1].to_string(); + return steam_id; + } else { + panic!("No steamid found in config.vdf. How is this possible?") + } +} + +pub fn copy_configs(from: &Path, to: &Path){ + if let Err(e) = fs::copy(&from, &to){ + error!("Failed to Copy {} to {}", from.to_string_lossy(), to.to_string_lossy()); + error!("Error: {}", e); + } else { + info!("Copied {} to {}", from.to_string_lossy(), to.to_string_lossy()); + } +} + +pub fn process_raw_config_line(raw_config: &String) -> (&str, &str) { + let mut split = raw_config.split(';'); + split.next(); // Skip app id + let config_path: &str = split.next().unwrap(); + let config: &str = split.next().unwrap(); + return (config_path, config) +} \ No newline at end of file