From fafd226000ce31337bef0570cafcc8c141a64aee Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:19:03 -0600 Subject: [PATCH 01/17] File Exclusion --- maxima-lib/Cargo.toml | 1 + maxima-lib/src/content/exclusion.rs | 46 +++++++++++++++++++++++++++++ maxima-lib/src/content/manager.rs | 13 +++++++- maxima-lib/src/content/mod.rs | 1 + 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 maxima-lib/src/content/exclusion.rs diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 9f3a6e7..07a30d7 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -76,6 +76,7 @@ gethostname = "0.5.0" thiserror = "2.0.12" url = "2.5.2" http = "0.2.12" +globset = "0.4" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = [ diff --git a/maxima-lib/src/content/exclusion.rs b/maxima-lib/src/content/exclusion.rs new file mode 100644 index 0000000..c685869 --- /dev/null +++ b/maxima-lib/src/content/exclusion.rs @@ -0,0 +1,46 @@ +use std::fs::File; +use std::io::{BufRead, BufReader}; +use crate::util::native::maxima_dir; +use globset::{GlobSet, GlobSetBuilder, Glob}; +use log::{info, warn, error}; + +pub fn get_exclusion_list(offer_id: String) -> GlobSet +{ + let mut builder = GlobSetBuilder::new(); + + if let Ok(dir) = maxima_dir() // Checks to make sure maxima directory exists + { + let filepath = dir.join("exclude").join(&offer_id); // Path to exclusion file + info!("Loading exclusion file from {}", filepath.display()); + + if let Ok(file) = File::open(&filepath) // Opens the exclusion file, fails if not found + { + let reader = BufReader::new(file); + for line in reader.lines().flatten() + { + let entry = line.trim(); + if !entry.is_empty() && !entry.starts_with('#') + { + match Glob::new(entry) // Create a glob from the entry, checks if valid pattern, if not logs a warning, + { + Ok(glob) => + { + builder.add(glob); + } + Err(_) => warn!("Invalid glob pattern '{}' in {}", entry, filepath.display()), + } + } + } + } + else + { + warn!("Exclusion file not found: {}", filepath.display()); + } + } + else + { + error!("Failed to resolve maxima data directory"); + } + + builder.build().unwrap_or_else(|_| GlobSetBuilder::new().build().unwrap()) // Returns an empty GlobSet on failure +} \ No newline at end of file diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f16116d..ead9d03 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -15,10 +15,12 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; +use globset::GlobSet; use crate::{ content::{ downloader::{DownloadError, ZipDownloader}, + exclusion::get_exclusion_list, zip::{self, CompressionType, ZipError, ZipFileEntry}, ContentService, }, @@ -149,8 +151,17 @@ impl GameDownloader { let downloader = ZipDownloader::new(&game.offer_id, &url.url(), &game.path).await?; let mut entries = Vec::new(); - for ele in downloader.manifest().entries() { + + let exclusion_list = get_exclusion_list(game.offer_id.clone()); + + for ele in downloader.manifest().entries() + { // TODO: Filtering + if exclusion_list.is_match(&ele.name()) + { + info!("Excluding file from download: {}", ele.name()); + continue; + } entries.push(ele.clone()); } diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 4a98e43..d649f8f 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -15,6 +15,7 @@ pub mod downloader; pub mod manager; pub mod zip; pub mod zlib; +pub mod exclusion; pub struct ContentService { service_layer: ServiceLayerClient, From c9b5b5bf6936b66108ed1dc362abc0e185edfef5 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:17:01 -0600 Subject: [PATCH 02/17] Ability to use SLR and custom proton (could be very much refined) --- maxima-lib/src/core/launch.rs | 10 ++- maxima-lib/src/unix/wine.rs | 143 ++++++++++++++++++++++++++++++++-- maxima-lib/src/util/native.rs | 6 ++ 3 files changed, 151 insertions(+), 8 deletions(-) diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 6281672..87579f1 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -402,9 +402,10 @@ async fn request_opaque_ooa_token(access_token: &str) -> Result Result<(), NativeError> { use crate::unix::wine::{ check_runtime_validity, check_wine_validity, get_lutris_runtimes, install_runtime, - install_wine, setup_wine_registry, + install_wine, setup_wine_registry, run_wine_command, wine_prefix_dir, CommandType, }; + std::fs::create_dir_all(wine_prefix_dir()?)?; info!("Verifying wine dependencies..."); let skip = std::env::var("MAXIMA_DISABLE_WINE_VERIFICATION").is_ok(); @@ -413,14 +414,17 @@ pub async fn mx_linux_setup() -> Result<(), NativeError> { install_wine().await?; } let runtimes = get_lutris_runtimes().await?; - if !check_runtime_validity("eac_runtime", &runtimes).await? { + if !check_runtime_validity("eac_runtime", &runtimes).await? && !std::env::var("MAXIMA_DISABLE_EAC").is_ok() { install_runtime("eac_runtime", &runtimes).await?; } - if !check_runtime_validity("umu", &runtimes).await? { + let use_slr = std::env::var("MAXIMA_USE_SLR").is_ok(); + if !check_runtime_validity("umu", &runtimes).await? && !use_slr { install_runtime("umu", &runtimes).await?; } } + let _ = run_wine_command("wineboot", Some(vec!["--init"]), None, false, CommandType::Run).await; + info!("Setting up wine registry..."); setup_wine_registry().await?; Ok(()) diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 50c61a6..96c4b50 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -23,7 +23,7 @@ use tokio::{ use xz2::read::XzDecoder; use crate::util::{ - github::{fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease}, + github::{fetch_github_releases, github_download_asset, GithubRelease}, native::{maxima_dir, DownloadError, NativeError, SafeParent, SafeStr, WineError}, registry::RegistryError, }; @@ -72,10 +72,18 @@ struct Versions { /// Returns internal prtoton pfx path pub fn wine_prefix_dir() -> Result { + if let Ok(path) = env::var("MAXIMA_WINE_PREFIX") { + return Ok(PathBuf::from(path)); + } + Ok(maxima_dir()?.join("wine/prefix")) } pub fn proton_dir() -> Result { + if let Ok(path) = env::var("MAXIMA_PROTON_PATH") { + return Ok(PathBuf::from(path)); + } + Ok(maxima_dir()?.join("wine/proton")) } @@ -108,6 +116,12 @@ fn set_versions(versions: Versions) -> Result<(), NativeError> { } pub(crate) async fn check_wine_validity() -> Result { + // Skip check if using custom Proton path + if env::var("MAXIMA_PROTON_PATH").is_ok() { + info!("Using custom Proton path, skipping validity check"); + return Ok(true); + } + if !proton_dir()?.exists() { return Ok(false); } @@ -234,7 +248,8 @@ fn get_wine_release() -> Result { release.ok_or(WineError::Fetch) } -pub async fn run_wine_command, T: AsRef>( +/// Run a wine command using UMU launcher +async fn run_wine_command_umu, T: AsRef>( arg: T, args: Option, cwd: Option, @@ -252,10 +267,10 @@ pub async fn run_wine_command, T: AsRef>( // Create command with all necessary wine env variables let mut binding = Command::new(wine_path.clone()); let mut child = binding - .env("WINEPREFIX", proton_prefix_path) + .env("WINEPREFIX", &proton_prefix_path) .env("GAMEID", "umu-0") .env("PROTON_VERB", &command_type.to_string()) - .env("PROTONPATH", proton_path) + .env("PROTONPATH", &proton_path) .env("STORE", "ea") .env("PROTON_EAC_RUNTIME", eac_path) .env("UMU_ZENITY", "1") @@ -304,7 +319,125 @@ pub async fn run_wine_command, T: AsRef>( Ok(output_str.to_string()) } +/// Run a wine command using Steam Linux Runtime +async fn run_wine_command_slr, T: AsRef>( + arg: T, + args: Option, + cwd: Option, + want_output: bool, + command_type: CommandType, +) -> Result { + let slr_path = env::var("MAXIMA_SLR_PATH") + .map_err(|_| NativeError::Wine(WineError::MissingSLRPath))?; + let proton_dir_path = env::var("MAXIMA_PROTON_PATH") + .map_err(|_| NativeError::Wine(WineError::MissingProtonPath))?; + let proton_exe = PathBuf::from(&proton_dir_path) + .join("proton") + .to_string_lossy() + .to_string(); + let proton_prefix_path = wine_prefix_dir()?; + + // Get the Steam client install path, defaulting to common location + let steam_client_path = env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH") + .unwrap_or_else(|_| { + env::var("HOME") + .map(|h| format!("{}/.steam/steam", h)) + .unwrap_or_else(|_| "/home/user/.steam/steam".to_string()) + }); + + // Build the SLR entry point path + let slr_entry_point = PathBuf::from(&slr_path).join("_v2-entry-point"); + + if !slr_entry_point.exists() { + return Err(NativeError::Wine(WineError::SLRNotFound(slr_entry_point))); + } + + // Build proton command with verb passed to _v2-entry-point + let mut proton_args = vec![proton_exe.clone(), "run".to_string()]; + proton_args.push(arg.as_ref().to_string_lossy().to_string()); + + if let Some(arguments) = args { + for a in arguments { + proton_args.push(a.as_ref().to_string_lossy().to_string()); + } + } + + let slr_verb = format!("--verb={}", command_type.to_string()); + + let mut binding = Command::new(slr_entry_point); + let mut child = binding + .env("WINEPREFIX", &proton_prefix_path) + .env("STEAM_COMPAT_DATA_PATH", &proton_prefix_path) + .env("STEAM_COMPAT_CLIENT_INSTALL_PATH", &steam_client_path) + .env("SteamAppId", "0") + .env("STEAM_COMPAT_APP_ID", "0") + .env("SteamGameId", "0") + .env("WINEDEBUG", "fixme-all") + .env("LD_PRELOAD", "") + .arg(&slr_verb) + .arg("--") + .args(proton_args); + + // Hardcode compat install path until dynamic wiring is added; still honor cwd for working dir + child = child.env( + "STEAM_COMPAT_INSTALL_PATH", + "/mnt/games/Games/mass-effect-legendary-edition", + ); + + if let Some(ref dir) = cwd { + child = child.current_dir(dir); + } + + let status: ExitStatus; + let mut output_str = String::new(); + + if want_output { + let output = child + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output() + .await?; + output_str = String::from_utf8_lossy(&output.stdout).to_string(); + status = output.status; + } else { + status = child.spawn()?.wait().await?; + }; + + if !status.success() { + return Err(NativeError::Wine(WineError::Command { + output: output_str, + exit: status, + })); + } + + Ok(output_str.to_string()) +} + +pub async fn run_wine_command, T: AsRef>( + arg: T, + args: Option, + cwd: Option, + want_output: bool, + command_type: CommandType, +) -> Result { + // Check if using Steam Linux Runtime + let use_slr = env::var("MAXIMA_USE_SLR").is_ok(); + + if use_slr { + run_wine_command_slr(arg, args, cwd, want_output, command_type).await + } else { + run_wine_command_umu(arg, args, cwd, want_output, command_type).await + } +} + pub(crate) async fn install_wine() -> Result<(), NativeError> { + // Skip installation if using custom Proton path + if env::var("MAXIMA_PROTON_PATH").is_ok() { + info!("Using custom Proton path, skipping Proton-GE installation"); + let _ = run_wine_command("", None::<[&str; 0]>, None, false, CommandType::Run).await; + return Ok(()); + } + let release = get_wine_release()?; let asset = match release .assets @@ -476,7 +609,7 @@ async fn parse_wine_registry(file_path: &str) -> WineRegistry { } pub async fn parse_mx_wine_registry() -> Result { - let path = wine_prefix_dir()?.join("system.reg"); + let path = wine_prefix_dir()?.join("pfx").join("system.reg"); if !path.exists() { return Ok(HashMap::new()); } diff --git a/maxima-lib/src/util/native.rs b/maxima-lib/src/util/native.rs index 5adfc60..f8d8f35 100644 --- a/maxima-lib/src/util/native.rs +++ b/maxima-lib/src/util/native.rs @@ -58,6 +58,12 @@ pub enum WineError { UnimplementedRuntime(String), #[error("couldn't find suitable wine release")] Fetch, + #[error("MAXIMA_SLR_PATH environment variable must be set when using SLR")] + MissingSLRPath, + #[error("MAXIMA_PROTON_PATH environment variable must be set when using SLR")] + MissingProtonPath, + #[error("Steam Linux Runtime entry point not found at: {0}")] + SLRNotFound(PathBuf), } pub trait SafeParent { fn safe_parent(&self) -> Result<&Path, NativeError>; From 2c4612d65dbc77ee97d7d2a3c629adf41e6c5389 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:42:35 -0600 Subject: [PATCH 03/17] Partial work for settings implementation --- maxima-cli/src/main.rs | 9 ++-- maxima-lib/src/core/launch.rs | 4 +- maxima-lib/src/core/library.rs | 8 +-- maxima-lib/src/core/mod.rs | 14 +++++- maxima-lib/src/gamesettings/mod.rs | 81 ++++++++++++++++++++++++++++++ maxima-lib/src/lib.rs | 1 + maxima-ui/src/bridge/get_games.rs | 5 +- 7 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 maxima-lib/src/gamesettings/mod.rs diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index de2ce96..e3afc39 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -359,8 +359,9 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; let mut owned_games = Vec::new(); + let game_settings = maxima.mut_game_settings().clone(); for game in maxima.mut_library().games().await? { - if !game.base_offer().is_installed().await { + if !game.base_offer().is_installed(&game_settings).await { continue; } @@ -387,8 +388,9 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let offer_id = { let mut owned_games = Vec::new(); + let game_settings = maxima.mut_game_settings().clone(); for game in maxima.mut_library().games().await? { - if game.base_offer().is_installed().await { + if game.base_offer().is_installed(&game_settings).await { continue; } @@ -740,6 +742,7 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; info!("Owned games:"); + let game_settings = maxima.game_settings().clone(); let titles = maxima.mut_library().games().await?; for title in titles { @@ -748,7 +751,7 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { title.base_offer().slug(), title.name(), title.base_offer().offer_id(), - title.base_offer().is_installed().await, + title.base_offer().is_installed(&game_settings).await, width = 35, width2 = 35, width3 = 25, diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 87579f1..18b9b5a 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -188,6 +188,8 @@ pub async fn start_game( } } + let game_settings = maxima.mut_game_settings().clone(); + let (content_id, online_offline, offer, access_token) = if let LaunchMode::Online(ref offer_id) = mode { let access_token = &maxima.access_token().await?; @@ -196,7 +198,7 @@ pub async fn start_game( None => return Err(LaunchError::NoOfferFound(offer_id.clone())), }; - if !offer.is_installed().await { + if !offer.is_installed(&game_settings).await { return Err(LaunchError::NotInstalled(offer.offer_id().clone())); } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index faa730c..41bfc99 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -13,7 +13,7 @@ use super::{ }; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; -use crate::util::native::{NativeError, SafeStr}; +use crate::{core::settings, gamesettings::GameSettingsManager, util::native::{NativeError, SafeStr}}; use crate::util::registry::{parse_partial_registry_path, parse_registry_path, RegistryError}; use derive_getters::Getters; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; @@ -54,7 +54,7 @@ pub struct OwnedOffer { } impl OwnedOffer { - pub async fn is_installed(&self) -> bool { + pub async fn is_installed(&self, settings: &GameSettingsManager) -> bool { // I would love to throw an error here but that's just not feasible. // If you can't grab the path it may as well not be installed. let Some(path) = &self.offer.install_check_override().as_ref() else { @@ -105,8 +105,8 @@ impl OwnedOffer { } } - pub async fn installed_version(&self) -> Result { - if !self.is_installed().await { + pub async fn installed_version(&self, settings: &GameSettingsManager) -> Result { + if !self.is_installed(settings).await { return Err(LibraryError::NotInstalled(self.slug.clone())); } diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 88ff044..781ebe6 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -46,6 +46,7 @@ use std::sync::Arc; use thiserror::Error; use tokio::sync::Mutex; + use self::{ auth::storage::{AuthError, AuthStorage, LockedAuthStorage, TokenError}, cache::DynamicCache, @@ -67,6 +68,7 @@ use crate::{ lsx::{self, service::LSXServerError, types::LSXRequestType}, rtm::client::{BasicPresence, RtmClient}, util::native::{maxima_dir, NativeError}, + gamesettings::{GameSettingsManager}, }; #[derive(Clone, IntoStaticStr)] @@ -88,7 +90,8 @@ pub struct Maxima { #[getter(skip)] library: GameLibrary, - + #[getter(skip)] + game_settings: GameSettingsManager, playing: Option, lsx_port: u16, @@ -216,6 +219,7 @@ impl Maxima { request_cache, dummy_local_user, pending_events: Vec::new(), + game_settings: GameSettingsManager::new(), }))) } @@ -415,6 +419,14 @@ impl Maxima { &mut self.library } + pub fn game_settings(&self) -> &GameSettingsManager { + &self.game_settings + } + + pub fn mut_game_settings(&mut self) -> &mut GameSettingsManager { + &mut self.game_settings + } + pub fn content_manager(&mut self) -> &mut ContentManager { &mut self.content_manager } diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs new file mode 100644 index 0000000..e576524 --- /dev/null +++ b/maxima-lib/src/gamesettings/mod.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json; +use crate::util::native::maxima_dir; + +#[derive(Serialize, Deserialize, Clone)] +pub struct GameSettings { + cloud_saves: bool, + launch_args: String, + exe_override: String, + wine_prefix: String, +} + +impl GameSettings +{ + pub fn new() -> Self { + Self { + cloud_saves: true, + launch_args: String::new(), + exe_override: String::new(), + wine_prefix: String::new(), + } + } + + pub fn new_with_slug(slug: &str) -> Self { + let mut settings = Self::new(); + settings.wine_prefix = format!("/mnt/games/Games/{}/", slug); + settings + } +} + +pub fn get_game_settings(slug: &str) -> GameSettings +{ + let path = match maxima_dir() { + Ok(dir) => dir.join("settings").join(format!("{}.json", slug)), + Err(_) => return GameSettings::new_with_slug(slug), + }; + + let content = match std::fs::read_to_string(path) { + Ok(content) => content, + Err(_) => return GameSettings::new_with_slug(slug), + }; + + serde_json::from_str(&content).unwrap_or_else(|_| GameSettings::new_with_slug(slug)) +} + +pub fn save_game_settings(slug: &str, settings: &GameSettings) +{ + if let Ok(dir) = maxima_dir() + { + let path = dir.join("settings").join(format!("{}.json", slug)); + if let Ok(content) = serde_json::to_string_pretty(settings) + { + let _ = std::fs::write(path, content); + } + } +} + +#[derive(Clone)] +pub struct GameSettingsManager { + settings: HashMap, +} + +impl GameSettingsManager { + pub fn new() -> Self { + Self { + settings: HashMap::new(), + } + } + + pub fn get(&mut self, slug: &str) -> &GameSettings { + self.settings.entry(slug.to_string()) + .or_insert_with(|| get_game_settings(slug)) + } + + pub fn save(&mut self, slug: &str, settings: GameSettings) { + save_game_settings(slug, &settings); + self.settings.insert(slug.to_string(), settings); + } +} \ No newline at end of file diff --git a/maxima-lib/src/lib.rs b/maxima-lib/src/lib.rs index 0d878db..f444182 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -10,6 +10,7 @@ pub mod lsx; pub mod ooa; pub mod rtm; pub mod util; +pub mod gamesettings; #[cfg(unix)] pub mod unix; diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index f2a62ca..876ef8e 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -195,6 +195,7 @@ pub async fn get_games_request( let service_layer = maxima.service_layer().clone(); let locale = maxima.locale().short_str().to_owned(); let logged_in = maxima.auth_storage().lock().await.current().is_some(); + let game_settings = maxima.game_settings().clone(); if !logged_in { return Err(BackendError::LoggedOut); } @@ -212,7 +213,7 @@ pub async fn get_games_request( downloads.iter().find(|item| item.download_type() == "LIVE").unwrap() }; - let version = if let Ok(version) = game.base_offer().installed_version().await { + let version = if let Ok(version) = game.base_offer().installed_version(&game_settings).await { version } else { "Unknown".to_owned() @@ -229,7 +230,7 @@ pub async fn get_games_request( mandatory: opt.treat_updates_as_mandatory().clone(), }, dlc: game.extra_offers().clone(), - installed: game.base_offer().is_installed().await, + installed: game.base_offer().is_installed(&game_settings).await, has_cloud_saves: game.base_offer().offer().has_cloud_save(), }; let slug = game_info.slug.clone(); From c607d42d14da4b2fa791bb5555ea3d958c3e8788 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:16:39 -0600 Subject: [PATCH 04/17] Update UI code to account for GameSettingsManager --- maxima-lib/src/gamesettings/mod.rs | 46 ++++++++++++++++++++++++++++-- maxima-ui/src/bridge/get_games.rs | 15 +++++----- maxima-ui/src/bridge_processor.rs | 7 ++++- maxima-ui/src/bridge_thread.rs | 14 ++++++++- maxima-ui/src/main.rs | 33 ++++++--------------- maxima-ui/src/views/game_view.rs | 12 +++++--- 6 files changed, 88 insertions(+), 39 deletions(-) diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index e576524..b86241f 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use log::info; use serde::{Deserialize, Serialize}; use serde_json; use crate::util::native::maxima_dir; @@ -28,6 +29,30 @@ impl GameSettings settings.wine_prefix = format!("/mnt/games/Games/{}/", slug); settings } + + /// Public accessors for fields so consumers can read settings. + pub fn cloud_saves(&self) -> bool { + self.cloud_saves + } + + pub fn launch_args(&self) -> &str { + &self.launch_args + } + + pub fn exe_override(&self) -> &str { + &self.exe_override + } + + pub fn wine_prefix(&self) -> &str { + &self.wine_prefix + } + + /// Update mutable fields from UI-provided values while preserving any internal-only fields like `wine_prefix`. + pub fn update_from(&mut self, cloud_saves: bool, launch_args: String, exe_override: String) { + self.cloud_saves = cloud_saves; + self.launch_args = launch_args; + self.exe_override = exe_override; + } } pub fn get_game_settings(slug: &str) -> GameSettings @@ -47,14 +72,31 @@ pub fn get_game_settings(slug: &str) -> GameSettings pub fn save_game_settings(slug: &str, settings: &GameSettings) { + info!("Saving settings for {}...", slug); if let Ok(dir) = maxima_dir() { - let path = dir.join("settings").join(format!("{}.json", slug)); + let settings_dir = dir.join("settings"); + // Ensure the settings directory exists + if let Err(err) = std::fs::create_dir_all(&settings_dir) { + info!("Failed to create settings dir {:?}: {}", settings_dir, err); + return; + } + + let path = settings_dir.join(format!("{}.json", slug)); if let Ok(content) = serde_json::to_string_pretty(settings) { - let _ = std::fs::write(path, content); + match std::fs::write(&path, content) { + Ok(()) => info!("Saved settings to {:?}", path), + Err(err) => info!("Failed to write settings for {}: {}", slug, err), + } + } + else { + info!("Failed to serialize settings for {}", slug); } } + else { + info!("Failed to get maxima directory, cannot save settings for {}", slug); + } } #[derive(Clone)] diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 876ef8e..66e10d8 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -195,7 +195,7 @@ pub async fn get_games_request( let service_layer = maxima.service_layer().clone(); let locale = maxima.locale().short_str().to_owned(); let logged_in = maxima.auth_storage().lock().await.current().is_some(); - let game_settings = maxima.game_settings().clone(); + let mut game_settings = maxima.game_settings().clone(); if !logged_in { return Err(BackendError::LoggedOut); } @@ -213,7 +213,7 @@ pub async fn get_games_request( downloads.iter().find(|item| item.download_type() == "LIVE").unwrap() }; - let version = if let Ok(version) = game.base_offer().installed_version(&game_settings).await { + let version = if let Ok(version) = game.base_offer().installed_version(&mut game_settings).await { version } else { "Unknown".to_owned() @@ -230,15 +230,16 @@ pub async fn get_games_request( mandatory: opt.treat_updates_as_mandatory().clone(), }, dlc: game.extra_offers().clone(), - installed: game.base_offer().is_installed(&game_settings).await, + installed: game.base_offer().is_installed(&mut game_settings).await, has_cloud_saves: game.base_offer().offer().has_cloud_save(), }; let slug = game_info.slug.clone(); + // Grab persisted settings from Maxima's GameSettingsManager if available + let core_settings = game_settings.get(&slug); let settings = crate::GameSettings { - //TODO: eventually support cloud saves, the option is here for that but for now, keep it disabled in ui! - cloud_saves: true, - launch_args: String::new(), - exe_override: String::new(), + cloud_saves: core_settings.cloud_saves(), + launch_args: core_settings.launch_args().to_string(), + exe_override: core_settings.exe_override().to_string(), }; let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info, diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 9a98c03..5ae00e0 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -44,7 +44,12 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } ServiceStarted => app.backend_state = BackendStallState::Starting, GameInfoResponse(res) => { - app.games.insert(res.game.slug.clone(), res.game); + // Move the game out of the response, keep the slug for storing settings + let game = res.game; + let slug = game.slug.clone(); + // Store the game-specific settings coming from the backend into frontend settings + app.settings.game_settings.insert(slug.clone(), res.settings); + app.games.insert(slug, game); } GameDetailsResponse(res) => { let response = res.response; diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 456c177..46b8e8e 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -84,8 +84,10 @@ pub enum MaximaLibRequest { StartGameRequest(GameInfo, Option), InstallGameRequest(String, PathBuf), LocateGameRequest(String), + /// Persist UI-side game settings into the core GameSettingsManager + SaveGameSettings(String, GameSettings), ShutdownRequest, -} +} pub enum MaximaLibResponse { LoginResponse(Result), @@ -508,6 +510,16 @@ impl BridgeThread { MaximaLibRequest::StartGameRequest(info, settings) => { Ok(start_game_request(maxima_arc.clone(), info, settings).await?) } + MaximaLibRequest::SaveGameSettings(slug, settings) => { + // Persist the UI settings into the core GameSettingsManager + let mut maxima = maxima_arc.lock().await; + let manager = maxima.mut_game_settings(); + // Clone current core settings, update the mutable fields, then save + let mut core_settings = manager.get(&slug).clone(); + core_settings.update_from(settings.cloud_saves, settings.launch_args.clone(), settings.exe_override.clone()); + manager.save(&slug, core_settings); + Ok(()) + } MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), //TODO: kill the bridge thread }; if let Err(err) = action { diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index e3e0278..6c1fc39 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -599,31 +599,7 @@ fn tab_button(ui: &mut Ui, edit_var: &mut PageType, page: PageType, label: &str) // god-awful macro to do something incredibly simple because apparently wrapping it in a function has rustc fucking implode // say what you want about C++ footguns but rust is the polar fucking opposite, shooting you in the head for doing literally anything -macro_rules! set_app_modal { - ($arg1:expr, $arg2:expr) => { - if let Some(modal) = $arg2 { - match modal { - PopupModal::GameSettings(slug) => { - if $arg1.settings.game_settings.get(&slug).is_none() { - $arg1 - .settings - .game_settings - .insert(slug.clone(), crate::GameSettings::new()); - } - } - PopupModal::GameInstall(_) => { - $arg1.installer_state = InstallModalState::new(&$arg1.settings); - } - PopupModal::GameLaunchOOD(_) => {} - } - $arg1.modal = $arg2; - } else { - $arg1.modal = None; - } - }; -} -pub(crate) use set_app_modal; impl MaximaEguiApp { fn tab_bar(&mut self, header: &mut Ui) { @@ -984,6 +960,15 @@ impl MaximaEguiApp { }); } if clear { + if let Some(PopupModal::GameSettings(slug)) = &self.modal { + if let Some(settings) = self.settings.game_settings.get(slug) { + // Send the updated settings to the backend to persist them + let _ = self + .backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::SaveGameSettings(slug.clone(), settings.clone())); + } + } self.modal = None; } } diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index 5caae1e..1d481c6 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -1,5 +1,5 @@ use crate::{ - bridge_thread, set_app_modal, translation_manager::TranslationManager, + bridge_thread, translation_manager::TranslationManager, widgets::enum_dropdown::enum_dropdown, GameDetails, GameDetailsWrapper, GameInfo, InstallModalState, MaximaEguiApp, PageType, PopupModal, }; @@ -87,7 +87,7 @@ fn game_view_action_buttons(app: &mut MaximaEguiApp, game: &GameInfo, ui: &mut U if !app.settings.ignore_ood_games && &game.version.installed != &game.version.latest { - set_app_modal!(app, Some(PopupModal::GameLaunchOOD(game.slug.clone()))); + app.modal = Some(PopupModal::GameLaunchOOD(game.slug.clone())); } else { app.playing_game = Some(game.slug.clone()); let settings = app.settings.game_settings.get(&game.slug); @@ -114,14 +114,18 @@ fn game_view_action_buttons(app: &mut MaximaEguiApp, game: &GameInfo, ui: &mut U } else { let install_str = format!(" {} ", &localization.install.to_uppercase()); if game_view_action_button(install_str, buttons) { - set_app_modal!(app, Some(PopupModal::GameInstall(game.slug.clone()))); + app.installer_state = InstallModalState::new(&app.settings); + app.modal = Some(PopupModal::GameInstall(game.slug.clone())); } } } let settings_str = format!(" {} ", &localization.settings.to_uppercase()); if game_view_action_button(settings_str, buttons) { - set_app_modal!(app, Some(PopupModal::GameSettings(game.slug.clone()))); + if app.settings.game_settings.get(&game.slug).is_none() { + app.settings.game_settings.insert(game.slug.clone(), crate::GameSettings::new()); + } + app.modal = Some(PopupModal::GameSettings(game.slug.clone())); } }); }); From eb16caea446b3d83d3e4a5f8ab49996e3f15e8fc Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:17:47 -0600 Subject: [PATCH 05/17] Installation based on JSON settings --- maxima-cli/src/main.rs | 9 +++------ maxima-lib/src/content/manager.rs | 2 +- maxima-lib/src/core/launch.rs | 4 +--- maxima-lib/src/core/library.rs | 28 ++++++++-------------------- maxima-lib/src/core/mod.rs | 3 ++- maxima-lib/src/gamesettings/mod.rs | 17 +++++++++-------- maxima-ui/src/bridge/get_games.rs | 15 +++++++-------- maxima-ui/src/bridge_processor.rs | 30 +++++++++++++++++++++++++++--- maxima-ui/src/bridge_thread.rs | 19 +++++++++++++------ maxima-ui/src/main.rs | 18 +----------------- 10 files changed, 72 insertions(+), 73 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index e3afc39..de2ce96 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -359,9 +359,8 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; let mut owned_games = Vec::new(); - let game_settings = maxima.mut_game_settings().clone(); for game in maxima.mut_library().games().await? { - if !game.base_offer().is_installed(&game_settings).await { + if !game.base_offer().is_installed().await { continue; } @@ -388,9 +387,8 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let offer_id = { let mut owned_games = Vec::new(); - let game_settings = maxima.mut_game_settings().clone(); for game in maxima.mut_library().games().await? { - if game.base_offer().is_installed(&game_settings).await { + if game.base_offer().is_installed().await { continue; } @@ -742,7 +740,6 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; info!("Owned games:"); - let game_settings = maxima.game_settings().clone(); let titles = maxima.mut_library().games().await?; for title in titles { @@ -751,7 +748,7 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { title.base_offer().slug(), title.name(), title.base_offer().offer_id(), - title.base_offer().is_installed(&game_settings).await, + title.base_offer().is_installed().await, width = 35, width2 = 35, width3 = 25, diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index ead9d03..9b9fe10 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -16,6 +16,7 @@ use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; use globset::GlobSet; +use crate::core::LockedMaxima; use crate::{ content::{ @@ -269,7 +270,6 @@ impl GameDownloader { info!("Files downloaded, running touchup..."); let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(path).await?; info!("Installation finished!"); diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 18b9b5a..87579f1 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -188,8 +188,6 @@ pub async fn start_game( } } - let game_settings = maxima.mut_game_settings().clone(); - let (content_id, online_offline, offer, access_token) = if let LaunchMode::Online(ref offer_id) = mode { let access_token = &maxima.access_token().await?; @@ -198,7 +196,7 @@ pub async fn start_game( None => return Err(LaunchError::NoOfferFound(offer_id.clone())), }; - if !offer.is_installed(&game_settings).await { + if !offer.is_installed().await { return Err(LaunchError::NotInstalled(offer.offer_id().clone())); } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 41bfc99..376b23d 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -13,9 +13,10 @@ use super::{ }; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; -use crate::{core::settings, gamesettings::GameSettingsManager, util::native::{NativeError, SafeStr}}; +use crate::{core::settings, gamesettings::GameSettingsManager, util::native::{NativeError, SafeStr, maxima_dir}}; use crate::util::registry::{parse_partial_registry_path, parse_registry_path, RegistryError}; use derive_getters::Getters; +use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; @@ -54,23 +55,10 @@ pub struct OwnedOffer { } impl OwnedOffer { - pub async fn is_installed(&self, settings: &GameSettingsManager) -> bool { - // I would love to throw an error here but that's just not feasible. - // If you can't grab the path it may as well not be installed. - let Some(path) = &self.offer.install_check_override().as_ref() else { - return false; - }; - let path = match parse_registry_path(path).await { - Ok(path) => path, - Err(_) => return false, - }; - // If it wasn't replaced... - if path.starts_with("[") { - return false; - } - #[cfg(unix)] - let path = case_insensitive_path(path); - path.exists() + pub async fn is_installed(&self) -> bool { + let maxima_dir = maxima_dir().unwrap(); + let manifest_path = maxima_dir.join("settings").join(format!("{}.json", self.slug)); + manifest_path.exists() } pub async fn install_check_path(&self) -> Result { @@ -105,8 +93,8 @@ impl OwnedOffer { } } - pub async fn installed_version(&self, settings: &GameSettingsManager) -> Result { - if !self.is_installed(settings).await { + pub async fn installed_version(&self) -> Result { + if !self.is_installed().await { return Err(LibraryError::NotInstalled(self.slug.clone())); } diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 781ebe6..b20db7e 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -458,7 +458,8 @@ impl Maxima { match result { Err(err) => warn!("Failed to update content manager: {}", err), Ok(result) => { - if let Some(event) = result { + if let Some(event) = result + { self.call_event(event); } } diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index b86241f..8e15eee 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap}; use log::info; use serde::{Deserialize, Serialize}; @@ -7,10 +7,11 @@ use crate::util::native::maxima_dir; #[derive(Serialize, Deserialize, Clone)] pub struct GameSettings { - cloud_saves: bool, - launch_args: String, - exe_override: String, - wine_prefix: String, + pub cloud_saves: bool, + pub installed: bool, + pub launch_args: String, + pub exe_override: String, + pub wine_prefix: String, } impl GameSettings @@ -18,6 +19,7 @@ impl GameSettings pub fn new() -> Self { Self { cloud_saves: true, + installed: false, launch_args: String::new(), exe_override: String::new(), wine_prefix: String::new(), @@ -111,9 +113,8 @@ impl GameSettingsManager { } } - pub fn get(&mut self, slug: &str) -> &GameSettings { - self.settings.entry(slug.to_string()) - .or_insert_with(|| get_game_settings(slug)) + pub fn get(&self, slug: &str) -> GameSettings { + self.settings.get(slug).cloned().unwrap_or_else(|| GameSettings::new_with_slug(slug)) } pub fn save(&mut self, slug: &str, settings: GameSettings) { diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 66e10d8..8587220 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -18,6 +18,8 @@ use maxima::{ }; use std::{fs, sync::mpsc::Sender}; +use maxima::gamesettings::{GameSettings, GameSettingsManager}; + fn get_preferred_bg_hero(heroes: &Option) -> Option { let heroes = match heroes { Some(h) => h.items().get(0), @@ -195,13 +197,14 @@ pub async fn get_games_request( let service_layer = maxima.service_layer().clone(); let locale = maxima.locale().short_str().to_owned(); let logged_in = maxima.auth_storage().lock().await.current().is_some(); - let mut game_settings = maxima.game_settings().clone(); if !logged_in { return Err(BackendError::LoggedOut); } + let game_settings = maxima.game_settings().clone(); let owned_games = maxima.mut_library().games().await?; + for game in owned_games { let slug = game.base_offer().slug().clone(); info!("processing {}", &slug); @@ -213,7 +216,7 @@ pub async fn get_games_request( downloads.iter().find(|item| item.download_type() == "LIVE").unwrap() }; - let version = if let Ok(version) = game.base_offer().installed_version(&mut game_settings).await { + let version = if let Ok(version) = game.base_offer().installed_version().await { version } else { "Unknown".to_owned() @@ -230,17 +233,13 @@ pub async fn get_games_request( mandatory: opt.treat_updates_as_mandatory().clone(), }, dlc: game.extra_offers().clone(), - installed: game.base_offer().is_installed(&mut game_settings).await, + installed: game.base_offer().is_installed().await, has_cloud_saves: game.base_offer().offer().has_cloud_save(), }; let slug = game_info.slug.clone(); // Grab persisted settings from Maxima's GameSettingsManager if available let core_settings = game_settings.get(&slug); - let settings = crate::GameSettings { - cloud_saves: core_settings.cloud_saves(), - launch_args: core_settings.launch_args().to_string(), - exe_override: core_settings.exe_override().to_string(), - }; + let settings = core_settings.clone(); let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info, settings, diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 5ae00e0..d3cf25a 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -5,6 +5,7 @@ use crate::{ }; use log::{error, info, warn}; use std::sync::mpsc::TryRecvError; +use maxima::gamesettings::GameSettings; pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { puffin::profile_function!(); @@ -85,9 +86,32 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } } } - DownloadFinished(_) => { - // idk - } + DownloadFinished(offer_id) => + { + let mut slug = String::new(); + for (s, game) in &app.games { + if game.offer == offer_id { + slug = s.clone(); + break; + } + } + if slug.is_empty() { continue; } + + // update frontend settings + if let Some(mut settings) = app.settings.game_settings.remove(&slug) { + settings.installed = true; + app.settings.game_settings.insert(slug.clone(), settings.clone()); + + // update game info displayed + if let Some(game) = app.games.get_mut(&slug) { + game.installed = true; + } + + // persist to core + let _ = app.backend.backend_commander.send(bridge_thread::MaximaLibRequest::SaveGameSettings(slug.clone(), settings.clone())); + } + + } DownloadQueueUpdate(current, queue) => { if let Some(current) = current { if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 46b8e8e..5b8fa5f 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -403,9 +403,19 @@ impl BridgeThread { for ev in maxima.consume_pending_events() { match ev { maxima::core::MaximaEvent::ReceivedLSXRequest(_, _) => {} - maxima::core::MaximaEvent::InstallFinished(offer_id) => { + maxima::core::MaximaEvent::InstallFinished(offer_id) => + { + // Easy access to mutably update game settings + if let Ok(Some(title)) = maxima.mut_library().title_by_base_offer(&offer_id).await + { + let slug = title.base_offer().slug().clone(); + let manager = maxima.mut_game_settings(); + let mut settings = manager.get(&slug); + settings.installed = true; + manager.save(&slug, settings); + } backend_responder - .send(MaximaLibResponse::DownloadFinished(offer_id))?; + .send(MaximaLibResponse::DownloadFinished(offer_id))?; // UI can handle updating settings, can easily access UI Frontend structures in DownloadFinished Self::update_queue(maxima.content_manager(), backend_responder.clone()); } } @@ -514,10 +524,7 @@ impl BridgeThread { // Persist the UI settings into the core GameSettingsManager let mut maxima = maxima_arc.lock().await; let manager = maxima.mut_game_settings(); - // Clone current core settings, update the mutable fields, then save - let mut core_settings = manager.get(&slug).clone(); - core_settings.update_from(settings.cloud_saves, settings.launch_args.clone(), settings.exe_override.clone()); - manager.save(&slug, core_settings); + manager.save(&slug, settings); Ok(()) } MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), //TODO: kill the bridge thread diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 6c1fc39..b66b549 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -33,6 +33,7 @@ use bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameResponse use game_view_bg_renderer::GameViewBgRenderer; use renderers::{app_bg_renderer, game_view_bg_renderer}; use translation_manager::{positional_replace, TranslationManager}; +use maxima::gamesettings::GameSettings; pub mod bridge; pub mod util; @@ -186,23 +187,6 @@ pub enum GameDetailsWrapper { Available(GameDetails), } -#[derive(Clone, serde::Serialize, serde::Deserialize)] -pub struct GameSettings { - cloud_saves: bool, - launch_args: String, - exe_override: String, -} - -impl GameSettings { - pub fn new() -> Self { - Self { - cloud_saves: true, - launch_args: String::new(), - exe_override: String::new(), - } - } -} - #[derive(Clone)] pub struct GameVersionInfo { installed: String, From fbc452aa562036f2da6e604942402673f8bcc3e8 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:44:17 -0600 Subject: [PATCH 06/17] Check for boolean in is_installed, fix wrong method when reading game settings during iteration of owned games --- maxima-lib/src/core/library.rs | 18 +++++++++++++++++- maxima-ui/src/bridge/get_games.rs | 18 +++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 376b23d..becf876 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -58,7 +58,23 @@ impl OwnedOffer { pub async fn is_installed(&self) -> bool { let maxima_dir = maxima_dir().unwrap(); let manifest_path = maxima_dir.join("settings").join(format!("{}.json", self.slug)); - manifest_path.exists() + if !manifest_path.exists() { + return false; + } + + let contents = match std::fs::read_to_string(&manifest_path) + { + Ok(s) => s, + Err(_) => return false, + }; + + match serde_json::from_str::(&contents) { + Ok(json) => json + .get("installed") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + Err(_) => false, + } } pub async fn install_check_path(&self) -> Result { diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 8587220..14340bb 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -7,14 +7,10 @@ use egui::Context; use log::{debug, error, info}; use maxima::{ core::{ - service_layer::{ - ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, - ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, - SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, - }, - LockedMaxima, - }, - util::native::maxima_dir, + LockedMaxima, service_layer::{ + SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient + } + }, gamesettings::get_game_settings, util::native::maxima_dir }; use std::{fs, sync::mpsc::Sender}; @@ -200,7 +196,7 @@ pub async fn get_games_request( if !logged_in { return Err(BackendError::LoggedOut); } - let game_settings = maxima.game_settings().clone(); + let mut game_settings = maxima.mut_game_settings().clone(); let owned_games = maxima.mut_library().games().await?; @@ -208,7 +204,6 @@ pub async fn get_games_request( for game in owned_games { let slug = game.base_offer().slug().clone(); info!("processing {}", &slug); - let downloads = game.base_offer().offer().downloads(); let opt = if downloads.len() == 1 { &downloads[0] @@ -238,7 +233,8 @@ pub async fn get_games_request( }; let slug = game_info.slug.clone(); // Grab persisted settings from Maxima's GameSettingsManager if available - let core_settings = game_settings.get(&slug); + let core_settings = get_game_settings(&slug); + game_settings.save(&slug, core_settings.clone()); let settings = core_settings.clone(); let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info, From 50b9632bd36cb1a717f3de93e9dd54f2c357ddf6 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:07:37 -0600 Subject: [PATCH 07/17] Only write settings file if game is installed --- maxima-lib/src/gamesettings/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index 8e15eee..ab3cb6a 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -74,6 +74,11 @@ pub fn get_game_settings(slug: &str) -> GameSettings pub fn save_game_settings(slug: &str, settings: &GameSettings) { + if settings.installed == false + { + info!("Skipping save for {} as game is not installed.", slug); + return; + } info!("Saving settings for {}...", slug); if let Ok(dir) = maxima_dir() { From 07dc67ea5c3c1ac9bd0a77b4ccbd3e3d9c717a80 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 30 Jan 2026 01:17:23 -0600 Subject: [PATCH 08/17] Run cargo fmt --- maxima-lib/src/content/exclusion.rs | 48 ++++++++++++--------------- maxima-lib/src/content/manager.rs | 10 +++--- maxima-lib/src/content/mod.rs | 2 +- maxima-lib/src/core/auth/hardware.rs | 8 +++-- maxima-lib/src/core/cloudsync.rs | 3 +- maxima-lib/src/core/launch.rs | 15 +++++++-- maxima-lib/src/core/library.rs | 19 +++++++---- maxima-lib/src/core/mod.rs | 6 ++-- maxima-lib/src/gamesettings/mod.rs | 40 +++++++++++----------- maxima-lib/src/lib.rs | 2 +- maxima-lib/src/lsx/request/account.rs | 3 +- maxima-lib/src/lsx/request/license.rs | 2 +- maxima-lib/src/unix/wine.rs | 21 ++++++------ maxima-ui/src/bridge/get_games.rs | 14 +++++--- maxima-ui/src/bridge_processor.rs | 47 ++++++++++++++------------ maxima-ui/src/bridge_thread.rs | 12 +++---- maxima-ui/src/main.rs | 13 ++++---- maxima-ui/src/views/game_view.rs | 10 +++--- 18 files changed, 146 insertions(+), 129 deletions(-) diff --git a/maxima-lib/src/content/exclusion.rs b/maxima-lib/src/content/exclusion.rs index c685869..9142939 100644 --- a/maxima-lib/src/content/exclusion.rs +++ b/maxima-lib/src/content/exclusion.rs @@ -1,46 +1,40 @@ +use crate::util::native::maxima_dir; +use globset::{Glob, GlobSet, GlobSetBuilder}; +use log::{error, info, warn}; use std::fs::File; use std::io::{BufRead, BufReader}; -use crate::util::native::maxima_dir; -use globset::{GlobSet, GlobSetBuilder, Glob}; -use log::{info, warn, error}; -pub fn get_exclusion_list(offer_id: String) -> GlobSet -{ +pub fn get_exclusion_list(offer_id: String) -> GlobSet { let mut builder = GlobSetBuilder::new(); - if let Ok(dir) = maxima_dir() // Checks to make sure maxima directory exists + if let Ok(dir) = maxima_dir() + // Checks to make sure maxima directory exists { let filepath = dir.join("exclude").join(&offer_id); // Path to exclusion file info!("Loading exclusion file from {}", filepath.display()); - if let Ok(file) = File::open(&filepath) // Opens the exclusion file, fails if not found - { + if let Ok(file) = File::open(&filepath) + // Opens the exclusion file, fails if not found + { let reader = BufReader::new(file); - for line in reader.lines().flatten() - { + for line in reader.lines().flatten() { let entry = line.trim(); - if !entry.is_empty() && !entry.starts_with('#') - { - match Glob::new(entry) // Create a glob from the entry, checks if valid pattern, if not logs a warning, - { - Ok(glob) => - { - builder.add(glob); - } - Err(_) => warn!("Invalid glob pattern '{}' in {}", entry, filepath.display()), + if !entry.is_empty() && !entry.starts_with('#') { + if let Ok(g) = Glob::new(entry) { + builder.add(g); + } else { + warn!("Invalid glob pattern '{}' in {}", entry, filepath.display()); } } } - } - else - { + } else { warn!("Exclusion file not found: {}", filepath.display()); } - } - else - { + } else { error!("Failed to resolve maxima data directory"); } - builder.build().unwrap_or_else(|_| GlobSetBuilder::new().build().unwrap()) // Returns an empty GlobSet on failure -} \ No newline at end of file + builder + .build() + .unwrap_or_else(|_| GlobSetBuilder::new().build().unwrap()) // Returns an empty GlobSet on failure +} diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 9b9fe10..ae9b50d 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -6,17 +6,17 @@ use std::{ }, }; +use crate::core::LockedMaxima; use derive_builder::Builder; use derive_getters::Getters; use futures::StreamExt; +use globset::GlobSet; use log::{debug, error, info}; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; -use globset::GlobSet; -use crate::core::LockedMaxima; use crate::{ content::{ @@ -155,11 +155,9 @@ impl GameDownloader { let exclusion_list = get_exclusion_list(game.offer_id.clone()); - for ele in downloader.manifest().entries() - { + for ele in downloader.manifest().entries() { // TODO: Filtering - if exclusion_list.is_match(&ele.name()) - { + if exclusion_list.is_match(&ele.name()) { info!("Excluding file from download: {}", ele.name()); continue; } diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index d649f8f..252950d 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -12,10 +12,10 @@ use crate::core::{ }; pub mod downloader; +pub mod exclusion; pub mod manager; pub mod zip; pub mod zlib; -pub mod exclusion; pub struct ContentService { service_layer: ServiceLayerClient, diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index c11df25..2eaf817 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -282,7 +282,8 @@ impl HardwareInfo { let mut gpu_pnp_id: Option = None; let output = Command::new("system_profiler") .args(["SPDisplaysDataType", "-json"]) - .output().unwrap(); + .output() + .unwrap(); if output.status.success() { let json = String::from_utf8_lossy(&output.stdout); let result: SPDisplaysDataType = serde_json::from_str(&json).unwrap(); @@ -298,7 +299,10 @@ impl HardwareInfo { } let mut disk_sn = String::from("None"); - let output = Command::new("diskutil").args(["info", "/"]).output().unwrap(); + let output = Command::new("diskutil") + .args(["info", "/"]) + .output() + .unwrap(); // Check if the command was successful if output.status.success() { // Convert the output bytes to a UTF-8 string diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 41e4cd7..69b6a21 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -13,7 +13,6 @@ /// - Call `/lock/authorize` with a `Vec`, creating details that match the file and keeping track of them for later /// - Push the files to the endpoints, along with a manifest outlining the files you uploaded and/or that are already there. /// - Call `/lock/delete` - use super::{ auth::storage::LockedAuthStorage, endpoints::API_CLOUDSYNC, launch::LaunchMode, library::OwnedOffer, @@ -506,7 +505,7 @@ impl<'a> CloudSyncLock<'a> { len as u64 } - WriteData::Text {text, .. } => { + WriteData::Text { text, .. } => { req = req.body(text.to_owned()); text.len() as u64 } diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 87579f1..e6c183d 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -402,7 +402,7 @@ async fn request_opaque_ooa_token(access_token: &str) -> Result Result<(), NativeError> { use crate::unix::wine::{ check_runtime_validity, check_wine_validity, get_lutris_runtimes, install_runtime, - install_wine, setup_wine_registry, run_wine_command, wine_prefix_dir, CommandType, + install_wine, run_wine_command, setup_wine_registry, wine_prefix_dir, CommandType, }; std::fs::create_dir_all(wine_prefix_dir()?)?; @@ -414,7 +414,9 @@ pub async fn mx_linux_setup() -> Result<(), NativeError> { install_wine().await?; } let runtimes = get_lutris_runtimes().await?; - if !check_runtime_validity("eac_runtime", &runtimes).await? && !std::env::var("MAXIMA_DISABLE_EAC").is_ok() { + if !check_runtime_validity("eac_runtime", &runtimes).await? + && !std::env::var("MAXIMA_DISABLE_EAC").is_ok() + { install_runtime("eac_runtime", &runtimes).await?; } let use_slr = std::env::var("MAXIMA_USE_SLR").is_ok(); @@ -423,7 +425,14 @@ pub async fn mx_linux_setup() -> Result<(), NativeError> { } } - let _ = run_wine_command("wineboot", Some(vec!["--init"]), None, false, CommandType::Run).await; + let _ = run_wine_command( + "wineboot", + Some(vec!["--init"]), + None, + false, + CommandType::Run, + ) + .await; info!("Setting up wine registry..."); setup_wine_registry().await?; diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index becf876..8779694 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -13,8 +13,12 @@ use super::{ }; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; -use crate::{core::settings, gamesettings::GameSettingsManager, util::native::{NativeError, SafeStr, maxima_dir}}; use crate::util::registry::{parse_partial_registry_path, parse_registry_path, RegistryError}; +use crate::{ + core::settings, + gamesettings::GameSettingsManager, + util::native::{maxima_dir, NativeError, SafeStr}, +}; use derive_getters::Getters; use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; @@ -57,22 +61,23 @@ pub struct OwnedOffer { impl OwnedOffer { pub async fn is_installed(&self) -> bool { let maxima_dir = maxima_dir().unwrap(); - let manifest_path = maxima_dir.join("settings").join(format!("{}.json", self.slug)); + let manifest_path = maxima_dir + .join("settings") + .join(format!("{}.json", self.slug)); if !manifest_path.exists() { return false; } - let contents = match std::fs::read_to_string(&manifest_path) - { + let contents = match std::fs::read_to_string(&manifest_path) { Ok(s) => s, Err(_) => return false, }; match serde_json::from_str::(&contents) { Ok(json) => json - .get("installed") - .and_then(|v| v.as_bool()) - .unwrap_or(false), + .get("installed") + .and_then(|v| v.as_bool()) + .unwrap_or(false), Err(_) => false, } } diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index b20db7e..2109460 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -46,7 +46,6 @@ use std::sync::Arc; use thiserror::Error; use tokio::sync::Mutex; - use self::{ auth::storage::{AuthError, AuthStorage, LockedAuthStorage, TokenError}, cache::DynamicCache, @@ -65,10 +64,10 @@ use self::{ }; use crate::{ content::manager::{ContentManager, ContentManagerError}, + gamesettings::GameSettingsManager, lsx::{self, service::LSXServerError, types::LSXRequestType}, rtm::client::{BasicPresence, RtmClient}, util::native::{maxima_dir, NativeError}, - gamesettings::{GameSettingsManager}, }; #[derive(Clone, IntoStaticStr)] @@ -458,8 +457,7 @@ impl Maxima { match result { Err(err) => warn!("Failed to update content manager: {}", err), Ok(result) => { - if let Some(event) = result - { + if let Some(event) = result { self.call_event(event); } } diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index ab3cb6a..dc86912 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap}; +use std::collections::HashMap; +use crate::util::native::maxima_dir; use log::info; use serde::{Deserialize, Serialize}; use serde_json; -use crate::util::native::maxima_dir; #[derive(Serialize, Deserialize, Clone)] pub struct GameSettings { @@ -14,8 +14,7 @@ pub struct GameSettings { pub wine_prefix: String, } -impl GameSettings -{ +impl GameSettings { pub fn new() -> Self { Self { cloud_saves: true, @@ -57,8 +56,7 @@ impl GameSettings } } -pub fn get_game_settings(slug: &str) -> GameSettings -{ +pub fn get_game_settings(slug: &str) -> GameSettings { let path = match maxima_dir() { Ok(dir) => dir.join("settings").join(format!("{}.json", slug)), Err(_) => return GameSettings::new_with_slug(slug), @@ -72,16 +70,13 @@ pub fn get_game_settings(slug: &str) -> GameSettings serde_json::from_str(&content).unwrap_or_else(|_| GameSettings::new_with_slug(slug)) } -pub fn save_game_settings(slug: &str, settings: &GameSettings) -{ - if settings.installed == false - { +pub fn save_game_settings(slug: &str, settings: &GameSettings) { + if settings.installed == false { info!("Skipping save for {} as game is not installed.", slug); return; } info!("Saving settings for {}...", slug); - if let Ok(dir) = maxima_dir() - { + if let Ok(dir) = maxima_dir() { let settings_dir = dir.join("settings"); // Ensure the settings directory exists if let Err(err) = std::fs::create_dir_all(&settings_dir) { @@ -90,19 +85,19 @@ pub fn save_game_settings(slug: &str, settings: &GameSettings) } let path = settings_dir.join(format!("{}.json", slug)); - if let Ok(content) = serde_json::to_string_pretty(settings) - { + if let Ok(content) = serde_json::to_string_pretty(settings) { match std::fs::write(&path, content) { Ok(()) => info!("Saved settings to {:?}", path), Err(err) => info!("Failed to write settings for {}: {}", slug, err), } - } - else { + } else { info!("Failed to serialize settings for {}", slug); } - } - else { - info!("Failed to get maxima directory, cannot save settings for {}", slug); + } else { + info!( + "Failed to get maxima directory, cannot save settings for {}", + slug + ); } } @@ -119,11 +114,14 @@ impl GameSettingsManager { } pub fn get(&self, slug: &str) -> GameSettings { - self.settings.get(slug).cloned().unwrap_or_else(|| GameSettings::new_with_slug(slug)) + self.settings + .get(slug) + .cloned() + .unwrap_or_else(|| GameSettings::new_with_slug(slug)) } pub fn save(&mut self, slug: &str, settings: GameSettings) { save_game_settings(slug, &settings); self.settings.insert(slug.to_string(), settings); } -} \ No newline at end of file +} diff --git a/maxima-lib/src/lib.rs b/maxima-lib/src/lib.rs index f444182..8493414 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -6,11 +6,11 @@ pub mod content; pub mod core; +pub mod gamesettings; pub mod lsx; pub mod ooa; pub mod rtm; pub mod util; -pub mod gamesettings; #[cfg(unix)] pub mod unix; diff --git a/maxima-lib/src/lsx/request/account.rs b/maxima-lib/src/lsx/request/account.rs index 5ddad4f..3edd36f 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -33,7 +33,8 @@ pub async fn handle_query_entitlements_request( .include_child_groups(false) .entitlement_tag("".to_string()) .group_names([request.attr_Group.clone()].to_vec()) - .build().unwrap(), + .build() + .unwrap(), ) .await?; diff --git a/maxima-lib/src/lsx/request/license.rs b/maxima-lib/src/lsx/request/license.rs index 19261ac..757bda9 100644 --- a/maxima-lib/src/lsx/request/license.rs +++ b/maxima-lib/src/lsx/request/license.rs @@ -1,5 +1,5 @@ -use std::env; use log::{debug, info}; +use std::env; use crate::{ core::{auth::hardware::HardwareInfo, launch::LaunchMode}, diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 96c4b50..ba6c5d9 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -327,8 +327,8 @@ async fn run_wine_command_slr, T: AsRef>( want_output: bool, command_type: CommandType, ) -> Result { - let slr_path = env::var("MAXIMA_SLR_PATH") - .map_err(|_| NativeError::Wine(WineError::MissingSLRPath))?; + let slr_path = + env::var("MAXIMA_SLR_PATH").map_err(|_| NativeError::Wine(WineError::MissingSLRPath))?; let proton_dir_path = env::var("MAXIMA_PROTON_PATH") .map_err(|_| NativeError::Wine(WineError::MissingProtonPath))?; let proton_exe = PathBuf::from(&proton_dir_path) @@ -336,18 +336,17 @@ async fn run_wine_command_slr, T: AsRef>( .to_string_lossy() .to_string(); let proton_prefix_path = wine_prefix_dir()?; - + // Get the Steam client install path, defaulting to common location - let steam_client_path = env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH") - .unwrap_or_else(|_| { - env::var("HOME") - .map(|h| format!("{}/.steam/steam", h)) - .unwrap_or_else(|_| "/home/user/.steam/steam".to_string()) - }); + let steam_client_path = env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH").unwrap_or_else(|_| { + env::var("HOME") + .map(|h| format!("{}/.steam/steam", h)) + .unwrap_or_else(|_| "/home/user/.steam/steam".to_string()) + }); // Build the SLR entry point path let slr_entry_point = PathBuf::from(&slr_path).join("_v2-entry-point"); - + if !slr_entry_point.exists() { return Err(NativeError::Wine(WineError::SLRNotFound(slr_entry_point))); } @@ -355,7 +354,7 @@ async fn run_wine_command_slr, T: AsRef>( // Build proton command with verb passed to _v2-entry-point let mut proton_args = vec![proton_exe.clone(), "run".to_string()]; proton_args.push(arg.as_ref().to_string_lossy().to_string()); - + if let Some(arguments) = args { for a in arguments { proton_args.push(a.as_ref().to_string_lossy().to_string()); diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 14340bb..79672b8 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -7,10 +7,15 @@ use egui::Context; use log::{debug, error, info}; use maxima::{ core::{ - LockedMaxima, service_layer::{ - SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient - } - }, gamesettings::get_game_settings, util::native::maxima_dir + service_layer::{ + ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, + ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, + SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, + }, + LockedMaxima, + }, + gamesettings::get_game_settings, + util::native::maxima_dir, }; use std::{fs, sync::mpsc::Sender}; @@ -200,7 +205,6 @@ pub async fn get_games_request( let owned_games = maxima.mut_library().games().await?; - for game in owned_games { let slug = game.base_offer().slug().clone(); info!("processing {}", &slug); diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index d3cf25a..65a023a 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -4,8 +4,8 @@ use crate::{ BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, }; use log::{error, info, warn}; -use std::sync::mpsc::TryRecvError; use maxima::gamesettings::GameSettings; +use std::sync::mpsc::TryRecvError; pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { puffin::profile_function!(); @@ -86,32 +86,37 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } } } - DownloadFinished(offer_id) => - { + DownloadFinished(offer_id) => { let mut slug = String::new(); - for (s, game) in &app.games { - if game.offer == offer_id { - slug = s.clone(); - break; - } + for (s, game) in &app.games { + if game.offer == offer_id { + slug = s.clone(); + break; } - if slug.is_empty() { continue; } - - // update frontend settings - if let Some(mut settings) = app.settings.game_settings.remove(&slug) { - settings.installed = true; - app.settings.game_settings.insert(slug.clone(), settings.clone()); + } + if slug.is_empty() { + continue; + } - // update game info displayed - if let Some(game) = app.games.get_mut(&slug) { - game.installed = true; - } + // update frontend settings + if let Some(mut settings) = app.settings.game_settings.remove(&slug) { + settings.installed = true; + app.settings.game_settings.insert(slug.clone(), settings.clone()); - // persist to core - let _ = app.backend.backend_commander.send(bridge_thread::MaximaLibRequest::SaveGameSettings(slug.clone(), settings.clone())); + // update game info displayed + if let Some(game) = app.games.get_mut(&slug) { + game.installed = true; } - } + // persist to core + let _ = app.backend.backend_commander.send( + bridge_thread::MaximaLibRequest::SaveGameSettings( + slug.clone(), + settings.clone(), + ), + ); + } + } DownloadQueueUpdate(current, queue) => { if let Some(current) = current { if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 5b8fa5f..7c8f6bf 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -87,7 +87,7 @@ pub enum MaximaLibRequest { /// Persist UI-side game settings into the core GameSettingsManager SaveGameSettings(String, GameSettings), ShutdownRequest, -} +} pub enum MaximaLibResponse { LoginResponse(Result), @@ -403,11 +403,11 @@ impl BridgeThread { for ev in maxima.consume_pending_events() { match ev { maxima::core::MaximaEvent::ReceivedLSXRequest(_, _) => {} - maxima::core::MaximaEvent::InstallFinished(offer_id) => - { + maxima::core::MaximaEvent::InstallFinished(offer_id) => { // Easy access to mutably update game settings - if let Ok(Some(title)) = maxima.mut_library().title_by_base_offer(&offer_id).await - { + if let Ok(Some(title)) = + maxima.mut_library().title_by_base_offer(&offer_id).await + { let slug = title.base_offer().slug().clone(); let manager = maxima.mut_game_settings(); let mut settings = manager.get(&slug); @@ -524,7 +524,7 @@ impl BridgeThread { // Persist the UI settings into the core GameSettingsManager let mut maxima = maxima_arc.lock().await; let manager = maxima.mut_game_settings(); - manager.save(&slug, settings); + manager.save(&slug, settings); Ok(()) } MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), //TODO: kill the bridge thread diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index b66b549..0971aff 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -31,9 +31,9 @@ use egui_glow::glow; use app_bg_renderer::AppBgRenderer; use bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameResponse}; use game_view_bg_renderer::GameViewBgRenderer; +use maxima::gamesettings::GameSettings; use renderers::{app_bg_renderer, game_view_bg_renderer}; use translation_manager::{positional_replace, TranslationManager}; -use maxima::gamesettings::GameSettings; pub mod bridge; pub mod util; @@ -584,7 +584,6 @@ fn tab_button(ui: &mut Ui, edit_var: &mut PageType, page: PageType, label: &str) // god-awful macro to do something incredibly simple because apparently wrapping it in a function has rustc fucking implode // say what you want about C++ footguns but rust is the polar fucking opposite, shooting you in the head for doing literally anything - impl MaximaEguiApp { fn tab_bar(&mut self, header: &mut Ui) { puffin::profile_function!(); @@ -947,10 +946,12 @@ impl MaximaEguiApp { if let Some(PopupModal::GameSettings(slug)) = &self.modal { if let Some(settings) = self.settings.game_settings.get(slug) { // Send the updated settings to the backend to persist them - let _ = self - .backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::SaveGameSettings(slug.clone(), settings.clone())); + let _ = self.backend.backend_commander.send( + bridge_thread::MaximaLibRequest::SaveGameSettings( + slug.clone(), + settings.clone(), + ), + ); } } self.modal = None; diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index 1d481c6..15031a5 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -1,7 +1,7 @@ use crate::{ - bridge_thread, translation_manager::TranslationManager, - widgets::enum_dropdown::enum_dropdown, GameDetails, GameDetailsWrapper, GameInfo, - InstallModalState, MaximaEguiApp, PageType, PopupModal, + bridge_thread, translation_manager::TranslationManager, widgets::enum_dropdown::enum_dropdown, + GameDetails, GameDetailsWrapper, GameInfo, InstallModalState, MaximaEguiApp, PageType, + PopupModal, }; use egui::{ pos2, vec2, Color32, Margin, Mesh, Pos2, Rect, RichText, Rounding, ScrollArea, Shape, Stroke, @@ -123,7 +123,9 @@ fn game_view_action_buttons(app: &mut MaximaEguiApp, game: &GameInfo, ui: &mut U let settings_str = format!(" {} ", &localization.settings.to_uppercase()); if game_view_action_button(settings_str, buttons) { if app.settings.game_settings.get(&game.slug).is_none() { - app.settings.game_settings.insert(game.slug.clone(), crate::GameSettings::new()); + app.settings + .game_settings + .insert(game.slug.clone(), crate::GameSettings::new()); } app.modal = Some(PopupModal::GameSettings(game.slug.clone())); } From a2a0d08dc906f64e9b17a4dc819906a530e2283a Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:28:53 -0600 Subject: [PATCH 09/17] Seperate wine prefixes protype --- maxima-bootstrap/src/main.rs | 1 + maxima-cli/src/main.rs | 9 ++-- maxima-lib/src/content/manager.rs | 8 +++- maxima-lib/src/core/auth/hardware.rs | 21 ++++------ maxima-lib/src/core/auth/pc_sign.rs | 2 +- maxima-lib/src/core/background_service_nix.rs | 14 ++++++- maxima-lib/src/core/cloudsync.rs | 28 ++++++++----- maxima-lib/src/core/launch.rs | 41 ++++++++++++++---- maxima-lib/src/core/library.rs | 4 +- maxima-lib/src/core/manifest/dip.rs | 16 +++++-- maxima-lib/src/core/manifest/mod.rs | 10 ++--- maxima-lib/src/core/manifest/pre_dip.rs | 16 +++++-- maxima-lib/src/lsx/connection.rs | 17 ++++++-- maxima-lib/src/lsx/request/license.rs | 3 +- maxima-lib/src/ooa/mod.rs | 24 +++++++---- maxima-lib/src/unix/wine.rs | 42 ++++++++++--------- maxima-lib/src/util/registry.rs | 17 ++++---- maxima-ui/src/bridge_thread.rs | 15 +++---- maxima-ui/src/main.rs | 4 +- 19 files changed, 191 insertions(+), 101 deletions(-) diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index 9cd16c2..0429789 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -132,6 +132,7 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { None, false, CommandType::WaitForExitAndRun, + Some(&args.slug), ) .await?; diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index de2ce96..2f46d97 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -70,6 +70,7 @@ enum Mode { ListGames, LocateGame { path: String, + slug: String, }, CloudSync { game_slug: String, @@ -301,7 +302,7 @@ async fn startup() -> Result<()> { start_game(&offer_id, game_path, game_args, login, maxima_arc.clone()).await } Mode::ListGames => list_games(maxima_arc.clone()).await, - Mode::LocateGame { path } => locate_game(maxima_arc.clone(), &path).await, + Mode::LocateGame { path, slug } => locate_game(maxima_arc.clone(), &path, &slug).await, Mode::CloudSync { game_slug, write } => { do_cloud_sync(maxima_arc.clone(), &game_slug, write).await } @@ -622,7 +623,7 @@ async fn juno_token_refresh(maxima_arc: LockedMaxima) -> Result<()> { } async fn read_license_file(content_id: &str) -> Result<()> { - let path = ooa::get_license_dir()?.join(format!("{}.dlf", content_id)); + let path = ooa::get_license_dir(None)?.join(format!("{}.dlf", content_id)); let mut data = tokio::fs::read(path).await?; data.drain(0..65); // Signature @@ -768,10 +769,10 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { Ok(()) } -async fn locate_game(maxima_arc: LockedMaxima, path: &str) -> Result<()> { +async fn locate_game(maxima_arc: LockedMaxima, path: &str, slug: &str) -> Result<()> { let path = PathBuf::from(path); let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(&path).await?; + manifest.run_touchup(&path, slug).await?; info!("Installed!"); Ok(()) } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index ae9b50d..4f29ced 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -41,6 +41,7 @@ pub struct QueuedGame { offer_id: String, build_id: String, path: PathBuf, + slug: String, } #[derive(Default, Getters, Serialize, Deserialize)] @@ -127,6 +128,7 @@ impl DownloadQueue { pub struct GameDownloader { offer_id: String, + slug: String, downloader: Arc, entries: Vec, @@ -173,6 +175,7 @@ impl GameDownloader { Ok(GameDownloader { offer_id: game.offer_id.to_owned(), + slug: game.slug.to_owned(), downloader: Arc::new(downloader), entries, @@ -188,6 +191,7 @@ impl GameDownloader { let (downloader_arc, entries, cancel_token, completed_bytes, notify) = self.prepare_download_vars(); let total_count = self.total_count; + let slug = self.slug.clone(); tokio::spawn(async move { let dl = GameDownloader::start_downloads( total_count, @@ -196,6 +200,7 @@ impl GameDownloader { cancel_token, completed_bytes, notify, + slug, ) .await; if let Err(err) = dl { @@ -229,6 +234,7 @@ impl GameDownloader { cancel_token: CancellationToken, completed_bytes: Arc, notify: Arc, + slug: String, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(total_count); @@ -268,7 +274,7 @@ impl GameDownloader { info!("Files downloaded, running touchup..."); let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(path).await?; + manifest.run_touchup(path, &slug).await?; info!("Installation finished!"); completed_bytes.fetch_add(1, Ordering::SeqCst); diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index 2eaf817..ecc5b39 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -149,7 +149,7 @@ impl HardwareInfo { } #[cfg(target_os = "linux")] - pub fn new(version: u32) -> Self { + pub fn new(version: u32, slug: Option<&str>) -> Self { use std::{fs, path::Path, process::Command}; let board_manufacturer = match fs::read_to_string("/sys/class/dmi/id/board_vendor") { @@ -167,7 +167,7 @@ impl HardwareInfo { }; let bios_sn = String::from("Serial number"); - let os_install_date = get_root_creation_str(); + let os_install_date = get_root_creation_str(slug); let os_sn = String::from("00330-50000-00000-AAOEM"); let mut gpu_pnp_id: Option = None; @@ -246,7 +246,7 @@ impl HardwareInfo { } #[cfg(target_os = "macos")] - pub fn new(version: u32) -> Self { + pub fn new(version: u32, slug: Option<&str>) -> Self { use std::process::Command; use smbioslib::{ @@ -273,7 +273,7 @@ impl HardwareInfo { bios_sn = bios.serial_number().to_string(); } - let os_install_date = get_root_creation_str(); + let os_install_date = get_root_creation_str(slug); let mut os_sn = String::from("None"); if let Some(uuid) = bios_data.and_then(|bios| bios.uuid()) { os_sn = uuid.to_string(); @@ -476,23 +476,20 @@ impl HardwareInfo { } #[cfg(unix)] -fn get_root_creation_str() -> String { +fn get_root_creation_str(slug: Option<&str>) -> String { use crate::unix::wine::wine_prefix_dir; use chrono::{TimeZone, Utc}; use std::{fs, os::unix::fs::MetadataExt}; let date_str = String::from("1970010100:00:00.000000000+0000"); - let wine_prefix = wine_prefix_dir(); - if wine_prefix.is_err() { - return date_str; - } - let wine_prefix = wine_prefix.unwrap(); + let wine_prefix = match wine_prefix_dir(slug) { + Ok(prefix) => prefix, + Err(_) => return date_str, + }; let date_str = match fs::metadata(wine_prefix.join("drive_c")) { Ok(metadata) => { let nsec = (metadata.mtime_nsec() / 1_000_000) * 1_000_000; - // Convert Unix timestamp to a DateTime let datetime = Utc.timestamp_nanos((metadata.mtime() * 1_000_000_000) + nsec); - // Format the DateTime return datetime.format("%Y%m%d%H%M%S%.6f+000").to_string(); } Err(_) => date_str, diff --git a/maxima-lib/src/core/auth/pc_sign.rs b/maxima-lib/src/core/auth/pc_sign.rs index 6c3bc13..cdc8614 100644 --- a/maxima-lib/src/core/auth/pc_sign.rs +++ b/maxima-lib/src/core/auth/pc_sign.rs @@ -34,7 +34,7 @@ pub struct PCSign<'a> { impl PCSign<'_> { pub fn new() -> Result { - let hw_info = HardwareInfo::new(1); + let hw_info = HardwareInfo::new(1, None); let timestamp = Utc::now(); let formatted_timestamp = timestamp.format("%Y-%m-%d %H:%M:%S:%3f"); diff --git a/maxima-lib/src/core/background_service_nix.rs b/maxima-lib/src/core/background_service_nix.rs index d383350..b169346 100644 --- a/maxima-lib/src/core/background_service_nix.rs +++ b/maxima-lib/src/core/background_service_nix.rs @@ -25,7 +25,11 @@ pub struct WineInjectArgs { pub path: String, } -pub async fn wine_get_pid(launch_id: &str, name: &str) -> Result { +pub async fn wine_get_pid( + launch_id: &str, + name: &str, + slug: Option<&str>, +) -> Result { debug!("Searching for wine PID for {}", name); let launch_args = WineGetPidArgs { @@ -43,6 +47,7 @@ pub async fn wine_get_pid(launch_id: &str, name: &str) -> Result Result Result<(), NativeError> { +pub async fn request_library_injection( + pid: u32, + path: &str, + slug: Option<&str>, +) -> Result<(), NativeError> { debug!("Injecting {}", path); let launch_args = WineInjectArgs { @@ -81,6 +90,7 @@ pub async fn request_library_injection(pid: u32, path: &str) -> Result<(), Nativ None, false, CommandType::RunInPrefix, + slug, ) .await?; diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 69b6a21..b790406 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -105,20 +105,20 @@ fn home_dir() -> Result { } #[cfg(unix)] -fn home_dir() -> Result { +fn home_dir(slug: Option<&str>) -> Result { use crate::unix::wine::wine_prefix_dir; - Ok(wine_prefix_dir()?.join("drive_c/users/steamuser")) + Ok(wine_prefix_dir(slug)?.join("drive_c/users/steamuser")) } -fn substitute_paths>(path: P) -> Result { +fn substitute_paths>(path: P, slug: Option<&str>) -> Result { let mut result = PathBuf::new(); let path_str = path.as_ref(); if path_str.contains("%Documents%") { - let path = home_dir()?.join("Documents"); + let path = home_dir(slug)?.join("Documents"); result.push(path_str.replace("%Documents%", path.to_str().unwrap_or_default())); } else if path_str.contains("%SavedGames%") { - let path = home_dir()?.join("Saved Games"); + let path = home_dir(slug)?.join("Saved Games"); result.push(path_str.replace("%SavedGames%", path.to_str().unwrap_or_default())); } else { result.push(path_str); @@ -127,9 +127,9 @@ fn substitute_paths>(path: P) -> Result { Ok(result) } -fn unsubstitute_paths>(path: P) -> Result { +fn unsubstitute_paths>(path: P, slug: Option<&str>) -> Result { let path = path.as_ref(); - let home = home_dir()?; + let home = home_dir(slug)?; let documents_path = home.join("Documents"); let saved_games_path = home.join("Saved Games"); @@ -196,6 +196,7 @@ pub struct CloudSyncLock<'a> { manifest: CloudSyncManifest, mode: CloudSyncLockMode, allowed_files: Vec, + slug: String, } impl<'a> CloudSyncLock<'a> { @@ -206,6 +207,7 @@ impl<'a> CloudSyncLock<'a> { lock: String, mode: CloudSyncLockMode, allowed_files: Vec, + slug: String, ) -> Result { let res = client.get(manifest_url).send().await?; @@ -237,6 +239,7 @@ impl<'a> CloudSyncLock<'a> { manifest, mode, allowed_files, + slug, }) } @@ -273,7 +276,7 @@ impl<'a> CloudSyncLock<'a> { let mut paths = HashMap::new(); for i in 0..self.manifest.file.len() { let local_path = &self.manifest.file[i].local_name; - let path = substitute_paths(local_path)?; + let path = substitute_paths(local_path, Some(&self.slug))?; let file = OpenOptions::new().read(true).open(path.clone()).await; @@ -409,7 +412,7 @@ impl<'a> CloudSyncLock<'a> { continue; } - let name = unsubstitute_paths(&path)?; + let name = unsubstitute_paths(&path, Some(&self.slug))?; let write_data = WriteData::File { name, file, @@ -558,11 +561,12 @@ impl CloudSyncClient { offer.offer().multiplayer_id().as_ref().unwrap() ); + let slug = offer.slug().to_string(); let mut allowed_files = Vec::new(); if let Some(config) = offer.offer().cloud_save_configuration_override() { let criteria: CloudSyncSaveFileCriteria = quick_xml::de::from_str(config)?; for include in criteria.include { - let path = substitute_paths(include.value)?; + let path = substitute_paths(include.value, Some(&slug))?; let paths = glob::glob(path.safe_str()?)?; for path in paths { let path = path?; @@ -577,7 +581,7 @@ impl CloudSyncClient { return Err(CloudSyncError::NoConfig(offer.offer_id().clone())); } - Ok(self.obtain_lock_raw(&id, mode, allowed_files).await?) + Ok(self.obtain_lock_raw(&id, mode, allowed_files, slug).await?) } pub async fn obtain_lock_raw<'a>( @@ -585,6 +589,7 @@ impl CloudSyncClient { id: &str, mode: CloudSyncLockMode, allowed_files: Vec, + slug: String, ) -> Result { let (token, user_id) = acquire_auth(&self.auth).await?; @@ -612,6 +617,7 @@ impl CloudSyncClient { lock, mode, allowed_files, + slug, ) .await?) } diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index e6c183d..c95a0f9 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -114,6 +114,7 @@ pub struct ActiveGameContext { launch_id: String, game_path: String, content_id: String, + slug: Option, offer: Option, mode: LaunchMode, injections: Vec, @@ -131,11 +132,13 @@ impl ActiveGameContext { offer: Option, mode: LaunchMode, process: Child, + slug: Option, ) -> Self { Self { launch_id: launch_id.to_owned(), game_path: game_path.to_owned(), content_id: content_id.to_owned(), + slug, offer, mode, injections: Vec::new(), @@ -158,6 +161,7 @@ impl ActiveGameContext { pub struct BootstrapLaunchArgs { pub path: String, pub args: Vec, + pub slug: String, } impl Display for LaunchMode { @@ -232,8 +236,14 @@ pub async fn start_game( let path = path.safe_str()?; info!("Game path: {}", path); + let slug = if let LaunchMode::Online(ref _offer_id) = mode { + offer.as_ref().map(|o| o.slug().to_owned()) + } else { + None + }; + #[cfg(unix)] - mx_linux_setup().await?; + mx_linux_setup(slug.as_deref()).await?; match mode { LaunchMode::Offline(_) => {} @@ -241,13 +251,19 @@ pub async fn start_game( let auth = LicenseAuth::AccessToken(maxima.access_token().await?); let offer = offer.as_ref().unwrap(); - if needs_license_update(&content_id).await? { + if needs_license_update(&content_id, slug.as_deref()).await? { info!( "Requesting new game license for {}...", offer.offer().display_name() ); - request_and_save_license(&auth, &content_id, path.to_owned().into()).await?; + request_and_save_license( + &auth, + &content_id, + path.to_owned().into(), + slug.as_deref(), + ) + .await?; } else { info!("Existing game license is still valid, not updating"); } @@ -278,8 +294,14 @@ pub async fn start_game( LaunchMode::OnlineOffline(_, ref persona, ref password) => { let auth = LicenseAuth::Direct(persona.to_owned(), password.to_owned()); - if needs_license_update(&content_id).await? { - request_and_save_license(&auth, &content_id, path.to_owned().into()).await?; + if needs_license_update(&content_id, slug.as_deref()).await? { + request_and_save_license( + &auth, + &content_id, + path.to_owned().into(), + slug.as_deref(), + ) + .await?; } else { info!("Existing game license is still valid, not updating"); } @@ -303,6 +325,7 @@ pub async fn start_game( let bootstrap_args = BootstrapLaunchArgs { path: path.to_string(), args: game_args, + slug: slug.clone().unwrap_or_default(), }; let b64 = general_purpose::STANDARD.encode(serde_json::to_string(&bootstrap_args)?); @@ -373,6 +396,7 @@ pub async fn start_game( offer, mode, child, + slug, )); Ok(()) @@ -399,13 +423,13 @@ async fn request_opaque_ooa_token(access_token: &str) -> Result Result<(), NativeError> { +pub async fn mx_linux_setup(slug: Option<&str>) -> Result<(), NativeError> { use crate::unix::wine::{ check_runtime_validity, check_wine_validity, get_lutris_runtimes, install_runtime, install_wine, run_wine_command, setup_wine_registry, wine_prefix_dir, CommandType, }; - std::fs::create_dir_all(wine_prefix_dir()?)?; + std::fs::create_dir_all(wine_prefix_dir(slug)?)?; info!("Verifying wine dependencies..."); let skip = std::env::var("MAXIMA_DISABLE_WINE_VERIFICATION").is_ok(); @@ -431,10 +455,11 @@ pub async fn mx_linux_setup() -> Result<(), NativeError> { None, false, CommandType::Run, + slug, ) .await; info!("Setting up wine registry..."); - setup_wine_registry().await?; + setup_wine_registry(slug).await?; Ok(()) } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 8779694..e52dafb 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -89,6 +89,7 @@ impl OwnedOffer { .install_check_override() .as_ref() .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, + Some(&self.slug), ) .await? .safe_str()? @@ -108,7 +109,7 @@ impl OwnedOffer { }; if let Some(path) = path { - Ok(parse_registry_path(path).await?) + Ok(parse_registry_path(path, Some(&self.slug)).await?) } else { Err(LibraryError::NoPath(self.slug.clone())) } @@ -151,6 +152,7 @@ impl OwnedOffer { .install_check_override() .as_ref() .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, + Some(&self.slug), ) .await? .safe_str()? diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index b8da5ce..001b042 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -191,7 +191,11 @@ impl DiPManifest { } #[cfg(unix)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup( + &self, + install_path: &PathBuf, + slug: &str, + ) -> Result<(), ManifestError> { use crate::{ core::launch::mx_linux_setup, unix::{ @@ -200,7 +204,7 @@ impl DiPManifest { }, }; - mx_linux_setup().await?; + mx_linux_setup(Some(slug)).await?; let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, @@ -208,14 +212,18 @@ impl DiPManifest { let args = self.collect_touchup_args(&install_path)?; let path = install_path.join(&self.touchup.path()); let path = case_insensitive_path(path); - run_wine_command(path, Some(args), None, true, CommandType::Run).await?; + run_wine_command(path, Some(args), None, true, CommandType::Run, Some(slug)).await?; invalidate_mx_wine_registry().await; Ok(()) } #[cfg(windows)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup( + &self, + install_path: &PathBuf, + _slug: &str, + ) -> Result<(), ManifestError> { use crate::util::native::NativeError; use tokio::process::Command; diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index 42fa80f..b177216 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -33,14 +33,14 @@ pub const MANIFEST_RELATIVE_PATH: &str = "__Installer/installerdata.xml"; #[async_trait::async_trait] pub trait GameManifest: Send + std::fmt::Debug { - async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError>; + async fn run_touchup(&self, install_path: &PathBuf, slug: &str) -> Result<(), ManifestError>; fn execute_path(&self, trial: bool) -> Option; fn version(&self) -> Option; } #[async_trait::async_trait] impl GameManifest for DiPManifest { - async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { - self.run_touchup(install_path).await + async fn run_touchup(&self, install_path: &PathBuf, slug: &str) -> Result<(), ManifestError> { + self.run_touchup(install_path, slug).await } fn execute_path(&self, trial: bool) -> Option { @@ -54,8 +54,8 @@ impl GameManifest for DiPManifest { #[async_trait::async_trait] impl GameManifest for PreDiPManifest { - async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { - self.run_touchup(install_path).await + async fn run_touchup(&self, install_path: &PathBuf, slug: &str) -> Result<(), ManifestError> { + self.run_touchup(install_path, slug).await } fn execute_path(&self, _: bool) -> Option { diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 0ec9e1b..1f71c97 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -106,7 +106,11 @@ impl PreDiPManifest { } #[cfg(unix)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup( + &self, + install_path: &PathBuf, + slug: &str, + ) -> Result<(), ManifestError> { use crate::{ core::launch::mx_linux_setup, unix::{ @@ -115,7 +119,7 @@ impl PreDiPManifest { }, }; - mx_linux_setup().await?; + mx_linux_setup(Some(slug)).await?; let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, @@ -124,14 +128,18 @@ impl PreDiPManifest { let path = install_path.join(remove_leading_slash(&self.executable.file_path)); let path = case_insensitive_path(path); - run_wine_command(path, Some(args), None, true, CommandType::Run).await?; + run_wine_command(path, Some(args), None, true, CommandType::Run, Some(slug)).await?; invalidate_mx_wine_registry().await; Ok(()) } #[cfg(windows)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup( + &self, + install_path: &PathBuf, + _slug: &str, + ) -> Result<(), ManifestError> { use crate::util::native::NativeError; use tokio::process::Command; diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index ec7b683..e9b837e 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -183,15 +183,23 @@ pub fn get_os_pid(context: &ActiveGameContext) -> Result { } #[cfg(target_os = "windows")] -pub async fn get_wine_pid(_launch_id: &str, _name: &str) -> Result { +pub async fn get_wine_pid( + _launch_id: &str, + _name: &str, + _slug: Option<&str>, +) -> Result { Ok(0) } #[cfg(target_os = "linux")] -pub async fn get_wine_pid(launch_id: &str, name: &str) -> Result { +pub async fn get_wine_pid( + launch_id: &str, + name: &str, + slug: Option<&str>, +) -> Result { use crate::core::background_service::wine_get_pid; - wine_get_pid(launch_id, name).await + wine_get_pid(launch_id, name, slug).await } pub struct Connection { @@ -236,7 +244,8 @@ impl Connection { .ok_or(NativeError::Stringify)? .to_owned(); - pid = get_wine_pid(&context.launch_id(), &filename).await; + pid = get_wine_pid(&context.launch_id(), &filename, context.slug().as_deref()) + .await; } else { warn!( "Failed to find game process while looking for PID {}", diff --git a/maxima-lib/src/lsx/request/license.rs b/maxima-lib/src/lsx/request/license.rs index 757bda9..569a44c 100644 --- a/maxima-lib/src/lsx/request/license.rs +++ b/maxima-lib/src/lsx/request/license.rs @@ -28,6 +28,7 @@ pub async fn handle_license_request( let playing = maxima.playing().as_ref().unwrap(); let content_id = playing.content_id().to_owned(); let mode = playing.mode(); + let slug = playing.slug().clone(); let auth = match mode { LaunchMode::Offline(_) => { @@ -40,7 +41,7 @@ pub async fn handle_license_request( }; // TODO: how to get version - let hw_info = HardwareInfo::new(2); + let hw_info = HardwareInfo::new(2, slug.as_deref()); let license = request_license( &content_id, &hw_info.generate_hardware_hash(), diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index c61cf76..b621981 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -172,8 +172,11 @@ pub enum LicenseAuth { Direct(String, String), } -pub async fn needs_license_update(content_id: &str) -> Result { - let path = get_license_dir()?.join(format!("{}.dlf", content_id)); +pub async fn needs_license_update( + content_id: &str, + slug: Option<&str>, +) -> Result { + let path = get_license_dir(slug)?.join(format!("{}.dlf", content_id)); if !path.exists() { return Ok(true); } @@ -200,6 +203,7 @@ pub async fn request_and_save_license( auth: &LicenseAuth, content_id: &str, mut game_path: PathBuf, + slug: Option<&str>, ) -> Result<(), LicenseError> { if game_path.is_file() { game_path = game_path.safe_parent()?.to_path_buf(); @@ -213,7 +217,7 @@ pub async fn request_and_save_license( let version = detect_ooa_version(game_path).await.unwrap_or(1); debug!("OOA version is {version}"); - let hw_info = HardwareInfo::new(version); + let hw_info = HardwareInfo::new(version, slug); let license = request_license( content_id, &hw_info.generate_hardware_hash(), @@ -222,7 +226,7 @@ pub async fn request_and_save_license( None, ) .await?; - save_licenses(&license, state).await?; + save_licenses(&license, state, slug).await?; Ok(()) } @@ -337,8 +341,12 @@ pub async fn save_license( Ok(()) } -pub async fn save_licenses(license: &License, state: OOAState) -> Result<(), LicenseError> { - let path = get_license_dir()?; +pub async fn save_licenses( + license: &License, + state: OOAState, + slug: Option<&str>, +) -> Result<(), LicenseError> { + let path = get_license_dir(slug)?; debug!("Saving the license {license:#?}"); save_license( @@ -366,12 +374,12 @@ pub fn get_license_dir() -> Result { } #[cfg(unix)] -pub fn get_license_dir() -> Result { +pub fn get_license_dir(slug: Option<&str>) -> Result { use crate::unix::wine::wine_prefix_dir; let path = format!( "{}/drive_c/{}", - wine_prefix_dir()?.safe_str()?, + wine_prefix_dir(slug)?.safe_str()?, LICENSE_PATH.to_string() ); create_dir_all(&path)?; diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index ba6c5d9..14e7afb 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -71,12 +71,14 @@ struct Versions { } /// Returns internal prtoton pfx path -pub fn wine_prefix_dir() -> Result { - if let Ok(path) = env::var("MAXIMA_WINE_PREFIX") { - return Ok(PathBuf::from(path)); - } +pub fn wine_prefix_dir(slug: Option<&str>) -> Result { + let base = maxima_dir()?.join("wine/prefix"); - Ok(maxima_dir()?.join("wine/prefix")) + if let Some(slug) = slug { + Ok(base.join(slug)) + } else { + Ok(base) + } } pub fn proton_dir() -> Result { @@ -255,9 +257,10 @@ async fn run_wine_command_umu, T: AsRef>( cwd: Option, want_output: bool, command_type: CommandType, + slug: Option<&str>, ) -> Result { let proton_path = proton_dir()?; - let proton_prefix_path = wine_prefix_dir()?; + let proton_prefix_path = wine_prefix_dir(slug)?; let eac_path = eac_dir()?; let umu_bin = umu_bin()?; @@ -326,6 +329,7 @@ async fn run_wine_command_slr, T: AsRef>( cwd: Option, want_output: bool, command_type: CommandType, + slug: Option<&str>, ) -> Result { let slr_path = env::var("MAXIMA_SLR_PATH").map_err(|_| NativeError::Wine(WineError::MissingSLRPath))?; @@ -335,7 +339,7 @@ async fn run_wine_command_slr, T: AsRef>( .join("proton") .to_string_lossy() .to_string(); - let proton_prefix_path = wine_prefix_dir()?; + let proton_prefix_path = wine_prefix_dir(slug)?; // Get the Steam client install path, defaulting to common location let steam_client_path = env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH").unwrap_or_else(|_| { @@ -418,22 +422,20 @@ pub async fn run_wine_command, T: AsRef>( cwd: Option, want_output: bool, command_type: CommandType, + slug: Option<&str>, ) -> Result { - // Check if using Steam Linux Runtime let use_slr = env::var("MAXIMA_USE_SLR").is_ok(); if use_slr { - run_wine_command_slr(arg, args, cwd, want_output, command_type).await + run_wine_command_slr(arg, args, cwd, want_output, command_type, slug).await } else { - run_wine_command_umu(arg, args, cwd, want_output, command_type).await + run_wine_command_umu(arg, args, cwd, want_output, command_type, slug).await } } pub(crate) async fn install_wine() -> Result<(), NativeError> { - // Skip installation if using custom Proton path if env::var("MAXIMA_PROTON_PATH").is_ok() { info!("Using custom Proton path, skipping Proton-GE installation"); - let _ = run_wine_command("", None::<[&str; 0]>, None, false, CommandType::Run).await; return Ok(()); } @@ -462,8 +464,6 @@ pub(crate) async fn install_wine() -> Result<(), NativeError> { warn!("Failed to delete {:?} - {:?}", path, err); } - let _ = run_wine_command("", None::<[&str; 0]>, None, false, CommandType::Run).await; - Ok(()) } @@ -508,7 +508,7 @@ fn extract_archive( Ok(()) } -pub async fn setup_wine_registry() -> Result<(), NativeError> { +pub async fn setup_wine_registry(slug: Option<&str>) -> Result<(), NativeError> { let mut reg_content = "Windows Registry Editor Version 5.00\n\n".to_string(); // This supports text values only at the moment // if you need a dword - implement it @@ -559,6 +559,7 @@ pub async fn setup_wine_registry() -> Result<(), NativeError> { None, false, CommandType::Run, + slug, ) .await?; @@ -607,8 +608,8 @@ async fn parse_wine_registry(file_path: &str) -> WineRegistry { registry_map.clone() } -pub async fn parse_mx_wine_registry() -> Result { - let path = wine_prefix_dir()?.join("pfx").join("system.reg"); +pub async fn parse_mx_wine_registry(slug: Option<&str>) -> Result { + let path = wine_prefix_dir(slug)?.join("pfx").join("system.reg"); if !path.exists() { return Ok(HashMap::new()); } @@ -631,8 +632,11 @@ fn normalize_key(key: &str) -> String { } } -pub async fn get_mx_wine_registry_value(query_key: &str) -> Result, RegistryError> { - let registry_map = parse_mx_wine_registry().await?; +pub async fn get_mx_wine_registry_value( + query_key: &str, + slug: Option<&str>, +) -> Result, RegistryError> { + let registry_map = parse_mx_wine_registry(slug).await?; let normalized_query_key = normalize_key(query_key); let value = if let Some(value) = registry_map.get(&normalized_query_key) { diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 624fc0d..b61a646 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -103,7 +103,7 @@ pub fn check_registry_validity() -> Result<(), RegistryError> { } #[cfg(windows)] -async fn read_reg_key(path: &str) -> Result, RegistryError> { +async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, RegistryError> { if let (Some(hkey_segment), Some(value_segment)) = (path.find('\\'), path.rfind('\\')) { let sub_key = &path[(hkey_segment + 1)..value_segment]; let value_name = &path[(value_segment + 1)..]; @@ -176,18 +176,18 @@ async fn read_reg_key(path: &str) -> Result, RegistryError> { } #[cfg(unix)] -async fn read_reg_key(path: &str) -> Result, RegistryError> { +async fn read_reg_key(path: &str, slug: Option<&str>) -> Result, RegistryError> { use crate::unix::wine::get_mx_wine_registry_value; - Ok(get_mx_wine_registry_value(path).await?) + Ok(get_mx_wine_registry_value(path, slug).await?) } -pub async fn parse_registry_path(key: &str) -> Result { +pub async fn parse_registry_path(key: &str, slug: Option<&str>) -> Result { let mut parts = key .split(|c| c == '[' || c == ']') .filter(|s| !s.is_empty()); let path = if let (Some(first), Some(second)) = (parts.next(), parts.next()) { - let path = match read_reg_key(first).await? { + let path = match read_reg_key(first, slug).await? { Some(path) => path.replace("\\", "/").replace("//", "/"), None => return Ok(PathBuf::from(key.to_owned())), }; @@ -205,13 +205,16 @@ pub async fn parse_registry_path(key: &str) -> Result { Ok(path) } -pub async fn parse_partial_registry_path(key: &str) -> Result { +pub async fn parse_partial_registry_path( + key: &str, + slug: Option<&str>, +) -> Result { let mut parts = key .split(|c| c == '[' || c == ']') .filter(|s| !s.is_empty()); let path = if let (Some(first), Some(_second)) = (parts.next(), parts.next()) { - let path = match read_reg_key(first).await? { + let path = match read_reg_key(first, slug).await? { Some(path) => path.replace("\\", "/"), None => return Ok(PathBuf::from(key.to_owned())), }; diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 7c8f6bf..2e686c8 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -82,8 +82,8 @@ pub enum MaximaLibRequest { GetFriendsRequest, GetGameDetailsRequest(String), StartGameRequest(GameInfo, Option), - InstallGameRequest(String, PathBuf), - LocateGameRequest(String), + InstallGameRequest(String, String, PathBuf), + LocateGameRequest(String, String), /// Persist UI-side game settings into the core GameSettingsManager SaveGameSettings(String, GameSettings), ShutdownRequest, @@ -453,9 +453,9 @@ impl BridgeThread { let context = ctx.clone(); async move { game_details_request(maxima, slug.clone(), channel, &context).await }.await } - MaximaLibRequest::LocateGameRequest(path) => { + MaximaLibRequest::LocateGameRequest(slug, path) => { #[cfg(unix)] - maxima::core::launch::mx_linux_setup().await?; + maxima::core::launch::mx_linux_setup(Some(&slug)).await?; let mut path = path; if path.ends_with("/") || path.ends_with("\\") { path.remove(path.len() - 1); @@ -463,7 +463,7 @@ impl BridgeThread { let path = PathBuf::from(path); let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await; if let Ok(manifest) = manifest { - let guh = manifest.run_touchup(&path).await; + let guh = manifest.run_touchup(&path, &slug).await; if let Err(err) = guh { let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse( InteractThreadLocateGameResponse::Error( @@ -500,7 +500,7 @@ impl BridgeThread { ctx.request_repaint(); Ok(()) } - MaximaLibRequest::InstallGameRequest(offer, path) => { + MaximaLibRequest::InstallGameRequest(offer, slug, path) => { let mut maxima = maxima_arc.lock().await; let builds = maxima.content_manager().service().available_builds(&offer).await?; @@ -511,9 +511,10 @@ impl BridgeThread { }; let game = QueuedGameBuilder::default() - .offer_id(offer) + .offer_id(offer.clone()) .build_id(build.build_id().to_owned()) .path(path.to_owned()) + .slug(slug.to_owned()) .build()?; Ok(maxima.content_manager().add_install(game).await?) } diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 0971aff..0359736 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -850,7 +850,7 @@ impl MaximaEguiApp { ui.add_enabled_ui(PathBuf::from(&self.installer_state.locate_path).exists(), |ui| { if ui.add_sized(button_size, egui::Button::new(&self.locale.localization.modals.game_install.locate_action.to_ascii_uppercase())).clicked() { - self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::LocateGameRequest(self.installer_state.locate_path.clone())).unwrap(); + self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::LocateGameRequest(slug.clone(), self.installer_state.locate_path.clone())).unwrap(); self.installer_state.locating = true; } }); @@ -875,7 +875,7 @@ impl MaximaEguiApp { } else { self.install_queue.insert(game.offer.clone(),QueuedDownload { slug: game.slug.clone(), offer: game.offer.clone(), downloaded_bytes: 0, total_bytes: 0 }); } - self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), path.join(slug))).unwrap(); + self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), slug.clone(), path.join(slug))).unwrap(); clear = true; } From 42ae6c96d5db45ae47234d03199a4cca68d989b7 Mon Sep 17 00:00:00 2001 From: sjp761 Date: Wed, 4 Feb 2026 15:31:23 -0600 Subject: [PATCH 10/17] Revert "Ability to use SLR and custom proton (could be very much refined)" --- maxima-lib/src/core/launch.rs | 17 +---- maxima-lib/src/unix/wine.rs | 135 +--------------------------------- maxima-lib/src/util/native.rs | 6 -- 3 files changed, 6 insertions(+), 152 deletions(-) diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index c95a0f9..f99bb2f 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -438,27 +438,14 @@ pub async fn mx_linux_setup(slug: Option<&str>) -> Result<(), NativeError> { install_wine().await?; } let runtimes = get_lutris_runtimes().await?; - if !check_runtime_validity("eac_runtime", &runtimes).await? - && !std::env::var("MAXIMA_DISABLE_EAC").is_ok() - { + if !check_runtime_validity("eac_runtime", &runtimes).await? { install_runtime("eac_runtime", &runtimes).await?; } - let use_slr = std::env::var("MAXIMA_USE_SLR").is_ok(); - if !check_runtime_validity("umu", &runtimes).await? && !use_slr { + if !check_runtime_validity("umu", &runtimes).await? { install_runtime("umu", &runtimes).await?; } } - let _ = run_wine_command( - "wineboot", - Some(vec!["--init"]), - None, - false, - CommandType::Run, - slug, - ) - .await; - info!("Setting up wine registry..."); setup_wine_registry(slug).await?; Ok(()) diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 14e7afb..9383258 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -23,7 +23,7 @@ use tokio::{ use xz2::read::XzDecoder; use crate::util::{ - github::{fetch_github_releases, github_download_asset, GithubRelease}, + github::{fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease}, native::{maxima_dir, DownloadError, NativeError, SafeParent, SafeStr, WineError}, registry::RegistryError, }; @@ -82,10 +82,6 @@ pub fn wine_prefix_dir(slug: Option<&str>) -> Result { } pub fn proton_dir() -> Result { - if let Ok(path) = env::var("MAXIMA_PROTON_PATH") { - return Ok(PathBuf::from(path)); - } - Ok(maxima_dir()?.join("wine/proton")) } @@ -118,12 +114,6 @@ fn set_versions(versions: Versions) -> Result<(), NativeError> { } pub(crate) async fn check_wine_validity() -> Result { - // Skip check if using custom Proton path - if env::var("MAXIMA_PROTON_PATH").is_ok() { - info!("Using custom Proton path, skipping validity check"); - return Ok(true); - } - if !proton_dir()?.exists() { return Ok(false); } @@ -250,8 +240,7 @@ fn get_wine_release() -> Result { release.ok_or(WineError::Fetch) } -/// Run a wine command using UMU launcher -async fn run_wine_command_umu, T: AsRef>( +pub async fn run_wine_command, T: AsRef>( arg: T, args: Option, cwd: Option, @@ -270,10 +259,10 @@ async fn run_wine_command_umu, T: AsRef>( // Create command with all necessary wine env variables let mut binding = Command::new(wine_path.clone()); let mut child = binding - .env("WINEPREFIX", &proton_prefix_path) + .env("WINEPREFIX", proton_prefix_path) .env("GAMEID", "umu-0") .env("PROTON_VERB", &command_type.to_string()) - .env("PROTONPATH", &proton_path) + .env("PROTONPATH", proton_path) .env("STORE", "ea") .env("PROTON_EAC_RUNTIME", eac_path) .env("UMU_ZENITY", "1") @@ -322,123 +311,7 @@ async fn run_wine_command_umu, T: AsRef>( Ok(output_str.to_string()) } -/// Run a wine command using Steam Linux Runtime -async fn run_wine_command_slr, T: AsRef>( - arg: T, - args: Option, - cwd: Option, - want_output: bool, - command_type: CommandType, - slug: Option<&str>, -) -> Result { - let slr_path = - env::var("MAXIMA_SLR_PATH").map_err(|_| NativeError::Wine(WineError::MissingSLRPath))?; - let proton_dir_path = env::var("MAXIMA_PROTON_PATH") - .map_err(|_| NativeError::Wine(WineError::MissingProtonPath))?; - let proton_exe = PathBuf::from(&proton_dir_path) - .join("proton") - .to_string_lossy() - .to_string(); - let proton_prefix_path = wine_prefix_dir(slug)?; - - // Get the Steam client install path, defaulting to common location - let steam_client_path = env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH").unwrap_or_else(|_| { - env::var("HOME") - .map(|h| format!("{}/.steam/steam", h)) - .unwrap_or_else(|_| "/home/user/.steam/steam".to_string()) - }); - - // Build the SLR entry point path - let slr_entry_point = PathBuf::from(&slr_path).join("_v2-entry-point"); - - if !slr_entry_point.exists() { - return Err(NativeError::Wine(WineError::SLRNotFound(slr_entry_point))); - } - - // Build proton command with verb passed to _v2-entry-point - let mut proton_args = vec![proton_exe.clone(), "run".to_string()]; - proton_args.push(arg.as_ref().to_string_lossy().to_string()); - - if let Some(arguments) = args { - for a in arguments { - proton_args.push(a.as_ref().to_string_lossy().to_string()); - } - } - - let slr_verb = format!("--verb={}", command_type.to_string()); - - let mut binding = Command::new(slr_entry_point); - let mut child = binding - .env("WINEPREFIX", &proton_prefix_path) - .env("STEAM_COMPAT_DATA_PATH", &proton_prefix_path) - .env("STEAM_COMPAT_CLIENT_INSTALL_PATH", &steam_client_path) - .env("SteamAppId", "0") - .env("STEAM_COMPAT_APP_ID", "0") - .env("SteamGameId", "0") - .env("WINEDEBUG", "fixme-all") - .env("LD_PRELOAD", "") - .arg(&slr_verb) - .arg("--") - .args(proton_args); - - // Hardcode compat install path until dynamic wiring is added; still honor cwd for working dir - child = child.env( - "STEAM_COMPAT_INSTALL_PATH", - "/mnt/games/Games/mass-effect-legendary-edition", - ); - - if let Some(ref dir) = cwd { - child = child.current_dir(dir); - } - - let status: ExitStatus; - let mut output_str = String::new(); - - if want_output { - let output = child - .stdout(Stdio::piped()) - .spawn()? - .wait_with_output() - .await?; - output_str = String::from_utf8_lossy(&output.stdout).to_string(); - status = output.status; - } else { - status = child.spawn()?.wait().await?; - }; - - if !status.success() { - return Err(NativeError::Wine(WineError::Command { - output: output_str, - exit: status, - })); - } - - Ok(output_str.to_string()) -} - -pub async fn run_wine_command, T: AsRef>( - arg: T, - args: Option, - cwd: Option, - want_output: bool, - command_type: CommandType, - slug: Option<&str>, -) -> Result { - let use_slr = env::var("MAXIMA_USE_SLR").is_ok(); - - if use_slr { - run_wine_command_slr(arg, args, cwd, want_output, command_type, slug).await - } else { - run_wine_command_umu(arg, args, cwd, want_output, command_type, slug).await - } -} - pub(crate) async fn install_wine() -> Result<(), NativeError> { - if env::var("MAXIMA_PROTON_PATH").is_ok() { - info!("Using custom Proton path, skipping Proton-GE installation"); - return Ok(()); - } - let release = get_wine_release()?; let asset = match release .assets diff --git a/maxima-lib/src/util/native.rs b/maxima-lib/src/util/native.rs index f8d8f35..5adfc60 100644 --- a/maxima-lib/src/util/native.rs +++ b/maxima-lib/src/util/native.rs @@ -58,12 +58,6 @@ pub enum WineError { UnimplementedRuntime(String), #[error("couldn't find suitable wine release")] Fetch, - #[error("MAXIMA_SLR_PATH environment variable must be set when using SLR")] - MissingSLRPath, - #[error("MAXIMA_PROTON_PATH environment variable must be set when using SLR")] - MissingProtonPath, - #[error("Steam Linux Runtime entry point not found at: {0}")] - SLRNotFound(PathBuf), } pub trait SafeParent { fn safe_parent(&self) -> Result<&Path, NativeError>; From c1b47562a39759ce85f5ac3a44c3651af55e3a9a Mon Sep 17 00:00:00 2001 From: sjp761 Date: Thu, 5 Feb 2026 11:37:01 -0600 Subject: [PATCH 11/17] Use slug instead of offer id for exclusion file --- maxima-lib/src/content/exclusion.rs | 8 +++----- maxima-lib/src/content/manager.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/maxima-lib/src/content/exclusion.rs b/maxima-lib/src/content/exclusion.rs index 9142939..0094ce1 100644 --- a/maxima-lib/src/content/exclusion.rs +++ b/maxima-lib/src/content/exclusion.rs @@ -4,18 +4,16 @@ use log::{error, info, warn}; use std::fs::File; use std::io::{BufRead, BufReader}; -pub fn get_exclusion_list(offer_id: String) -> GlobSet { +pub fn get_exclusion_list(slug: String) -> GlobSet { let mut builder = GlobSetBuilder::new(); if let Ok(dir) = maxima_dir() // Checks to make sure maxima directory exists { - let filepath = dir.join("exclude").join(&offer_id); // Path to exclusion file + let filepath = dir.join("exclude").join(&slug); // Path to exclusion file info!("Loading exclusion file from {}", filepath.display()); - if let Ok(file) = File::open(&filepath) - // Opens the exclusion file, fails if not found - { + if let Ok(file) = File::open(&filepath) { let reader = BufReader::new(file); for line in reader.lines().flatten() { let entry = line.trim(); diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 4f29ced..84bdf27 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -155,7 +155,7 @@ impl GameDownloader { let mut entries = Vec::new(); - let exclusion_list = get_exclusion_list(game.offer_id.clone()); + let exclusion_list = get_exclusion_list(game.slug.clone()); for ele in downloader.manifest().entries() { // TODO: Filtering From 0aba2e1df70b624ece31af5b9cf48e1232fc7639 Mon Sep 17 00:00:00 2001 From: sjp761 Date: Thu, 5 Feb 2026 11:41:59 -0600 Subject: [PATCH 12/17] Use string slice instead of String for exclusion --- maxima-lib/src/content/exclusion.rs | 2 +- maxima-lib/src/content/manager.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/maxima-lib/src/content/exclusion.rs b/maxima-lib/src/content/exclusion.rs index 0094ce1..a47f6d5 100644 --- a/maxima-lib/src/content/exclusion.rs +++ b/maxima-lib/src/content/exclusion.rs @@ -4,7 +4,7 @@ use log::{error, info, warn}; use std::fs::File; use std::io::{BufRead, BufReader}; -pub fn get_exclusion_list(slug: String) -> GlobSet { +pub fn get_exclusion_list(slug: &str) -> GlobSet { let mut builder = GlobSetBuilder::new(); if let Ok(dir) = maxima_dir() diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 84bdf27..1c542e4 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -155,12 +155,12 @@ impl GameDownloader { let mut entries = Vec::new(); - let exclusion_list = get_exclusion_list(game.slug.clone()); + let exclusion_list = get_exclusion_list(game.slug.as_str()); for ele in downloader.manifest().entries() { // TODO: Filtering if exclusion_list.is_match(&ele.name()) { - info!("Excluding file from download: {}", ele.name()); + // info!("Excluding file from download: {}", ele.name()); Spams if a lot of files are excluded continue; } entries.push(ele.clone()); From 7f0aa7324230bc541724451633dc674463435004 Mon Sep 17 00:00:00 2001 From: sjp761 Date: Thu, 5 Feb 2026 12:13:10 -0600 Subject: [PATCH 13/17] Correct more String parameters to &str --- maxima-lib/src/content/manager.rs | 4 ++-- maxima-lib/src/core/cloudsync.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 1c542e4..f79754b 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -200,7 +200,7 @@ impl GameDownloader { cancel_token, completed_bytes, notify, - slug, + &slug, ) .await; if let Err(err) = dl { @@ -234,7 +234,7 @@ impl GameDownloader { cancel_token: CancellationToken, completed_bytes: Arc, notify: Arc, - slug: String, + slug: &str, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(total_count); diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index b790406..3c048f9 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -207,7 +207,7 @@ impl<'a> CloudSyncLock<'a> { lock: String, mode: CloudSyncLockMode, allowed_files: Vec, - slug: String, + slug: &str, ) -> Result { let res = client.get(manifest_url).send().await?; @@ -239,7 +239,7 @@ impl<'a> CloudSyncLock<'a> { manifest, mode, allowed_files, - slug, + slug: slug.to_owned(), }) } @@ -581,7 +581,7 @@ impl CloudSyncClient { return Err(CloudSyncError::NoConfig(offer.offer_id().clone())); } - Ok(self.obtain_lock_raw(&id, mode, allowed_files, slug).await?) + Ok(self.obtain_lock_raw(&id, mode, allowed_files, &slug).await?) } pub async fn obtain_lock_raw<'a>( @@ -589,7 +589,7 @@ impl CloudSyncClient { id: &str, mode: CloudSyncLockMode, allowed_files: Vec, - slug: String, + slug: &str, ) -> Result { let (token, user_id) = acquire_auth(&self.auth).await?; From 9d3dabec08bc21cf7043206f70ad59d1ebd0f036 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:07:47 -0600 Subject: [PATCH 14/17] Implement universal hashmap to for custom prefix location (read from game settings json) --- maxima-lib/src/core/mod.rs | 6 ++++++ maxima-lib/src/gamesettings/mod.rs | 9 ++++++--- maxima-lib/src/unix/wine.rs | 15 ++++++++++----- maxima-ui/src/bridge/get_games.rs | 13 +++++++------ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 2109460..1f99b08 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -42,6 +42,8 @@ use derive_getters::Getters; use log::{error, info, warn}; use strum_macros::IntoStaticStr; +use lazy_static::lazy_static; +use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; use tokio::sync::Mutex; @@ -143,6 +145,10 @@ pub enum MaximaCreationError { pub type LockedMaxima = Arc>; +lazy_static! { + pub static ref GamePrefixMap: std::sync::Mutex> = std::sync::Mutex::new(HashMap::new()); +} + impl Maxima { pub async fn new_with_options( options: MaximaOptions, diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index dc86912..4f7e0a3 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::util::native::maxima_dir; +use crate::{core::GamePrefixMap, util::native::maxima_dir}; use log::info; use serde::{Deserialize, Serialize}; use serde_json; @@ -120,8 +120,11 @@ impl GameSettingsManager { .unwrap_or_else(|| GameSettings::new_with_slug(slug)) } - pub fn save(&mut self, slug: &str, settings: GameSettings) { + pub async fn save(&mut self, slug: &str, settings: GameSettings) { save_game_settings(slug, &settings); - self.settings.insert(slug.to_string(), settings); + self.settings.insert(slug.to_string(), settings.clone()); + GamePrefixMap.lock() + .unwrap() + .insert(slug.to_string(), settings.wine_prefix().to_string().into()); } } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 9383258..9b391a1 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -22,11 +22,11 @@ use tokio::{ }; use xz2::read::XzDecoder; -use crate::util::{ - github::{fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease}, - native::{maxima_dir, DownloadError, NativeError, SafeParent, SafeStr, WineError}, +use crate::{core::GamePrefixMap, util::{ + github::{GithubRelease, fetch_github_release, fetch_github_releases, github_download_asset}, + native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, registry::RegistryError, -}; +}}; lazy_static! { static ref PROTON_PATTERN: Regex = Regex::new(r"GE-Proton\d+-\d+\.tar\.gz").unwrap(); @@ -72,8 +72,12 @@ struct Versions { /// Returns internal prtoton pfx path pub fn wine_prefix_dir(slug: Option<&str>) -> Result { + if let Some(slug) = slug { + if let Some(prefix) = GamePrefixMap.lock().unwrap().get(slug) { + return Ok(prefix.clone().into()); + } + } let base = maxima_dir()?.join("wine/prefix"); - if let Some(slug) = slug { Ok(base.join(slug)) } else { @@ -253,6 +257,7 @@ pub async fn run_wine_command, T: AsRef>( let eac_path = eac_dir()?; let umu_bin = umu_bin()?; + info!("Wine Prefix: {:?}", proton_prefix_path); let wine_path = env::var("MAXIMA_WINE_COMMAND").unwrap_or_else(|_| umu_bin.to_string_lossy().to_string()); diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 79672b8..01e3841 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -7,12 +7,9 @@ use egui::Context; use log::{debug, error, info}; use maxima::{ core::{ - service_layer::{ - ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, - ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, - SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, - }, - LockedMaxima, + GamePrefixMap, LockedMaxima, service_layer::{ + SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient + } }, gamesettings::get_game_settings, util::native::maxima_dir, @@ -239,6 +236,10 @@ pub async fn get_games_request( // Grab persisted settings from Maxima's GameSettingsManager if available let core_settings = get_game_settings(&slug); game_settings.save(&slug, core_settings.clone()); + GamePrefixMap + .lock() + .unwrap() + .insert(slug.clone(), core_settings.wine_prefix.clone()); let settings = core_settings.clone(); let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info, From 63353e86d0af97eebe34866b07fa5b12857ef0d5 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:08:34 -0600 Subject: [PATCH 15/17] Cargo fmt run --- maxima-lib/src/core/cloudsync.rs | 4 +++- maxima-lib/src/core/mod.rs | 3 ++- maxima-lib/src/gamesettings/mod.rs | 3 ++- maxima-lib/src/unix/wine.rs | 15 ++++++++++----- maxima-ui/src/bridge/get_games.rs | 14 +++++++------- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 3c048f9..396980e 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -581,7 +581,9 @@ impl CloudSyncClient { return Err(CloudSyncError::NoConfig(offer.offer_id().clone())); } - Ok(self.obtain_lock_raw(&id, mode, allowed_files, &slug).await?) + Ok(self + .obtain_lock_raw(&id, mode, allowed_files, &slug) + .await?) } pub async fn obtain_lock_raw<'a>( diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 1f99b08..e8a2896 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -146,7 +146,8 @@ pub enum MaximaCreationError { pub type LockedMaxima = Arc>; lazy_static! { - pub static ref GamePrefixMap: std::sync::Mutex> = std::sync::Mutex::new(HashMap::new()); + pub static ref GamePrefixMap: std::sync::Mutex> = + std::sync::Mutex::new(HashMap::new()); } impl Maxima { diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index 4f7e0a3..bb3e1e1 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -123,7 +123,8 @@ impl GameSettingsManager { pub async fn save(&mut self, slug: &str, settings: GameSettings) { save_game_settings(slug, &settings); self.settings.insert(slug.to_string(), settings.clone()); - GamePrefixMap.lock() + GamePrefixMap + .lock() .unwrap() .insert(slug.to_string(), settings.wine_prefix().to_string().into()); } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 9b391a1..20772c2 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -22,11 +22,16 @@ use tokio::{ }; use xz2::read::XzDecoder; -use crate::{core::GamePrefixMap, util::{ - github::{GithubRelease, fetch_github_release, fetch_github_releases, github_download_asset}, - native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, - registry::RegistryError, -}}; +use crate::{ + core::GamePrefixMap, + util::{ + github::{ + fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease, + }, + native::{maxima_dir, DownloadError, NativeError, SafeParent, SafeStr, WineError}, + registry::RegistryError, + }, +}; lazy_static! { static ref PROTON_PATTERN: Regex = Regex::new(r"GE-Proton\d+-\d+\.tar\.gz").unwrap(); diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 01e3841..4b4f47a 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -7,9 +7,12 @@ use egui::Context; use log::{debug, error, info}; use maxima::{ core::{ - GamePrefixMap, LockedMaxima, service_layer::{ - SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient - } + service_layer::{ + ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, + ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, + SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, + }, + GamePrefixMap, LockedMaxima, }, gamesettings::get_game_settings, util::native::maxima_dir, @@ -236,10 +239,7 @@ pub async fn get_games_request( // Grab persisted settings from Maxima's GameSettingsManager if available let core_settings = get_game_settings(&slug); game_settings.save(&slug, core_settings.clone()); - GamePrefixMap - .lock() - .unwrap() - .insert(slug.clone(), core_settings.wine_prefix.clone()); + GamePrefixMap.lock().unwrap().insert(slug.clone(), core_settings.wine_prefix.clone()); let settings = core_settings.clone(); let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info, From 815b916fd7acf2329443e7dd44369e39ad28b1d5 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:28:47 -0600 Subject: [PATCH 16/17] Fix some warnings --- maxima-cli/src/main.rs | 4 ++-- maxima-lib/src/content/downloader.rs | 2 +- maxima-lib/src/content/manager.rs | 2 -- maxima-lib/src/core/launch.rs | 2 +- maxima-lib/src/core/library.rs | 7 +------ maxima-ui/src/bridge/get_games.rs | 2 -- 6 files changed, 5 insertions(+), 14 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 2f46d97..9be9107 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -667,8 +667,8 @@ async fn get_user_by_id(maxima_arc: LockedMaxima, user_id: &str) -> Result<()> { Ok(()) } -async fn get_game_by_slug(maxima_arc: LockedMaxima, slug: &str) -> Result<()> { - let maxima = maxima_arc.lock().await; +async fn get_game_by_slug(maxima_arc: LockedMaxima, _slug: &str) -> Result<()> { + let _maxima = maxima_arc.lock().await; // match maxima.owned_game_by_slug(slug).await { // Ok(game) => info!("Game: {}", game.id()), diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index e982a19..6f93ad0 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -25,7 +25,7 @@ use bytes::{Buf, BufMut, Bytes, BytesMut}; use derive_getters::Getters; use flate2::bufread::DeflateDecoder as BufreadDeflateDecoder; use futures::{Stream, StreamExt, TryStreamExt}; -use log::{debug, error, info, warn}; +use log::{debug, error, warn}; use reqwest::Client; use strum_macros::Display; use thiserror::Error; diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f79754b..7b3bd9e 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -6,11 +6,9 @@ use std::{ }, }; -use crate::core::LockedMaxima; use derive_builder::Builder; use derive_getters::Getters; use futures::StreamExt; -use globset::GlobSet; use log::{debug, error, info}; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index f99bb2f..4f1644e 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -426,7 +426,7 @@ async fn request_opaque_ooa_token(access_token: &str) -> Result) -> Result<(), NativeError> { use crate::unix::wine::{ check_runtime_validity, check_wine_validity, get_lutris_runtimes, install_runtime, - install_wine, run_wine_command, setup_wine_registry, wine_prefix_dir, CommandType, + install_wine, setup_wine_registry, wine_prefix_dir, }; std::fs::create_dir_all(wine_prefix_dir(slug)?)?; diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index e52dafb..04386c1 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -13,14 +13,9 @@ use super::{ }; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; +use crate::util::native::{maxima_dir, NativeError, SafeStr}; use crate::util::registry::{parse_partial_registry_path, parse_registry_path, RegistryError}; -use crate::{ - core::settings, - gamesettings::GameSettingsManager, - util::native::{maxima_dir, NativeError, SafeStr}, -}; use derive_getters::Getters; -use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 4b4f47a..b6c136c 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -19,8 +19,6 @@ use maxima::{ }; use std::{fs, sync::mpsc::Sender}; -use maxima::gamesettings::{GameSettings, GameSettingsManager}; - fn get_preferred_bg_hero(heroes: &Option) -> Option { let heroes = match heroes { Some(h) => h.items().get(0), From 60e1276e9ef6dc1e536c015a4283b8468a86cd38 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:09:06 -0600 Subject: [PATCH 17/17] Ensure consistency of prefix hashmap --- maxima-lib/src/gamesettings/mod.rs | 12 +++++++++--- maxima-lib/src/unix/wine.rs | 24 +++++++++++++++++++----- maxima-ui/src/bridge/get_games.rs | 5 ++--- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index bb3e1e1..36458dc 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -67,7 +67,13 @@ pub fn get_game_settings(slug: &str) -> GameSettings { Err(_) => return GameSettings::new_with_slug(slug), }; - serde_json::from_str(&content).unwrap_or_else(|_| GameSettings::new_with_slug(slug)) + let game_settings = + serde_json::from_str(&content).unwrap_or_else(|_| GameSettings::new_with_slug(slug)); + GamePrefixMap + .lock() + .unwrap() + .insert(slug.to_string(), game_settings.wine_prefix().to_string()); + game_settings } pub fn save_game_settings(slug: &str, settings: &GameSettings) { @@ -120,12 +126,12 @@ impl GameSettingsManager { .unwrap_or_else(|| GameSettings::new_with_slug(slug)) } - pub async fn save(&mut self, slug: &str, settings: GameSettings) { + pub fn save(&mut self, slug: &str, settings: GameSettings) { save_game_settings(slug, &settings); self.settings.insert(slug.to_string(), settings.clone()); GamePrefixMap .lock() .unwrap() - .insert(slug.to_string(), settings.wine_prefix().to_string().into()); + .insert(slug.to_string(), settings.wine_prefix().to_string()); } } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 20772c2..66fe498 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -78,15 +78,29 @@ struct Versions { /// Returns internal prtoton pfx path pub fn wine_prefix_dir(slug: Option<&str>) -> Result { if let Some(slug) = slug { + // Check if prefix is already in the cache map if let Some(prefix) = GamePrefixMap.lock().unwrap().get(slug) { return Ok(prefix.clone().into()); } - } - let base = maxima_dir()?.join("wine/prefix"); - if let Some(slug) = slug { - Ok(base.join(slug)) + + // Load settings from disk to get the wine_prefix + use crate::gamesettings::get_game_settings; + let settings = get_game_settings(slug); + let prefix = settings.wine_prefix(); + + // If settings have a non-empty wine_prefix, cache it and return it + if !prefix.is_empty() { + GamePrefixMap + .lock() + .unwrap() + .insert(slug.to_string(), prefix.to_string()); + return Ok(prefix.into()); + } + + // Fallback to default path + Ok(maxima_dir()?.join("wine/prefix").join(slug)) } else { - Ok(base) + Ok(maxima_dir()?.join("wine/prefix")) } } diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index b6c136c..3e3365f 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -8,11 +8,11 @@ use log::{debug, error, info}; use maxima::{ core::{ service_layer::{ - ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, + ServiceGame, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, }, - GamePrefixMap, LockedMaxima, + LockedMaxima, }, gamesettings::get_game_settings, util::native::maxima_dir, @@ -237,7 +237,6 @@ pub async fn get_games_request( // Grab persisted settings from Maxima's GameSettingsManager if available let core_settings = get_game_settings(&slug); game_settings.save(&slug, core_settings.clone()); - GamePrefixMap.lock().unwrap().insert(slug.clone(), core_settings.wine_prefix.clone()); let settings = core_settings.clone(); let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info,