use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static DATA_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
pub fn set_data_dir(path: PathBuf) {
DATA_DIR_OVERRIDE
.set(path)
.expect("data directory already set");
}
pub fn data_dir() -> PathBuf {
#[cfg(target_os = "linux")]
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
return PathBuf::from(xdg);
}
dirs::data_dir().unwrap_or_else(|| home_dir().join(".local/share"))
}
pub fn config_dir() -> PathBuf {
#[cfg(target_os = "linux")]
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
return PathBuf::from(xdg);
}
dirs::config_dir().unwrap_or_else(|| home_dir().join(".config"))
}
pub fn modde_data_dir() -> PathBuf {
if let Some(dir) = DATA_DIR_OVERRIDE.get() {
return dir.clone();
}
data_dir().join("modde")
}
pub fn store_dir() -> PathBuf {
modde_data_dir().join("store")
}
pub fn staging_dir() -> PathBuf {
modde_data_dir().join("staging")
}
pub fn profiles_dir() -> PathBuf {
modde_data_dir().join("profiles")
}
pub fn downloads_dir() -> PathBuf {
modde_data_dir().join("downloads")
}
pub fn stock_dir() -> PathBuf {
modde_data_dir().join("stock")
}
pub fn save_vaults_dir() -> PathBuf {
modde_data_dir().join("saves")
}
pub fn wabbajack_cache_dir() -> PathBuf {
modde_data_dir().join("wabbajack_cache")
}
pub fn wabbajack_cache_path(manifest_hash: &str) -> PathBuf {
wabbajack_cache_dir().join(format!("{manifest_hash}.wabbajack"))
}
pub fn save_vault_dir(game_id: &str) -> PathBuf {
save_vaults_dir().join(game_id)
}
fn steam_install_dir() -> PathBuf {
#[cfg(target_os = "linux")]
{
home_dir().join(".local/share/Steam")
}
#[cfg(target_os = "macos")]
{
home_dir().join("Library/Application Support/Steam")
}
#[cfg(target_os = "windows")]
{
steam_install_dir_windows()
}
}
#[cfg(target_os = "windows")]
fn steam_install_dir_windows() -> PathBuf {
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
if let Ok(key) = hkcu.open_subkey(r"Software\Valve\Steam") {
if let Ok(path) = key.get_value::<String, _>("SteamPath") {
return PathBuf::from(path);
}
}
PathBuf::from(r"C:\Program Files (x86)\Steam")
}
pub fn steam_common() -> PathBuf {
steam_install_dir().join("steamapps/common")
}
pub fn steam_library_folders() -> Vec<PathBuf> {
let vdf_path = steam_install_dir().join("steamapps/libraryfolders.vdf");
parse_library_folders_vdf(&vdf_path)
}
fn parse_library_folders_vdf(path: &Path) -> Vec<PathBuf> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return vec![],
};
let mut paths = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("\"path\"") {
let rest = rest.trim();
if let Some(val) = extract_vdf_string(rest) {
let lib_path = PathBuf::from(val);
if lib_path.exists() {
paths.push(lib_path);
}
}
}
}
let default_lib = steam_install_dir();
if !paths.iter().any(|p| p == &default_lib) && default_lib.exists() {
paths.insert(0, default_lib);
}
paths
}
fn extract_vdf_string(s: &str) -> Option<&str> {
let s = s.trim();
let s = s.strip_prefix('"')?;
let end = s.find('"')?;
Some(&s[..end])
}
pub fn heroic_config_dir() -> Option<PathBuf> {
let dir = config_dir().join("heroic");
dir.is_dir().then_some(dir)
}
#[cfg(target_os = "windows")]
pub fn heroic_exe_path() -> Option<PathBuf> {
let local_app = dirs::data_local_dir()?;
let exe = local_app.join(r"Programs\heroic\Heroic.exe");
exe.exists().then_some(exe)
}
pub fn db_path() -> PathBuf {
modde_data_dir().join("modde.db")
}
pub fn modde_config_dir() -> PathBuf {
config_dir().join("modde")
}
pub fn home_dir() -> PathBuf {
dirs::home_dir().unwrap_or_else(|| {
#[cfg(unix)]
{
PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()))
}
#[cfg(windows)]
{
PathBuf::from(
std::env::var("USERPROFILE").unwrap_or_else(|_| r"C:\Temp".to_string()),
)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
fn write_vdf(dir: &std::path::Path, content: &str) -> PathBuf {
let path = dir.join("libraryfolders.vdf");
std::fs::write(&path, content).unwrap();
path
}
#[test]
fn vdf_single_library() {
let tmp = tempfile::tempdir().unwrap();
let lib = tmp.path().join("steam");
std::fs::create_dir_all(&lib).unwrap();
let vdf = format!(
r#""libraryfolders"
{{
"0"
{{
"path" "{}"
}}
}}"#,
lib.display()
);
let vdf_path = write_vdf(tmp.path(), &vdf);
let paths = parse_library_folders_vdf(&vdf_path);
assert!(paths.contains(&lib), "expected {:?} in {:?}", lib, paths);
}
#[test]
fn vdf_multiple_libraries() {
let tmp = tempfile::tempdir().unwrap();
let lib1 = tmp.path().join("lib1");
let lib2 = tmp.path().join("lib2");
std::fs::create_dir_all(&lib1).unwrap();
std::fs::create_dir_all(&lib2).unwrap();
let vdf = format!(
"\"libraryfolders\"\n{{\n\t\"0\"\n\t{{\n\t\t\"path\"\t\t\"{}\"\n\t}}\n\t\"1\"\n\t{{\n\t\t\"path\"\t\t\"{}\"\n\t}}\n}}",
lib1.display(),
lib2.display()
);
let vdf_path = write_vdf(tmp.path(), &vdf);
let paths = parse_library_folders_vdf(&vdf_path);
assert!(paths.contains(&lib1), "expected {:?} in {:?}", lib1, paths);
assert!(paths.contains(&lib2), "expected {:?} in {:?}", lib2, paths);
}
#[test]
fn vdf_nonexistent_paths_excluded() {
let tmp = tempfile::tempdir().unwrap();
let vdf = r#""libraryfolders"
{
"0" { "path" "/nonexistent/path/to/steam" }
}"#;
let vdf_path = write_vdf(tmp.path(), vdf);
let paths = parse_library_folders_vdf(&vdf_path);
assert!(!paths.iter().any(|p| p.to_string_lossy().contains("nonexistent")));
}
#[test]
fn vdf_missing_file_returns_empty() {
let paths = parse_library_folders_vdf(std::path::Path::new("/nonexistent/libraryfolders.vdf"));
assert!(paths.is_empty());
}
#[test]
fn vdf_extra_fields_ignored() {
let tmp = tempfile::tempdir().unwrap();
let lib = tmp.path().join("steam");
std::fs::create_dir_all(&lib).unwrap();
let vdf = format!(
r#""libraryfolders"
{{
"0"
{{
"path" "{}"
"label" ""
"contentid" "12345"
"totalsize" "0"
"apps"
{{
"730" "12345"
}}
}}
}}"#,
lib.display()
);
let vdf_path = write_vdf(tmp.path(), &vdf);
let paths = parse_library_folders_vdf(&vdf_path);
assert!(paths.contains(&lib));
for p in &paths {
assert!(p.exists(), "all returned paths must exist");
}
}
#[test]
fn extract_vdf_string_basic() {
assert_eq!(extract_vdf_string(r#""hello""#), Some("hello"));
assert_eq!(extract_vdf_string(r#" "with spaces" "#), Some("with spaces"));
assert_eq!(extract_vdf_string(r#""/path/to/dir""#), Some("/path/to/dir"));
}
#[test]
fn extract_vdf_string_no_quotes() {
assert_eq!(extract_vdf_string("no quotes here"), None);
}
#[test]
fn extract_vdf_string_unclosed_quote() {
assert_eq!(extract_vdf_string(r#""unclosed"#), None);
}
}