Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 49 additions & 215 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,129 +1,57 @@
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);

//no arguments passed?
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<PathBuf> = None;

//Windows only
let mut custom_env: HashMap<String, String> = 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::<u64>().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::<u64>().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");

Expand All @@ -149,139 +77,45 @@ 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)
.args(args) // Remaining launch arguments
.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<String, String>,
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, String>>) -> String {
let re = Regex::new(r"%([^%]+)%").unwrap();
re.replace_all(input, |caps: &regex::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");
}
67 changes: 67 additions & 0 deletions src/platform/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use winreg::{RegKey, types::FromRegValue, HKEY};

Check failure on line 1 in src/platform/linux.rs

View workflow job for this annotation

GitHub Actions / Build for Linux

unresolved import `winreg`

Check failure on line 1 in src/platform/linux.rs

View workflow job for this annotation

GitHub Actions / Build for Linux

failed to resolve: use of unresolved module or unlinked crate `winreg`
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use regex::Regex;

Check failure on line 4 in src/platform/linux.rs

View workflow job for this annotation

GitHub Actions / Build for Linux

unused import: `regex::Regex`
use std::collections::HashMap;

Check failure on line 5 in src/platform/linux.rs

View workflow job for this annotation

GitHub Actions / Build for Linux

unused import: `std::collections::HashMap`
use std::env;
use log::{warn, debug, error, info, LevelFilter};

Check failure on line 7 in src/platform/linux.rs

View workflow job for this annotation

GitHub Actions / Build for Linux

unused imports: `LevelFilter`, `error`, `info`, and `warn`
use crate::shared;

//Linux only
const WINE_USER_PATH: &str = "pfx/drive_c/users/steamuser";
static PROTON_PREFIX: OnceLock<PathBuf> = 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::<u64>().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<String>,
_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)
);
}
}
}
5 changes: 5 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(target_os = "windows")]
pub mod windows;

#[cfg(target_os = "linux")]
pub mod linux;
Loading
Loading