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/42] 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 9f3a6e77..07a30d7a 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 00000000..c685869e --- /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 f16116db..ead9d03b 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 4a98e432..d649f8f6 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/42] 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 6281672c..87579f10 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 50c61a69..96c4b508 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 5adfc60f..f8d8f359 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/42] 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 de2ce960..e3afc394 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 87579f10..18b9b5a3 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 faa730cc..41bfc99b 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 88ff044b..781ebe62 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 00000000..e5765241 --- /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 0d878dbe..f4441827 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 f2a62cae..876ef8e3 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/42] 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 e5765241..b86241ff 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 876ef8e3..66e10d8b 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 9a98c035..5ae00e05 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 456c1778..46b8e8ec 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 e3e0278b..6c1fc391 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 5caae1ea..1d481c62 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/42] 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 e3afc394..de2ce960 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 ead9d03b..9b9fe107 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 18b9b5a3..87579f10 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 41bfc99b..376b23db 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 781ebe62..b20db7ed 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 b86241ff..8e15eee2 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 66e10d8b..85872206 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 5ae00e05..d3cf25a5 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 46b8e8ec..5b8fa5f9 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 6c1fc391..b66b5492 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/42] 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 376b23db..becf8760 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 85872206..14340bb2 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/42] 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 8e15eee2..ab3cb6a5 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/42] 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 c685869e..9142939f 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 9b9fe107..ae9b50d9 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 d649f8f6..252950d8 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 c11df25d..2eaf8177 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 41e4cd75..69b6a211 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 87579f10..e6c183d7 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 becf8760..8779694f 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 b20db7ed..21094608 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 ab3cb6a5..dc869120 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 f4441827..84934148 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 5ddad4f9..3edd36f9 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 19261ac1..757bda99 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 96c4b508..ba6c5d97 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 14340bb2..79672b8e 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 d3cf25a5..65a023af 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 5b8fa5f9..7c8f6bf0 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 b66b5492..0971aff6 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 1d481c62..15031a5d 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/42] 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 9cd16c2f..0429789d 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 de2ce960..2f46d972 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 ae9b50d9..4f29ced7 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 2eaf8177..ecc5b39e 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 6c3bc134..cdc86147 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 d383350c..b169346c 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 69b6a211..b7904068 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 e6c183d7..c95a0f92 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 8779694f..e52dafb9 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 b8da5ceb..001b042d 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 42fa80f4..b1772167 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 0ec9e1b4..1f71c972 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 ec7b6839..e9b837ec 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 757bda99..569a44cd 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 c61cf76e..b6219814 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 ba6c5d97..14e7afb6 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 624fc0d0..b61a6462 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 7c8f6bf0..2e686c83 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 0971aff6..03597361 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/42] 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 c95a0f92..f99bb2ff 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 14e7afb6..9383258e 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 f8d8f359..5adfc60f 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/42] 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 9142939f..0094ce15 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 4f29ced7..84bdf273 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/42] 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 0094ce15..a47f6d5a 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 84bdf273..1c542e48 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/42] 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 1c542e48..f79754ba 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 b7904068..3c048f91 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/42] 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 21094608..1f99b082 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 dc869120..4f7e0a31 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 9383258e..9b391a15 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 79672b8e..01e38419 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/42] 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 3c048f91..396980ea 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 1f99b082..e8a28969 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 4f7e0a31..bb3e1e1f 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 9b391a15..20772c2f 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 01e38419..4b4f47a0 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/42] 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 2f46d972..9be9107d 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 e982a190..6f93ad06 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 f79754ba..7b3bd9ee 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 f99bb2ff..4f1644e3 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 e52dafb9..04386c11 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 4b4f47a0..b6c136ca 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/42] 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 bb3e1e1f..36458dc8 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 20772c2f..66fe4983 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 b6c136ca..3e3365f8 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, From a26c4eac361b4c989e628e6f6bfe6c0734af881d Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:15:39 -0600 Subject: [PATCH 18/42] Store game path and wine prefix in file, detect version based on installed path, get rid of universal game prefix map --- maxima-lib/src/core/library.rs | 45 ++++++------------- maxima-lib/src/core/mod.rs | 5 --- maxima-lib/src/gamesettings/mod.rs | 31 +++---------- maxima-lib/src/gameversion/mod.rs | 70 ++++++++++++++++++++++++++++++ maxima-lib/src/lib.rs | 1 + maxima-lib/src/unix/wine.rs | 39 ++++++----------- maxima-ui/src/bridge_thread.rs | 4 ++ 7 files changed, 107 insertions(+), 88 deletions(-) create mode 100644 maxima-lib/src/gameversion/mod.rs diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 04386c11..46df9175 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -11,10 +11,11 @@ use super::{ SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, }, }; -#[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::util::registry::{parse_registry_path, RegistryError}; +use crate::{ + gameversion::load_game_version_from_json, + util::native::{maxima_dir, NativeError, SafeStr}, +}; use derive_getters::Getters; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; @@ -128,35 +129,17 @@ impl OwnedOffer { } pub async fn local_manifest(&self) -> Result>, ManifestError> { - let path = if self - .offer - .install_check_override() - .as_ref() - .ok_or(ManifestError::NoInstallPath(self.slug.clone()))? - .contains("installerdata.xml") - { - let ic_path = PathBuf::from(self.install_check_path().await?); - #[cfg(unix)] - let ic_path = case_insensitive_path(ic_path); - ic_path - } else { - let path = PathBuf::from( - parse_partial_registry_path( - &self - .offer - .install_check_override() - .as_ref() - .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, - Some(&self.slug), - ) - .await? - .safe_str()? - .to_owned(), - ); - - path.join(MANIFEST_RELATIVE_PATH) + let game_install_info = match load_game_version_from_json(&self.slug) { + Ok(info) => info, + Err(_) => return Ok(None), // No info file yet, placeholder for now }; + let path = game_install_info + .install_path_pathbuf() + .join(MANIFEST_RELATIVE_PATH); + if !path.exists() { + return Ok(None); + } Ok(Some(manifest::read(path).await?)) } diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index e8a28969..ee51bfa7 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -145,11 +145,6 @@ 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 36458dc8..f3bbc0b5 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::{core::GamePrefixMap, util::native::maxima_dir}; +use crate::util::native::maxima_dir; use log::info; use serde::{Deserialize, Serialize}; use serde_json; @@ -11,7 +11,6 @@ pub struct GameSettings { pub installed: bool, pub launch_args: String, pub exe_override: String, - pub wine_prefix: String, } impl GameSettings { @@ -21,16 +20,9 @@ impl GameSettings { installed: false, 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 - } - /// Public accessors for fields so consumers can read settings. pub fn cloud_saves(&self) -> bool { self.cloud_saves @@ -44,10 +36,6 @@ impl GameSettings { &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; @@ -59,20 +47,15 @@ impl 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), + Err(_) => return GameSettings::new(), }; let content = match std::fs::read_to_string(path) { Ok(content) => content, - Err(_) => return GameSettings::new_with_slug(slug), + Err(_) => return GameSettings::new(), }; - 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()); + let game_settings = serde_json::from_str(&content).unwrap_or_else(|_| GameSettings::new()); game_settings } @@ -123,15 +106,11 @@ impl GameSettingsManager { self.settings .get(slug) .cloned() - .unwrap_or_else(|| GameSettings::new_with_slug(slug)) + .unwrap_or_else(|| GameSettings::new()) } 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()); } } diff --git a/maxima-lib/src/gameversion/mod.rs b/maxima-lib/src/gameversion/mod.rs new file mode 100644 index 00000000..24d9d06e --- /dev/null +++ b/maxima-lib/src/gameversion/mod.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +use crate::util::native::maxima_dir; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum GameVersionError { + #[error(transparent)] + Native(#[from] crate::util::native::NativeError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error("game version info not found for `{0}`")] + NotFound(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameInstallInfo { + pub path: String, + pub wine_prefix: String, +} + +impl GameInstallInfo { + pub fn new(path: String) -> Self { + Self { + path, + wine_prefix: String::new(), + } + } + + pub fn path(&self) -> &str { + &self.path + } + + pub fn wine_prefix(&self) -> &str { + &self.wine_prefix + } + + pub fn install_path_pathbuf(&self) -> PathBuf { + PathBuf::from(&self.path) + } + + pub fn wine_prefix_pathbuf(&self) -> PathBuf { + PathBuf::from(&self.wine_prefix) + } + + // TODO: Maybe we can just query the slug by the filename of the path? Look into this later + pub fn save_to_json(&self, slug: &str) { + if let Ok(json) = serde_json::to_string_pretty(self) { + let mut path = maxima_dir(); + path.as_mut().unwrap().push("gameinfo"); + if let Ok(_) = std::fs::create_dir_all(&path.as_ref().unwrap()) { + path.as_mut().unwrap().push(format!("{}.json", slug)); + fs::write(path.unwrap(), json).unwrap(); + } + } + } +} +pub fn load_game_version_from_json(slug: &str) -> Result { + let mut path = maxima_dir(); + path.as_mut().unwrap().push("gameinfo"); + path.as_mut().unwrap().push(format!("{}.json", slug)); + let json = fs::read_to_string(path.unwrap())?; + let game_install_info: GameInstallInfo = serde_json::from_str(&json)?; + Ok(game_install_info) +} diff --git a/maxima-lib/src/lib.rs b/maxima-lib/src/lib.rs index 84934148..399a2fa0 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -7,6 +7,7 @@ pub mod content; pub mod core; pub mod gamesettings; +pub mod gameversion; pub mod lsx; pub mod ooa; pub mod rtm; diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 66fe4983..068fb6ff 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::{ - core::GamePrefixMap, + gameversion::load_game_version_from_json, util::{ github::{ fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease, @@ -75,33 +75,20 @@ struct Versions { umu: String, } -/// Returns internal prtoton pfx path +/// Returns internal proton 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()); - } - - // 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(maxima_dir()?.join("wine/prefix")) + let mut game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); + let mut prefix_path = game_install_info.wine_prefix_pathbuf(); + + if prefix_path.to_str().unwrap().is_empty() { + prefix_path = maxima_dir()? + .join("wine/prefixes/") + .join(slug.unwrap_or("default")); + game_install_info.wine_prefix = prefix_path.to_string_lossy().to_string(); + game_install_info.save_to_json(slug.unwrap_or("default")); } + + Ok(prefix_path) } pub fn proton_dir() -> Result { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 2e686c83..81db9818 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -26,6 +26,7 @@ use maxima::{ }, LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, }, + gameversion::GameInstallInfo, lsx::service::LSXServerError, rtm::RtmError, util::{ @@ -516,6 +517,9 @@ impl BridgeThread { .path(path.to_owned()) .slug(slug.to_owned()) .build()?; + let game_install_info = + GameInstallInfo::new(path.to_owned().to_string_lossy().to_string()); + game_install_info.save_to_json(&slug); Ok(maxima.content_manager().add_install(game).await?) } MaximaLibRequest::StartGameRequest(info, settings) => { From 5bc6c3280d540b8abfa2ebbc24700850a9f3399d Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:51:50 -0600 Subject: [PATCH 19/42] Slighly refactor game settings struct, PCSign uses default installation date --- maxima-lib/src/core/auth/hardware.rs | 5 +-- maxima-lib/src/core/cloudsync.rs | 4 ++- maxima-lib/src/core/launch.rs | 2 +- maxima-lib/src/gamesettings/mod.rs | 46 ++++++++++++++++++++-------- maxima-lib/src/ooa/mod.rs | 2 +- maxima-lib/src/unix/wine.rs | 21 +++++++++---- maxima-ui/src/bridge/get_games.rs | 12 +++----- maxima-ui/src/bridge_processor.rs | 1 - maxima-ui/src/bridge_thread.rs | 11 ++++--- 9 files changed, 66 insertions(+), 38 deletions(-) diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index ecc5b39e..dcf6bf65 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -483,8 +483,9 @@ fn get_root_creation_str(slug: Option<&str>) -> String { let date_str = String::from("1970010100:00:00.000000000+0000"); let wine_prefix = match wine_prefix_dir(slug) { - Ok(prefix) => prefix, - Err(_) => return date_str, + // This gets used in several places, default date should only be used with PCSign + Some(prefix) => prefix, + None => return date_str, }; let date_str = match fs::metadata(wine_prefix.join("drive_c")) { Ok(metadata) => { diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 396980ea..3b241249 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -107,7 +107,9 @@ fn home_dir() -> Result { #[cfg(unix)] fn home_dir(slug: Option<&str>) -> Result { use crate::unix::wine::wine_prefix_dir; - Ok(wine_prefix_dir(slug)?.join("drive_c/users/steamuser")) + Ok(wine_prefix_dir(slug) + .unwrap() + .join("drive_c/users/steamuser")) } fn substitute_paths>(path: P, slug: Option<&str>) -> Result { diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 4f1644e3..3953c89b 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -429,7 +429,7 @@ pub async fn mx_linux_setup(slug: Option<&str>) -> Result<(), NativeError> { install_wine, setup_wine_registry, wine_prefix_dir, }; - std::fs::create_dir_all(wine_prefix_dir(slug)?)?; + std::fs::create_dir_all(wine_prefix_dir(slug).unwrap())?; info!("Verifying wine dependencies..."); let skip = std::env::var("MAXIMA_DISABLE_WINE_VERIFICATION").is_ok(); diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index f3bbc0b5..70d95bc9 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -5,7 +5,7 @@ use log::info; use serde::{Deserialize, Serialize}; use serde_json; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct GameSettings { pub cloud_saves: bool, pub installed: bool, @@ -35,13 +35,6 @@ impl GameSettings { pub fn exe_override(&self) -> &str { &self.exe_override } - - /// 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 { @@ -102,15 +95,42 @@ impl GameSettingsManager { } } - pub fn get(&self, slug: &str) -> GameSettings { + pub fn get_or_load(&mut self, slug: &str) -> GameSettings { self.settings - .get(slug) - .cloned() - .unwrap_or_else(|| GameSettings::new()) + .entry(slug.to_string()) + .or_insert_with(|| get_game_settings(slug)) + .clone() + } + + pub fn update_with(&mut self, slug: &str, mutator: F) -> GameSettings + where + F: FnOnce(&mut GameSettings), + { + let settings = self + .settings + .entry(slug.to_string()) + .or_insert_with(|| get_game_settings(slug)); + + let previous = settings.clone(); + mutator(settings); + + if *settings != previous { + save_game_settings(slug, settings); + } + + settings.clone() } pub fn save(&mut self, slug: &str, settings: GameSettings) { - save_game_settings(slug, &settings); + let should_save = self + .settings + .get(slug) + .map_or(true, |existing| existing != &settings); + self.settings.insert(slug.to_string(), settings.clone()); + + if should_save { + save_game_settings(slug, &settings); + } } } diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index b6219814..935e92ae 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -379,7 +379,7 @@ pub fn get_license_dir(slug: Option<&str>) -> Result { let path = format!( "{}/drive_c/{}", - wine_prefix_dir(slug)?.safe_str()?, + wine_prefix_dir(slug).unwrap().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 068fb6ff..28f4e4fc 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -4,7 +4,7 @@ use std::{ ffi::OsStr, fs::{create_dir_all, remove_dir_all, remove_file, File}, io::Read, - path::PathBuf, + path::{Path, PathBuf}, process::{ExitStatus, Stdio}, }; @@ -76,19 +76,25 @@ struct Versions { } /// Returns internal proton pfx path -pub fn wine_prefix_dir(slug: Option<&str>) -> Result { +pub fn wine_prefix_dir(slug: Option<&str>) -> Option { + if slug.is_none() { + // This is used for PCSign, probably should make this more explicit later + return None; + } + let mut game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); let mut prefix_path = game_install_info.wine_prefix_pathbuf(); if prefix_path.to_str().unwrap().is_empty() { - prefix_path = maxima_dir()? + prefix_path = maxima_dir() + .unwrap() .join("wine/prefixes/") .join(slug.unwrap_or("default")); game_install_info.wine_prefix = prefix_path.to_string_lossy().to_string(); game_install_info.save_to_json(slug.unwrap_or("default")); } - Ok(prefix_path) + Some(prefix_path) } pub fn proton_dir() -> Result { @@ -259,7 +265,7 @@ pub async fn run_wine_command, T: AsRef>( slug: Option<&str>, ) -> Result { let proton_path = proton_dir()?; - let proton_prefix_path = wine_prefix_dir(slug)?; + let proton_prefix_path = wine_prefix_dir(slug).unwrap(); let eac_path = eac_dir()?; let umu_bin = umu_bin()?; @@ -493,7 +499,10 @@ async fn parse_wine_registry(file_path: &str) -> WineRegistry { } pub async fn parse_mx_wine_registry(slug: Option<&str>) -> Result { - let path = wine_prefix_dir(slug)?.join("pfx").join("system.reg"); + let path = wine_prefix_dir(slug) + .unwrap() + .join("pfx") + .join("system.reg"); if !path.exists() { return Ok(HashMap::new()); } diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 3e3365f8..580db769 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -4,7 +4,7 @@ use crate::{ GameDetailsWrapper, GameInfo, GameVersionInfo, }; use egui::Context; -use log::{debug, error, info}; +use log::{debug, info}; use maxima::{ core::{ service_layer::{ @@ -14,7 +14,6 @@ use maxima::{ }, LockedMaxima, }, - gamesettings::get_game_settings, util::native::maxima_dir, }; use std::{fs, sync::mpsc::Sender}; @@ -199,9 +198,9 @@ pub async fn get_games_request( if !logged_in { return Err(BackendError::LoggedOut); } - let mut game_settings = maxima.mut_game_settings().clone(); - let owned_games = maxima.mut_library().games().await?; + let owned_games = maxima.mut_library().games().await?.clone(); + let settings_manager = maxima.mut_game_settings(); for game in owned_games { let slug = game.base_offer().slug().clone(); @@ -234,10 +233,7 @@ pub async fn get_games_request( 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 = get_game_settings(&slug); - game_settings.save(&slug, core_settings.clone()); - let settings = core_settings.clone(); + let settings = settings_manager.get_or_load(&slug); 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 65a023af..d77d7b2b 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -4,7 +4,6 @@ use crate::{ BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, }; use log::{error, info, warn}; -use maxima::gamesettings::GameSettings; use std::sync::mpsc::TryRecvError; pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 81db9818..98c4986b 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -36,7 +36,6 @@ use maxima::{ }; use std::sync::mpsc::{SendError, TryRecvError}; use std::{ - panic, path::PathBuf, sync::mpsc::{Receiver, Sender}, time::{Duration, SystemTime}, @@ -411,9 +410,9 @@ impl BridgeThread { { 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); + manager.update_with(&slug, |settings| { + settings.installed = true; + }); } backend_responder .send(MaximaLibResponse::DownloadFinished(offer_id))?; // UI can handle updating settings, can easily access UI Frontend structures in DownloadFinished @@ -529,7 +528,9 @@ 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.update_with(&slug, |stored| { + *stored = settings; + }); Ok(()) } MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), //TODO: kill the bridge thread From b16c10ee7bf0c2533d8621506635008a6cac5829 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:14:58 -0600 Subject: [PATCH 20/42] Make install check see if game info file exists (Still need to create it at the end of install and not the beginning, also need to refresh UI when install is complete) --- maxima-lib/src/core/library.rs | 23 +++++---------------- maxima-lib/src/gamesettings/mod.rs | 6 ------ maxima-ui/src/bridge_processor.rs | 32 ++---------------------------- maxima-ui/src/bridge_thread.rs | 12 +---------- 4 files changed, 8 insertions(+), 65 deletions(-) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 46df9175..6a7ba27e 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -56,26 +56,13 @@ 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)); - if !manifest_path.exists() { - return false; - } - - let contents = match std::fs::read_to_string(&manifest_path) { - Ok(s) => s, + let maxima_dir = match maxima_dir() { + Ok(dir) => dir, 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, - } + + let game_info_path = maxima_dir.join("gameinfo").join(format!("{}.json", &self.slug)); + game_info_path.exists() } pub async fn install_check_path(&self) -> Result { diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs index 70d95bc9..4e82ed00 100644 --- a/maxima-lib/src/gamesettings/mod.rs +++ b/maxima-lib/src/gamesettings/mod.rs @@ -8,7 +8,6 @@ use serde_json; #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct GameSettings { pub cloud_saves: bool, - pub installed: bool, pub launch_args: String, pub exe_override: String, } @@ -17,7 +16,6 @@ impl GameSettings { pub fn new() -> Self { Self { cloud_saves: true, - installed: false, launch_args: String::new(), exe_override: String::new(), } @@ -53,10 +51,6 @@ 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() { let settings_dir = dir.join("settings"); diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index d77d7b2b..544dbf35 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -85,36 +85,8 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } } } - 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(), - ), - ); - } + DownloadFinished(_) => { + } DownloadQueueUpdate(current, queue) => { if let Some(current) = current { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 98c4986b..014851a4 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -404,18 +404,8 @@ impl BridgeThread { match ev { maxima::core::MaximaEvent::ReceivedLSXRequest(_, _) => {} 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(); - manager.update_with(&slug, |settings| { - settings.installed = true; - }); - } backend_responder - .send(MaximaLibResponse::DownloadFinished(offer_id))?; // UI can handle updating settings, can easily access UI Frontend structures in DownloadFinished + .send(MaximaLibResponse::DownloadFinished(offer_id))?; Self::update_queue(maxima.content_manager(), backend_responder.clone()); } } From 364481961fa0dfa44e9ac987fb2b028708bd127d Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:34:28 -0600 Subject: [PATCH 21/42] Create wine prefix folder if it does not exist for slug --- maxima-lib/src/unix/wine.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 28f4e4fc..411a8c06 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -94,8 +94,18 @@ pub fn wine_prefix_dir(slug: Option<&str>) -> Option { game_install_info.save_to_json(slug.unwrap_or("default")); } + if !prefix_path.exists() { + } if let Err(err) = create_dir_all(&prefix_path) { + warn!( + "Failed to create wine prefix directory at {:?}: {}", + prefix_path, err + ); + return None; + } Some(prefix_path) -} + + } + pub fn proton_dir() -> Result { Ok(maxima_dir()?.join("wine/proton")) From b796bd2c9d6efc657a646aaf1b120a2af1317568 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:07:34 -0600 Subject: [PATCH 22/42] Query executable path from JSON --- maxima-lib/src/core/auth/hardware.rs | 5 ++--- maxima-lib/src/core/library.rs | 6 ++++-- maxima-lib/src/unix/wine.rs | 27 ++++++++++----------------- maxima-lib/src/util/registry.rs | 28 +++++++++------------------- maxima-ui/src/bridge_processor.rs | 4 +--- 5 files changed, 26 insertions(+), 44 deletions(-) diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index dcf6bf65..ffbb49a7 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -482,9 +482,8 @@ fn get_root_creation_str(slug: Option<&str>) -> String { use std::{fs, os::unix::fs::MetadataExt}; let date_str = String::from("1970010100:00:00.000000000+0000"); - let wine_prefix = match wine_prefix_dir(slug) { - // This gets used in several places, default date should only be used with PCSign - Some(prefix) => prefix, + let wine_prefix = match slug { + Some(slug) => wine_prefix_dir(Some(slug)).unwrap(), None => return date_str, }; let date_str = match fs::metadata(wine_prefix.join("drive_c")) { diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 6a7ba27e..d472b508 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -60,8 +60,10 @@ impl OwnedOffer { Ok(dir) => dir, Err(_) => return false, }; - - let game_info_path = maxima_dir.join("gameinfo").join(format!("{}.json", &self.slug)); + + let game_info_path = maxima_dir + .join("gameinfo") + .join(format!("{}.json", &self.slug)); game_info_path.exists() } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 411a8c06..8589c355 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -76,12 +76,7 @@ struct Versions { } /// Returns internal proton pfx path -pub fn wine_prefix_dir(slug: Option<&str>) -> Option { - if slug.is_none() { - // This is used for PCSign, probably should make this more explicit later - return None; - } - +pub fn wine_prefix_dir(slug: Option<&str>) -> Result { let mut game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); let mut prefix_path = game_install_info.wine_prefix_pathbuf(); @@ -94,18 +89,16 @@ pub fn wine_prefix_dir(slug: Option<&str>) -> Option { game_install_info.save_to_json(slug.unwrap_or("default")); } - if !prefix_path.exists() { - } if let Err(err) = create_dir_all(&prefix_path) { - warn!( - "Failed to create wine prefix directory at {:?}: {}", - prefix_path, err - ); - return None; - } - Some(prefix_path) - + if !prefix_path.exists() {} + if let Err(err) = create_dir_all(&prefix_path) { + warn!( + "Failed to create wine prefix directory at {:?}: {}", + prefix_path, err + ); + return Err(NativeError::Io(err)); } - + Ok(prefix_path) +} pub fn proton_dir() -> Result { Ok(maxima_dir()?.join("wine/proton")) diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index b61a6462..38dfb8a6 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -1,7 +1,7 @@ #[cfg(windows)] extern crate winapi; -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use thiserror::Error; #[cfg(windows)] @@ -32,6 +32,7 @@ use winreg::{ #[cfg(unix)] use std::{collections::HashMap, env, fs}; +use crate::gameversion::load_game_version_from_json; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; @@ -182,24 +183,13 @@ async fn read_reg_key(path: &str, slug: Option<&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, slug).await? { - Some(path) => path.replace("\\", "/").replace("//", "/"), - None => return Ok(PathBuf::from(key.to_owned())), - }; - - let second = second.replace("\\", "/"); - let second = second.strip_prefix("/").unwrap_or(&second); - - return Ok([path, second.to_owned()].iter().collect()); - } else { - PathBuf::from(key.to_owned()) - }; - + let game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); + let idx = key.rfind(']'); + // Path looks like [HKEY_LOCAL_MACHINE\SOFTWARE\BioWare\Mass Effect Legendary Edition\Install Dir]Game\Launcher\MassEffectLauncher.exe + // Extract everything after the last ] and append it to the install path + // TODO: Maybe normalize path to OS? + let after_bracket = &key[(idx.unwrap() + 1)..]; + let path = game_install_info.install_path_pathbuf().join(after_bracket); #[cfg(unix)] let path = case_insensitive_path(path); Ok(path) diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 544dbf35..c58f7333 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -85,9 +85,7 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } } } - DownloadFinished(_) => { - - } + DownloadFinished(_) => {} DownloadQueueUpdate(current, queue) => { if let Some(current) = current { if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { From d78a6d049adf3bed3c6d764ab8b7ae8782615402 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:35:49 -0600 Subject: [PATCH 23/42] Exclude unused methods from build --- maxima-lib/src/unix/wine.rs | 1 + maxima-lib/src/util/registry.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 8589c355..c8aef60f 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -528,6 +528,7 @@ fn normalize_key(key: &str) -> String { } } +#[cfg(false)] // Unused method for now, but may be useful in the future pub async fn get_mx_wine_registry_value( query_key: &str, slug: Option<&str>, diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 38dfb8a6..027418e7 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -176,7 +176,7 @@ async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, Ok(None) } -#[cfg(unix)] +#[cfg(false)] // Unused method for 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, slug).await?) @@ -195,6 +195,7 @@ pub async fn parse_registry_path(key: &str, slug: Option<&str>) -> Result, From 899a5026722ee08a0137e852760a59d55dd6647c Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:06:21 -0600 Subject: [PATCH 24/42] Pass in slug where needed in maxima-cli --- maxima-cli/src/main.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 9be9107d..b4f50d11 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -296,10 +296,10 @@ async fn startup() -> Result<()> { } } } else { - slug + slug.clone() }; - start_game(&offer_id, game_path, game_args, login, maxima_arc.clone()).await + start_game(&offer_id, &slug, game_path, game_args, login, maxima_arc.clone()).await } Mode::ListGames => list_games(maxima_arc.clone()).await, Mode::LocateGame { path, slug } => locate_game(maxima_arc.clone(), &path, &slug).await, @@ -356,10 +356,10 @@ async fn run_interactive(maxima_arc: LockedMaxima) -> Result<()> { } async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { - let offer_id = { - let mut maxima = maxima_arc.lock().await; + let mut maxima = maxima_arc.lock().await; + let mut owned_games = Vec::new(); + let owned_games_strs = { - let mut owned_games = Vec::new(); for game in maxima.mut_library().games().await? { if !game.base_offer().is_installed().await { continue; @@ -368,17 +368,18 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { owned_games.push(game); } - let owned_games_strs = owned_games + owned_games .iter() .map(|g| g.name()) - .collect::>(); - - let name = Select::new("What game would you like to play?", owned_games_strs).prompt()?; - let game: &OwnedTitle = owned_games.iter().find(|g| g.name() == name).unwrap(); - game.base_offer().offer_id().to_owned() + .collect::>() }; - start_game(&offer_id, None, Vec::new(), None, maxima_arc.clone()).await?; + let name = Select::new("What game would you like to play?", owned_games_strs).prompt()?; + let game = owned_games.iter().find(|g| g.name() == name).unwrap(); + + let offer_id = game.base_offer().offer_id().to_owned().clone(); + let slug = game.base_offer().slug().to_owned().clone(); + start_game(&offer_id, &slug, None, Vec::new(), None, maxima_arc.clone()).await?; Ok(()) } @@ -810,6 +811,7 @@ async fn do_cloud_sync(maxima_arc: LockedMaxima, game_slug: &str, write: bool) - async fn start_game( offer_id: &str, + slug: &str, game_path_override: Option, game_args: Vec, login: Option, From 0a7f5fa668e1bdc018f13bb9eb554f810f399626 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:05:12 -0600 Subject: [PATCH 25/42] Fix game installation with maxima-cli --- maxima-cli/src/main.rs | 43 +++++++++++++++++++++++++++------ maxima-lib/src/util/registry.rs | 2 +- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index b4f50d11..dcf71709 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -41,9 +41,14 @@ use maxima::{ }, LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, }, + gameversion::GameInstallInfo, ooa, rtm::client::BasicPresence, - util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, + util::{ + log::init_logger, + native::{maxima_dir, take_foreground_focus}, + registry::check_registry_validity, + }, }; lazy_static! { @@ -299,7 +304,15 @@ async fn startup() -> Result<()> { slug.clone() }; - start_game(&offer_id, &slug, game_path, game_args, login, maxima_arc.clone()).await + start_game( + &offer_id, + &slug, + game_path, + game_args, + login, + maxima_arc.clone(), + ) + .await } Mode::ListGames => list_games(maxima_arc.clone()).await, Mode::LocateGame { path, slug } => locate_game(maxima_arc.clone(), &path, &slug).await, @@ -359,7 +372,6 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; let mut owned_games = Vec::new(); let owned_games_strs = { - for game in maxima.mut_library().games().await? { if !game.base_offer().is_installed().await { continue; @@ -376,9 +388,10 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { let name = Select::new("What game would you like to play?", owned_games_strs).prompt()?; let game = owned_games.iter().find(|g| g.name() == name).unwrap(); - + let offer_id = game.base_offer().offer_id().to_owned().clone(); let slug = game.base_offer().slug().to_owned().clone(); + drop(maxima); // To unlock before starting the game, since start_game will also need to lock start_game(&offer_id, &slug, None, Vec::new(), None, maxima_arc.clone()).await?; Ok(()) @@ -387,7 +400,7 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; - let offer_id = { + let game = { let mut owned_games = Vec::new(); for game in maxima.mut_library().games().await? { if game.base_offer().is_installed().await { @@ -404,10 +417,16 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let name = Select::new("What game would you like to install?", owned_games_strs).prompt()?; - let game = owned_games.iter().find(|g| g.name() == name).unwrap(); - game.base_offer().offer_id().to_owned() + owned_games + .iter() + .find(|g| g.name() == name) + .unwrap() + .clone() }; + let offer_id = game.base_offer().offer_id().to_owned(); + let slug = game.base_offer().slug().to_owned(); + let builds = maxima .content_manager() .service() @@ -425,6 +444,15 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { Text::new("Where would you like to install the game? (must be an absolute path)") .prompt()?, ); + let mut game_install_info = GameInstallInfo::new(path.to_str().unwrap().to_string()); + game_install_info.wine_prefix = maxima_dir() + .unwrap() + .join("wine/prefixes") + .join(&slug) + .to_str() + .unwrap() + .to_string(); + game_install_info.save_to_json(&slug); if !path.is_absolute() { error!("Path {:?} is not absolute.", path); return Ok(()); @@ -434,6 +462,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { .offer_id(offer_id) .build_id(build.build_id().to_owned()) .path(path.clone()) + .slug(slug) // Needs the slug here for the manifest touchup after installation, which needs to know the wine prefix path .build()?; let start_time = Instant::now(); diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 027418e7..d4677f55 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -176,7 +176,7 @@ async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, Ok(None) } -#[cfg(false)] // Unused method for +#[cfg(false)] // Unused method for 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, slug).await?) From d59e7019b77c646728ea5e71593720472429076c Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:56:13 -0600 Subject: [PATCH 26/42] Fix compile errors on WIndows --- maxima-lib/src/core/auth/hardware.rs | 2 +- maxima-lib/src/core/cloudsync.rs | 2 +- maxima-lib/src/ooa/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index ffbb49a7..0ca93fee 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -53,7 +53,7 @@ pub enum HardwareHashError { impl HardwareInfo { #[cfg(windows)] - pub fn new(version: u32) -> Self { + pub fn new(version: u32, _slug: Option<&str>) -> Self { use std::collections::HashMap; use log::warn; diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 3b241249..54f0db7f 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -98,7 +98,7 @@ async fn acquire_auth(auth: &LockedAuthStorage) -> Result<(String, String), Clou } #[cfg(windows)] -fn home_dir() -> Result { +fn home_dir(_slug: Option<&str>) -> Result { Ok(PathBuf::from( std::env::var_os("USERPROFILE").unwrap_or_else(|| "C:\\Users\\Public".into()), )) diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index 935e92ae..06c4086c 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -367,7 +367,7 @@ pub async fn save_licenses( } #[cfg(windows)] -pub fn get_license_dir() -> Result { +pub fn get_license_dir(_slug: Option<&str>) -> Result { let path = format!("C:/{}", LICENSE_PATH.to_string()); create_dir_all(&path)?; Ok(PathBuf::from(path)) From b16d2966b2dfbf2d0f65c29f835badba780f0c35 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:02:30 -0600 Subject: [PATCH 27/42] Separate game settings refactor from wine prefix support --- maxima-lib/src/core/mod.rs | 13 +-- maxima-lib/src/gameinfo/mod.rs | 70 ++++++++++++++++ maxima-lib/src/gamesettings/mod.rs | 130 ----------------------------- maxima-lib/src/lib.rs | 1 - maxima-ui/src/bridge/get_games.rs | 9 +- maxima-ui/src/bridge_processor.rs | 7 +- maxima-ui/src/bridge_thread.rs | 11 --- maxima-ui/src/main.rs | 54 +++++++++--- maxima-ui/src/views/game_view.rs | 18 ++-- 9 files changed, 126 insertions(+), 187 deletions(-) create mode 100644 maxima-lib/src/gameinfo/mod.rs delete mode 100644 maxima-lib/src/gamesettings/mod.rs diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index ee51bfa7..7a6642f1 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -66,7 +66,6 @@ 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}, @@ -91,8 +90,7 @@ pub struct Maxima { #[getter(skip)] library: GameLibrary, - #[getter(skip)] - game_settings: GameSettingsManager, + playing: Option, lsx_port: u16, @@ -220,7 +218,6 @@ impl Maxima { request_cache, dummy_local_user, pending_events: Vec::new(), - game_settings: GameSettingsManager::new(), }))) } @@ -420,14 +417,6 @@ 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/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs new file mode 100644 index 00000000..24d9d06e --- /dev/null +++ b/maxima-lib/src/gameinfo/mod.rs @@ -0,0 +1,70 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +use crate::util::native::maxima_dir; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum GameVersionError { + #[error(transparent)] + Native(#[from] crate::util::native::NativeError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error("game version info not found for `{0}`")] + NotFound(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameInstallInfo { + pub path: String, + pub wine_prefix: String, +} + +impl GameInstallInfo { + pub fn new(path: String) -> Self { + Self { + path, + wine_prefix: String::new(), + } + } + + pub fn path(&self) -> &str { + &self.path + } + + pub fn wine_prefix(&self) -> &str { + &self.wine_prefix + } + + pub fn install_path_pathbuf(&self) -> PathBuf { + PathBuf::from(&self.path) + } + + pub fn wine_prefix_pathbuf(&self) -> PathBuf { + PathBuf::from(&self.wine_prefix) + } + + // TODO: Maybe we can just query the slug by the filename of the path? Look into this later + pub fn save_to_json(&self, slug: &str) { + if let Ok(json) = serde_json::to_string_pretty(self) { + let mut path = maxima_dir(); + path.as_mut().unwrap().push("gameinfo"); + if let Ok(_) = std::fs::create_dir_all(&path.as_ref().unwrap()) { + path.as_mut().unwrap().push(format!("{}.json", slug)); + fs::write(path.unwrap(), json).unwrap(); + } + } + } +} +pub fn load_game_version_from_json(slug: &str) -> Result { + let mut path = maxima_dir(); + path.as_mut().unwrap().push("gameinfo"); + path.as_mut().unwrap().push(format!("{}.json", slug)); + let json = fs::read_to_string(path.unwrap())?; + let game_install_info: GameInstallInfo = serde_json::from_str(&json)?; + Ok(game_install_info) +} diff --git a/maxima-lib/src/gamesettings/mod.rs b/maxima-lib/src/gamesettings/mod.rs deleted file mode 100644 index 4e82ed00..00000000 --- a/maxima-lib/src/gamesettings/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::collections::HashMap; - -use crate::util::native::maxima_dir; -use log::info; -use serde::{Deserialize, Serialize}; -use serde_json; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct GameSettings { - pub cloud_saves: bool, - pub launch_args: String, - pub exe_override: String, -} - -impl GameSettings { - pub fn new() -> Self { - Self { - cloud_saves: true, - launch_args: String::new(), - exe_override: String::new(), - } - } - - /// 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 get_game_settings(slug: &str) -> GameSettings { - let path = match maxima_dir() { - Ok(dir) => dir.join("settings").join(format!("{}.json", slug)), - Err(_) => return GameSettings::new(), - }; - - let content = match std::fs::read_to_string(path) { - Ok(content) => content, - Err(_) => return GameSettings::new(), - }; - - let game_settings = serde_json::from_str(&content).unwrap_or_else(|_| GameSettings::new()); - game_settings -} - -pub fn save_game_settings(slug: &str, settings: &GameSettings) { - info!("Saving settings for {}...", slug); - 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) { - 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) { - 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)] -pub struct GameSettingsManager { - settings: HashMap, -} - -impl GameSettingsManager { - pub fn new() -> Self { - Self { - settings: HashMap::new(), - } - } - - pub fn get_or_load(&mut self, slug: &str) -> GameSettings { - self.settings - .entry(slug.to_string()) - .or_insert_with(|| get_game_settings(slug)) - .clone() - } - - pub fn update_with(&mut self, slug: &str, mutator: F) -> GameSettings - where - F: FnOnce(&mut GameSettings), - { - let settings = self - .settings - .entry(slug.to_string()) - .or_insert_with(|| get_game_settings(slug)); - - let previous = settings.clone(); - mutator(settings); - - if *settings != previous { - save_game_settings(slug, settings); - } - - settings.clone() - } - - pub fn save(&mut self, slug: &str, settings: GameSettings) { - let should_save = self - .settings - .get(slug) - .map_or(true, |existing| existing != &settings); - - self.settings.insert(slug.to_string(), settings.clone()); - - if should_save { - save_game_settings(slug, &settings); - } - } -} diff --git a/maxima-lib/src/lib.rs b/maxima-lib/src/lib.rs index 399a2fa0..8389d46a 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -6,7 +6,6 @@ pub mod content; pub mod core; -pub mod gamesettings; pub mod gameversion; pub mod lsx; pub mod ooa; diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 580db769..843f6c4d 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -1,7 +1,7 @@ use crate::{ bridge_thread::{BackendError, InteractThreadGameListResponse, MaximaLibResponse}, ui_image::UIImageCacheLoaderCommand, - GameDetailsWrapper, GameInfo, GameVersionInfo, + GameDetailsWrapper, GameInfo, GameSettings, GameVersionInfo, }; use egui::Context; use log::{debug, info}; @@ -200,7 +200,6 @@ pub async fn get_games_request( } let owned_games = maxima.mut_library().games().await?.clone(); - let settings_manager = maxima.mut_game_settings(); for game in owned_games { let slug = game.base_offer().slug().clone(); @@ -233,7 +232,11 @@ pub async fn get_games_request( has_cloud_saves: game.base_offer().offer().has_cloud_save(), }; let slug = game_info.slug.clone(); - let settings = settings_manager.get_or_load(&slug); + let settings = GameSettings { + exe_override: String::new(), + launch_args: String::new(), + cloud_saves: true, + }; 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 c58f7333..c77d01b9 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -44,12 +44,7 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } ServiceStarted => app.backend_state = BackendStallState::Starting, GameInfoResponse(res) => { - // 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); + app.games.insert(res.game.slug.clone(), res.game); } GameDetailsResponse(res) => { let response = res.response; diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 014851a4..010cc600 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -84,8 +84,6 @@ pub enum MaximaLibRequest { StartGameRequest(GameInfo, Option), InstallGameRequest(String, String, PathBuf), LocateGameRequest(String, String), - /// Persist UI-side game settings into the core GameSettingsManager - SaveGameSettings(String, GameSettings), ShutdownRequest, } @@ -514,15 +512,6 @@ 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(); - manager.update_with(&slug, |stored| { - *stored = 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 03597361..34681cf8 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -31,7 +31,6 @@ 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}; @@ -187,6 +186,23 @@ 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, @@ -583,6 +599,31 @@ 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) { @@ -943,17 +984,6 @@ 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 15031a5d..5caae1ea 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, set_app_modal, 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, @@ -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 { - app.modal = Some(PopupModal::GameLaunchOOD(game.slug.clone())); + set_app_modal!(app, Some(PopupModal::GameLaunchOOD(game.slug.clone()))); } else { app.playing_game = Some(game.slug.clone()); let settings = app.settings.game_settings.get(&game.slug); @@ -114,20 +114,14 @@ 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) { - app.installer_state = InstallModalState::new(&app.settings); - app.modal = Some(PopupModal::GameInstall(game.slug.clone())); + set_app_modal!(app, Some(PopupModal::GameInstall(game.slug.clone()))); } } } 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.modal = Some(PopupModal::GameSettings(game.slug.clone())); + set_app_modal!(app, Some(PopupModal::GameSettings(game.slug.clone()))); } }); }); From 5213c0caefc8b626a3419cf1360950b00261fda1 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:00:34 -0600 Subject: [PATCH 28/42] Prep work for UI options --- maxima-cli/src/main.rs | 2 +- maxima-lib/src/content/manager.rs | 34 +++++++-------- maxima-lib/src/core/library.rs | 2 +- maxima-lib/src/gameversion/mod.rs | 70 ------------------------------- maxima-lib/src/lib.rs | 2 +- maxima-lib/src/unix/wine.rs | 15 ++----- maxima-lib/src/util/registry.rs | 2 +- maxima-ui/src/bridge_thread.rs | 29 +++++++------ maxima-ui/src/main.rs | 2 +- 9 files changed, 39 insertions(+), 119 deletions(-) delete mode 100644 maxima-lib/src/gameversion/mod.rs diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index dcf71709..fb37925b 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -41,7 +41,7 @@ use maxima::{ }, LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, }, - gameversion::GameInstallInfo, + gameinfo::GameInstallInfo, ooa, rtm::client::BasicPresence, util::{ diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 7b3bd9ee..375e56e6 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -18,18 +18,10 @@ use tokio_util::sync::CancellationToken; use crate::{ content::{ - downloader::{DownloadError, ZipDownloader}, - exclusion::get_exclusion_list, - zip::{self, CompressionType, ZipError, ZipFileEntry}, - ContentService, - }, - core::{ - auth::storage::LockedAuthStorage, - manifest::{self, ManifestError, MANIFEST_RELATIVE_PATH}, - service_layer::ServiceLayerError, - MaximaEvent, - }, - util::native::{maxima_dir, NativeError}, + ContentService, downloader::{DownloadError, ZipDownloader}, exclusion::get_exclusion_list, zip::{self, CompressionType, ZipError, ZipFileEntry} + }, core::{ + MaximaEvent, auth::storage::LockedAuthStorage, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::ServiceLayerError + }, gameinfo::GameInstallInfo, util::native::{NativeError, maxima_dir} }; const QUEUE_FILE: &str = "download_queue.json"; @@ -40,6 +32,7 @@ pub struct QueuedGame { build_id: String, path: PathBuf, slug: String, + wine_prefix: Option, } #[derive(Default, Getters, Serialize, Deserialize)] @@ -127,6 +120,8 @@ impl DownloadQueue { pub struct GameDownloader { offer_id: String, slug: String, + path: PathBuf, + wine_prefix: Option, downloader: Arc, entries: Vec, @@ -174,7 +169,8 @@ impl GameDownloader { Ok(GameDownloader { offer_id: game.offer_id.to_owned(), slug: game.slug.to_owned(), - + path: game.path.to_owned(), + wine_prefix: game.wine_prefix.clone(), downloader: Arc::new(downloader), entries, cancel_token: CancellationToken::new(), @@ -198,7 +194,6 @@ impl GameDownloader { cancel_token, completed_bytes, notify, - &slug, ) .await; if let Err(err) = dl { @@ -232,7 +227,6 @@ impl GameDownloader { cancel_token: CancellationToken, completed_bytes: Arc, notify: Arc, - slug: &str, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(total_count); @@ -271,8 +265,10 @@ impl GameDownloader { let path = downloader_arc.path(); info!("Files downloaded, running touchup..."); - let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(path, &slug).await?; + + // let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; + // manifest.run_touchup(path, &slug).await?; + info!("Installation finished!"); completed_bytes.fetch_add(1, Ordering::SeqCst); @@ -378,6 +374,10 @@ impl ContentManager { if let Some(current) = &self.current { if current.is_done() { event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); + let mut game_install_info = + GameInstallInfo::new(current.path.to_str().unwrap().to_string()); + game_install_info.wine_prefix = maxima_dir().unwrap().join("wine/prefixes").join(¤t.slug).to_str().unwrap().to_string(); + game_install_info.save_to_json(¤t.slug); self.current = None; self.queue.current = None; diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index d472b508..1eccf839 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -13,7 +13,7 @@ use super::{ }; use crate::util::registry::{parse_registry_path, RegistryError}; use crate::{ - gameversion::load_game_version_from_json, + gameinfo::load_game_version_from_json, util::native::{maxima_dir, NativeError, SafeStr}, }; use derive_getters::Getters; diff --git a/maxima-lib/src/gameversion/mod.rs b/maxima-lib/src/gameversion/mod.rs deleted file mode 100644 index 24d9d06e..00000000 --- a/maxima-lib/src/gameversion/mod.rs +++ /dev/null @@ -1,70 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::PathBuf; - -use crate::util::native::maxima_dir; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum GameVersionError { - #[error(transparent)] - Native(#[from] crate::util::native::NativeError), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Json(#[from] serde_json::Error), - - #[error("game version info not found for `{0}`")] - NotFound(String), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GameInstallInfo { - pub path: String, - pub wine_prefix: String, -} - -impl GameInstallInfo { - pub fn new(path: String) -> Self { - Self { - path, - wine_prefix: String::new(), - } - } - - pub fn path(&self) -> &str { - &self.path - } - - pub fn wine_prefix(&self) -> &str { - &self.wine_prefix - } - - pub fn install_path_pathbuf(&self) -> PathBuf { - PathBuf::from(&self.path) - } - - pub fn wine_prefix_pathbuf(&self) -> PathBuf { - PathBuf::from(&self.wine_prefix) - } - - // TODO: Maybe we can just query the slug by the filename of the path? Look into this later - pub fn save_to_json(&self, slug: &str) { - if let Ok(json) = serde_json::to_string_pretty(self) { - let mut path = maxima_dir(); - path.as_mut().unwrap().push("gameinfo"); - if let Ok(_) = std::fs::create_dir_all(&path.as_ref().unwrap()) { - path.as_mut().unwrap().push(format!("{}.json", slug)); - fs::write(path.unwrap(), json).unwrap(); - } - } - } -} -pub fn load_game_version_from_json(slug: &str) -> Result { - let mut path = maxima_dir(); - path.as_mut().unwrap().push("gameinfo"); - path.as_mut().unwrap().push(format!("{}.json", slug)); - let json = fs::read_to_string(path.unwrap())?; - let game_install_info: GameInstallInfo = serde_json::from_str(&json)?; - Ok(game_install_info) -} diff --git a/maxima-lib/src/lib.rs b/maxima-lib/src/lib.rs index 8389d46a..d95e8100 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -6,7 +6,7 @@ pub mod content; pub mod core; -pub mod gameversion; +pub mod gameinfo; pub mod lsx; pub mod ooa; pub mod rtm; diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index c8aef60f..1c91ad0f 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::{ - gameversion::load_game_version_from_json, + gameinfo::load_game_version_from_json, util::{ github::{ fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease, @@ -77,17 +77,8 @@ struct Versions { /// Returns internal proton pfx path pub fn wine_prefix_dir(slug: Option<&str>) -> Result { - let mut game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); - let mut prefix_path = game_install_info.wine_prefix_pathbuf(); - - if prefix_path.to_str().unwrap().is_empty() { - prefix_path = maxima_dir() - .unwrap() - .join("wine/prefixes/") - .join(slug.unwrap_or("default")); - game_install_info.wine_prefix = prefix_path.to_string_lossy().to_string(); - game_install_info.save_to_json(slug.unwrap_or("default")); - } + let game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); + let prefix_path = game_install_info.wine_prefix_pathbuf(); if !prefix_path.exists() {} if let Err(err) = create_dir_all(&prefix_path) { diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index d4677f55..646d9396 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -32,7 +32,7 @@ use winreg::{ #[cfg(unix)] use std::{collections::HashMap, env, fs}; -use crate::gameversion::load_game_version_from_json; +use crate::gameinfo::load_game_version_from_json; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 010cc600..eac9084b 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -16,22 +16,17 @@ use maxima::{ ContentManager, ContentManagerError, QueuedGameBuilder, QueuedGameBuilderError, }, core::{ - auth::storage::{AuthError, TokenError}, - launch::LaunchError, - library::LibraryError, - manifest::{self, ManifestError, MANIFEST_RELATIVE_PATH}, - service_layer::{ + LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, auth::storage::{AuthError, TokenError}, launch::LaunchError, library::LibraryError, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ ServiceGameImagesRequestBuilderError, ServiceHeroBackgroundImageRequestBuilderError, ServiceLayerError, ServicePlayer, - }, - LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, + } }, - gameversion::GameInstallInfo, + gameinfo::GameInstallInfo, lsx::service::LSXServerError, rtm::RtmError, util::{ - native::NativeError, - registry::{check_registry_validity, set_up_registry, RegistryError}, + native::{NativeError, maxima_dir}, + registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; use std::sync::mpsc::{SendError, TryRecvError}; @@ -82,7 +77,7 @@ pub enum MaximaLibRequest { GetFriendsRequest, GetGameDetailsRequest(String), StartGameRequest(GameInfo, Option), - InstallGameRequest(String, String, PathBuf), + InstallGameRequest(String, String, PathBuf, Option), // offer, slug, path, wine prefix (unix only) LocateGameRequest(String, String), ShutdownRequest, } @@ -488,7 +483,7 @@ impl BridgeThread { ctx.request_repaint(); Ok(()) } - MaximaLibRequest::InstallGameRequest(offer, slug, path) => { + MaximaLibRequest::InstallGameRequest(offer, slug, path, wine_prefix) => { let mut maxima = maxima_arc.lock().await; let builds = maxima.content_manager().service().available_builds(&offer).await?; @@ -497,16 +492,20 @@ impl BridgeThread { } else { continue; }; + + #[cfg(unix)] + let wine_prefix = Some(maxima_dir().unwrap().join("wine/prefixes").join(&slug)); + + #[cfg(windows)] + let wine_prefix = None; let game = QueuedGameBuilder::default() .offer_id(offer.clone()) .build_id(build.build_id().to_owned()) .path(path.to_owned()) .slug(slug.to_owned()) + .wine_prefix(wine_prefix) .build()?; - let game_install_info = - GameInstallInfo::new(path.to_owned().to_string_lossy().to_string()); - game_install_info.save_to_json(&slug); Ok(maxima.content_manager().add_install(game).await?) } MaximaLibRequest::StartGameRequest(info, settings) => { diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 34681cf8..b2ad7bb0 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -916,7 +916,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(), slug.clone(), path.join(slug))).unwrap(); + self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), slug.clone(), path.join(slug), None)).unwrap(); clear = true; } From a96409ae4b9e1d3d1cfb1867484c97d4c1290298 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:41:11 -0600 Subject: [PATCH 29/42] UI stuff --- maxima-lib/src/content/manager.rs | 3 +- maxima-ui/res/locale/en_us.json | 3 +- maxima-ui/src/bridge_thread.rs | 4 +-- maxima-ui/src/main.rs | 41 +++++++++++++++++++++++++++- maxima-ui/src/translation_manager.rs | 2 ++ maxima-ui/src/views/settings_view.rs | 17 ++++++++++++ 6 files changed, 63 insertions(+), 7 deletions(-) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 375e56e6..9ef285e5 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -185,7 +185,6 @@ 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, @@ -376,7 +375,7 @@ impl ContentManager { event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); let mut game_install_info = GameInstallInfo::new(current.path.to_str().unwrap().to_string()); - game_install_info.wine_prefix = maxima_dir().unwrap().join("wine/prefixes").join(¤t.slug).to_str().unwrap().to_string(); + game_install_info.wine_prefix = current.wine_prefix.clone().unwrap().to_str().unwrap().to_string(); game_install_info.save_to_json(¤t.slug); self.current = None; self.queue.current = None; diff --git a/maxima-ui/res/locale/en_us.json b/maxima-ui/res/locale/en_us.json index 22d19a29..2a2864c6 100644 --- a/maxima-ui/res/locale/en_us.json +++ b/maxima-ui/res/locale/en_us.json @@ -94,8 +94,7 @@ }, "game_installation" : { "header": "Game Installation", - "default_folder": "Default installation folder", - "ignore_ood_warning": "Ignore out-of-date game launch warning" + "default_folder": "Default installation folder", "default_wine_prefix": "Default Wine prefix folder", "ignore_ood_warning": "Ignore out-of-date game launch warning" }, "performance" : { "header": "Performance", diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index eac9084b..5addfe9a 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -21,7 +21,6 @@ use maxima::{ ServiceLayerError, ServicePlayer, } }, - gameinfo::GameInstallInfo, lsx::service::LSXServerError, rtm::RtmError, util::{ @@ -494,7 +493,8 @@ impl BridgeThread { }; #[cfg(unix)] - let wine_prefix = Some(maxima_dir().unwrap().join("wine/prefixes").join(&slug)); + let wine_prefix = wine_prefix + .or_else(|| Some(maxima_dir().unwrap().join("wine/prefixes").join(&slug))); #[cfg(windows)] let wine_prefix = None; diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index b2ad7bb0..be30fd45 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -238,6 +238,7 @@ pub enum BackendStallState { pub struct InstallModalState { locate_path: String, install_folder: String, + wine_prefix: String, locating: bool, locate_response: Option, should_close: bool, @@ -248,6 +249,7 @@ impl InstallModalState { Self { locate_path: String::new(), install_folder: settings.default_install_folder.clone(), + wine_prefix: settings.default_wine_prefix_path.clone(), locating: false, locate_response: None, should_close: false, @@ -333,6 +335,7 @@ impl FrontendPerformanceSettings { #[derive(serde::Serialize, serde::Deserialize)] pub struct FrontendSettings { default_install_folder: String, + default_wine_prefix_path: String, language: FrontendLanguage, ignore_ood_games: bool, game_settings: HashMap, @@ -343,6 +346,7 @@ impl FrontendSettings { pub fn new() -> Self { Self { default_install_folder: String::new(), + default_wine_prefix_path: String::new(), language: FrontendLanguage::SystemDefault, ignore_ood_games: false, game_settings: HashMap::new(), @@ -909,14 +913,33 @@ impl MaximaEguiApp { }); let path = PathBuf::from(self.installer_state.install_folder.clone()); let valid = path.exists(); + + #[cfg(unix)] + { + ui.label("Wine prefix folder"); + ui.horizontal(|ui| { + let size = vec2(500.0 - (24.0 + ui.style().spacing.item_spacing.x), 30.0); + ui.add_sized(size, egui::TextEdit::singleline(&mut self.installer_state.wine_prefix).vertical_align(egui::Align::Center)); + }); + } + ui.add_enabled_ui(valid, |ui| { if ui.add_sized(button_size, egui::Button::new(&self.locale.localization.modals.game_install.fresh_action)).clicked() { + #[cfg(unix)] + let wine_prefix = if self.installer_state.wine_prefix.is_empty() { + None + } else { + Some(PathBuf::from(&self.installer_state.wine_prefix)) + }; + #[cfg(not(unix))] + let wine_prefix: Option = None; + if self.installing_now.is_none() { self.installing_now = Some(QueuedDownload { slug: game.slug.clone(), offer: game.offer.clone(), downloaded_bytes: 0, total_bytes: 0 }); } 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(), slug.clone(), path.join(slug), None)).unwrap(); + self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), slug.clone(), path.join(slug), wine_prefix)).unwrap(); clear = true; } @@ -927,6 +950,22 @@ impl MaximaEguiApp { egui::Label::new(egui::RichText::new(format!("{}", path.join(slug).display())).color(Color32::WHITE)).selectable(false).ui(folder_hint); }); + #[cfg(unix)] + { + let effective_prefix = if self.installer_state.wine_prefix.is_empty() { + maxima::util::native::maxima_dir() + .ok() + .map(|d| d.join("wine/prefixes").join(slug)) + .map(|p| p.display().to_string()) + .unwrap_or_default() + } else { + self.installer_state.wine_prefix.clone() + }; + ui.horizontal_wrapped(|prefix_hint| { + egui::Label::new("Wine prefix:").selectable(false).ui(prefix_hint); + egui::Label::new(egui::RichText::new(&effective_prefix).color(Color32::WHITE)).selectable(false).ui(prefix_hint); + }); + } if !valid { egui::Label::new(egui::RichText::new(&self.locale.localization.modals.game_install.fresh_path_invalid).color(Color32::RED)).ui(ui); } diff --git a/maxima-ui/src/translation_manager.rs b/maxima-ui/src/translation_manager.rs index 9e089c1b..1a7158dc 100644 --- a/maxima-ui/src/translation_manager.rs +++ b/maxima-ui/src/translation_manager.rs @@ -172,6 +172,8 @@ pub struct LocalizedGameInstallationSettings { pub header: String, /// Label for a text box for a default path to install games pub default_folder: String, + /// Label for a text box for the default Wine prefix path + pub default_wine_prefix: String, /// Checkbox for ignoring the out-of-date launch warning pub ignore_ood_warning: String, } diff --git a/maxima-ui/src/views/settings_view.rs b/maxima-ui/src/views/settings_view.rs index 4f36b128..a9f574ff 100644 --- a/maxima-ui/src/views/settings_view.rs +++ b/maxima-ui/src/views/settings_view.rs @@ -34,6 +34,23 @@ pub fn settings_view(app: &mut MaximaEguiApp, ui: &mut Ui) { ); if ui.add_sized(vec2(100.0, 30.0), egui::Button::new("BROWSE")).clicked() {} }); + + #[cfg(unix)] + { + ui.label(&localization.game_installation.default_wine_prefix); + ui.horizontal(|ui| { + ui.add_sized( + vec2( + ui.available_width() - (100.0 + ui.spacing().item_spacing.x), + 30.0, + ), + egui::TextEdit::singleline(&mut app.settings.default_wine_prefix_path) + .vertical_align(egui::Align::Center), + ); + if ui.add_sized(vec2(100.0, 30.0), egui::Button::new("BROWSE")).clicked() {} + }); + } + ui.checkbox( &mut app.settings.ignore_ood_games, &app.locale.localization.settings_view.game_installation.ignore_ood_warning, From b93ea2b46d415f3c0736933e86af721ac69b83ab Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:46:55 -0600 Subject: [PATCH 30/42] Implement deserializers, pass in wine prefix to locate game request --- maxima-cli/src/main.rs | 21 ++++++++----- maxima-lib/src/content/manager.rs | 22 ++++++++----- maxima-lib/src/core/library.rs | 4 +-- maxima-lib/src/gameinfo/mod.rs | 51 +++++++++++++++++++------------ maxima-lib/src/unix/wine.rs | 2 +- maxima-lib/src/util/registry.rs | 2 +- maxima-ui/src/bridge_thread.rs | 31 +++++++++++++------ maxima-ui/src/main.rs | 15 +++++++-- 8 files changed, 97 insertions(+), 51 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index fb37925b..86681602 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -44,6 +44,7 @@ use maxima::{ gameinfo::GameInstallInfo, ooa, rtm::client::BasicPresence, + unix::wine, util::{ log::init_logger, native::{maxima_dir, take_foreground_focus}, @@ -444,14 +445,17 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { Text::new("Where would you like to install the game? (must be an absolute path)") .prompt()?, ); - let mut game_install_info = GameInstallInfo::new(path.to_str().unwrap().to_string()); - game_install_info.wine_prefix = maxima_dir() - .unwrap() - .join("wine/prefixes") - .join(&slug) - .to_str() - .unwrap() - .to_string(); + + #[cfg(unix)] + let wine_prefix = { + let input = Text::new("If this game uses Wine, please enter the Wine prefix path (leave blank if not applicable)").prompt()?; + PathBuf::from(input) + }; + + #[cfg(not(unix))] + let wine_prefix = PathBuf::new(); + + let game_install_info = GameInstallInfo::new(path.clone(), Some(wine_prefix.clone())); game_install_info.save_to_json(&slug); if !path.is_absolute() { error!("Path {:?} is not absolute.", path); @@ -463,6 +467,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { .build_id(build.build_id().to_owned()) .path(path.clone()) .slug(slug) // Needs the slug here for the manifest touchup after installation, which needs to know the wine prefix path + .wine_prefix(Some(wine_prefix)) .build()?; let start_time = Instant::now(); diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 9ef285e5..74fe8cf5 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -18,10 +18,19 @@ use tokio_util::sync::CancellationToken; use crate::{ content::{ - ContentService, downloader::{DownloadError, ZipDownloader}, exclusion::get_exclusion_list, zip::{self, CompressionType, ZipError, ZipFileEntry} - }, core::{ - MaximaEvent, auth::storage::LockedAuthStorage, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::ServiceLayerError - }, gameinfo::GameInstallInfo, util::native::{NativeError, maxima_dir} + downloader::{DownloadError, ZipDownloader}, + exclusion::get_exclusion_list, + zip::{self, CompressionType, ZipError, ZipFileEntry}, + ContentService, + }, + core::{ + auth::storage::LockedAuthStorage, + manifest::{self, ManifestError, MANIFEST_RELATIVE_PATH}, + service_layer::ServiceLayerError, + MaximaEvent, + }, + gameinfo::GameInstallInfo, + util::native::{maxima_dir, NativeError}, }; const QUEUE_FILE: &str = "download_queue.json"; @@ -373,9 +382,8 @@ impl ContentManager { if let Some(current) = &self.current { if current.is_done() { event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - let mut game_install_info = - GameInstallInfo::new(current.path.to_str().unwrap().to_string()); - game_install_info.wine_prefix = current.wine_prefix.clone().unwrap().to_str().unwrap().to_string(); + let game_install_info = + GameInstallInfo::new(current.path.clone(), current.wine_prefix.clone()); game_install_info.save_to_json(¤t.slug); self.current = None; self.queue.current = None; diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 1eccf839..a2891dc2 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -123,9 +123,7 @@ impl OwnedOffer { Err(_) => return Ok(None), // No info file yet, placeholder for now }; - let path = game_install_info - .install_path_pathbuf() - .join(MANIFEST_RELATIVE_PATH); + let path = game_install_info.path().join(MANIFEST_RELATIVE_PATH); if !path.exists() { return Ok(None); } diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index 24d9d06e..83db76f5 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::fs; use std::path::PathBuf; @@ -18,34 +18,45 @@ pub enum GameVersionError { NotFound(String), } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GameInstallInfo { - pub path: String, - pub wine_prefix: String, +fn path_from_string<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Ok(PathBuf::from(s)) } -impl GameInstallInfo { - pub fn new(path: String) -> Self { - Self { - path, - wine_prefix: String::new(), - } +fn optional_path_from_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some(PathBuf::from(s))) } +} - pub fn path(&self) -> &str { - &self.path - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameInstallInfo { + #[serde(deserialize_with = "path_from_string")] + pub path: PathBuf, + #[serde(deserialize_with = "optional_path_from_string")] + pub wine_prefix: Option, +} - pub fn wine_prefix(&self) -> &str { - &self.wine_prefix +impl GameInstallInfo { + pub fn new(path: PathBuf, wine_prefix: Option) -> Self { + Self { path, wine_prefix } } - pub fn install_path_pathbuf(&self) -> PathBuf { - PathBuf::from(&self.path) + pub fn path(&self) -> PathBuf { + self.path.clone() } - pub fn wine_prefix_pathbuf(&self) -> PathBuf { - PathBuf::from(&self.wine_prefix) + pub fn wine_prefix(&self) -> Option { + self.wine_prefix.clone() } // TODO: Maybe we can just query the slug by the filename of the path? Look into this later diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 1c91ad0f..8f7179ee 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -78,7 +78,7 @@ struct Versions { /// Returns internal proton pfx path pub fn wine_prefix_dir(slug: Option<&str>) -> Result { let game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); - let prefix_path = game_install_info.wine_prefix_pathbuf(); + let prefix_path = game_install_info.wine_prefix().unwrap(); if !prefix_path.exists() {} if let Err(err) = create_dir_all(&prefix_path) { diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 646d9396..cdf0ff1a 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -189,7 +189,7 @@ pub async fn parse_registry_path(key: &str, slug: Option<&str>) -> Result), InstallGameRequest(String, String, PathBuf, Option), // offer, slug, path, wine prefix (unix only) - LocateGameRequest(String, String), + LocateGameRequest(String, String, Option), // slug, path, wine prefix (unix only) ShutdownRequest, } @@ -435,8 +442,11 @@ impl BridgeThread { let context = ctx.clone(); async move { game_details_request(maxima, slug.clone(), channel, &context).await }.await } - MaximaLibRequest::LocateGameRequest(slug, path) => { + MaximaLibRequest::LocateGameRequest(slug, path, wine_prefix) => { #[cfg(unix)] + let game_install_info = + GameInstallInfo::new(PathBuf::from(path.clone()), wine_prefix); // Bit of a hack here, the wine_prefix path is pulled from a json so we create it here + game_install_info.save_to_json(&slug); maxima::core::launch::mx_linux_setup(Some(&slug)).await?; let mut path = path; if path.ends_with("/") || path.ends_with("\\") { @@ -465,6 +475,10 @@ impl BridgeThread { )); } } else { + std::fs::remove_file( + maxima_dir().unwrap().join("gameinfo").join(format!("{}.json", slug)), + ) + .ok(); let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse( InteractThreadLocateGameResponse::Error( InteractThreadLocateGameFailure { @@ -491,10 +505,9 @@ impl BridgeThread { } else { continue; }; - + #[cfg(unix)] - let wine_prefix = wine_prefix - .or_else(|| Some(maxima_dir().unwrap().join("wine/prefixes").join(&slug))); + let wine_prefix = wine_prefix; // We handle empty cases in the UI, so we can just pass it through here #[cfg(windows)] let wine_prefix = None; diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index be30fd45..fded44ef 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -31,6 +31,7 @@ use egui_glow::glow; use app_bg_renderer::AppBgRenderer; use bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameResponse}; use game_view_bg_renderer::GameViewBgRenderer; +use maxima::util::native::maxima_dir; use renderers::{app_bg_renderer, game_view_bg_renderer}; use translation_manager::{positional_replace, TranslationManager}; @@ -895,7 +896,16 @@ 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(slug.clone(), self.installer_state.locate_path.clone())).unwrap(); + #[cfg(unix)] + let wine_prefix = if self.installer_state.wine_prefix.is_empty() { + Some(maxima_dir().unwrap().join("wine/prefixes").join(slug)) + } else { + Some(PathBuf::from(&self.installer_state.wine_prefix)) + }; + #[cfg(not(unix))] + let wine_prefix: Option = None; + + self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::LocateGameRequest(slug.clone(), self.installer_state.locate_path.clone(), wine_prefix)).unwrap(); self.installer_state.locating = true; } }); @@ -927,7 +937,8 @@ impl MaximaEguiApp { if ui.add_sized(button_size, egui::Button::new(&self.locale.localization.modals.game_install.fresh_action)).clicked() { #[cfg(unix)] let wine_prefix = if self.installer_state.wine_prefix.is_empty() { - None + + Some(maxima_dir().unwrap().join("wine/prefixes").join(slug)) } else { Some(PathBuf::from(&self.installer_state.wine_prefix)) }; From 6a436a60bd03035195181b9520a2eb60ece4d774 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:17:25 -0600 Subject: [PATCH 31/42] Fix a couple of things for windows, handle null if wine_prefix is None --- maxima-cli/src/main.rs | 1 - maxima-lib/src/core/library.rs | 3 +++ maxima-lib/src/gameinfo/mod.rs | 15 ++++++++++++++- maxima-ui/src/bridge_thread.rs | 5 ++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 86681602..62069373 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -44,7 +44,6 @@ use maxima::{ gameinfo::GameInstallInfo, ooa, rtm::client::BasicPresence, - unix::wine, util::{ log::init_logger, native::{maxima_dir, take_foreground_focus}, diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index a2891dc2..e9a394af 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -17,6 +17,7 @@ use crate::{ util::native::{maxima_dir, NativeError, SafeStr}, }; use derive_getters::Getters; +use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; @@ -118,12 +119,14 @@ impl OwnedOffer { } pub async fn local_manifest(&self) -> Result>, ManifestError> { + info!("Checking local manifest for `{}`", self.slug); let game_install_info = match load_game_version_from_json(&self.slug) { Ok(info) => info, Err(_) => return Ok(None), // No info file yet, placeholder for now }; let path = game_install_info.path().join(MANIFEST_RELATIVE_PATH); + info!("Checking for manifest at `{}`", path.display()); if !path.exists() { return Ok(None); } diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index 83db76f5..abac7d42 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -38,11 +38,24 @@ where } } +fn optional_path_to_string(value: &Option, serializer: S) -> Result +where + S: serde::Serializer, +{ + match value { + None => serializer.serialize_str(""), + Some(path) => serializer.serialize_str(path.to_string_lossy().as_ref()), + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GameInstallInfo { #[serde(deserialize_with = "path_from_string")] pub path: PathBuf, - #[serde(deserialize_with = "optional_path_from_string")] + #[serde( + deserialize_with = "optional_path_from_string", + serialize_with = "optional_path_to_string" + )] pub wine_prefix: Option, } diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 65c697a0..b63bac7b 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,7 +1,5 @@ use egui::Context; use log::{error, info, warn}; -#[cfg(unix)] -use maxima::gameinfo::GameInstallInfo; use crate::{ bridge::{ @@ -28,6 +26,7 @@ use maxima::{ }, LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, }, + gameinfo::GameInstallInfo, lsx::service::LSXServerError, rtm::RtmError, util::{ @@ -443,10 +442,10 @@ impl BridgeThread { async move { game_details_request(maxima, slug.clone(), channel, &context).await }.await } MaximaLibRequest::LocateGameRequest(slug, path, wine_prefix) => { - #[cfg(unix)] let game_install_info = GameInstallInfo::new(PathBuf::from(path.clone()), wine_prefix); // Bit of a hack here, the wine_prefix path is pulled from a json so we create it here game_install_info.save_to_json(&slug); + #[cfg(unix)] maxima::core::launch::mx_linux_setup(Some(&slug)).await?; let mut path = path; if path.ends_with("/") || path.ends_with("\\") { From 77b10328bb341911a2354824cd2a0a7c1b4e892e Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:47:58 -0600 Subject: [PATCH 32/42] Create gameinfo just before touchup --- maxima-lib/src/content/manager.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 74fe8cf5..01b6d2e2 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; +use winapi::um::winnt::SUBLANG_FULAH_SENEGAL; use crate::{ content::{ @@ -194,6 +195,9 @@ 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(); + let game_install_info = + GameInstallInfo::new(self.path.clone(), self.wine_prefix.clone()); tokio::spawn(async move { let dl = GameDownloader::start_downloads( total_count, @@ -202,6 +206,8 @@ impl GameDownloader { cancel_token, completed_bytes, notify, + slug, + game_install_info, ) .await; if let Err(err) = dl { @@ -229,12 +235,15 @@ impl GameDownloader { } async fn start_downloads( + total_count: usize, downloader_arc: Arc, entries: Vec, cancel_token: CancellationToken, completed_bytes: Arc, notify: Arc, + slug: String, + game_install_info: GameInstallInfo, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(total_count); @@ -272,10 +281,11 @@ impl GameDownloader { let path = downloader_arc.path(); + game_install_info.save_to_json(&slug); info!("Files downloaded, running touchup..."); - // let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - // manifest.run_touchup(path, &slug).await?; + let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; + manifest.run_touchup(path, &slug).await?; info!("Installation finished!"); @@ -382,9 +392,6 @@ impl ContentManager { if let Some(current) = &self.current { if current.is_done() { event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - let game_install_info = - GameInstallInfo::new(current.path.clone(), current.wine_prefix.clone()); - game_install_info.save_to_json(¤t.slug); self.current = None; self.queue.current = None; From d5097da37f63cb71b5ea9aeaf92d676f7000a47e Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:03:06 -0600 Subject: [PATCH 33/42] Remove accidental import --- maxima-lib/src/content/manager.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 01b6d2e2..f844a9ce 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -15,7 +15,6 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; -use winapi::um::winnt::SUBLANG_FULAH_SENEGAL; use crate::{ content::{ From c869a7d95e39c957c043324909b054163709be82 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:12:06 -0600 Subject: [PATCH 34/42] Fix wording for wine prefix prompt --- maxima-cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 62069373..df45dbab 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -447,7 +447,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { #[cfg(unix)] let wine_prefix = { - let input = Text::new("If this game uses Wine, please enter the Wine prefix path (leave blank if not applicable)").prompt()?; + let input = Text::new("Where do you want to store the Wine prefix? (must be an absolute path)").prompt()?; PathBuf::from(input) }; From c17c6d7594f352778a555e551a0dc1392afc5aff Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:43:12 -0600 Subject: [PATCH 35/42] Cargo fmt run --- maxima-cli/src/main.rs | 4 +++- maxima-lib/src/content/manager.rs | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index df45dbab..5112ea3c 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -447,7 +447,9 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { #[cfg(unix)] let wine_prefix = { - let input = Text::new("Where do you want to store the Wine prefix? (must be an absolute path)").prompt()?; + let input = + Text::new("Where do you want to store the Wine prefix? (must be an absolute path)") + .prompt()?; PathBuf::from(input) }; diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f844a9ce..2ca7c96f 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -195,8 +195,7 @@ impl GameDownloader { self.prepare_download_vars(); let total_count = self.total_count; let slug = self.slug.clone(); - let game_install_info = - GameInstallInfo::new(self.path.clone(), self.wine_prefix.clone()); + let game_install_info = GameInstallInfo::new(self.path.clone(), self.wine_prefix.clone()); tokio::spawn(async move { let dl = GameDownloader::start_downloads( total_count, @@ -234,7 +233,6 @@ impl GameDownloader { } async fn start_downloads( - total_count: usize, downloader_arc: Arc, entries: Vec, From 88cabed69fd6a3c2945444c49d84b80cb6265e0d Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:57:03 -0600 Subject: [PATCH 36/42] Dont create gameinfo file before installation is finished for maxima-cli --- maxima-cli/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 5112ea3c..992a8b70 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -456,8 +456,6 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { #[cfg(not(unix))] let wine_prefix = PathBuf::new(); - let game_install_info = GameInstallInfo::new(path.clone(), Some(wine_prefix.clone())); - game_install_info.save_to_json(&slug); if !path.is_absolute() { error!("Path {:?} is not absolute.", path); return Ok(()); From 854d63ce89e242b880cfe7c3c9f75deb40057c84 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:45:07 -0600 Subject: [PATCH 37/42] Remove unnecessary gameinfo serializer --- maxima-lib/src/gameinfo/mod.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index abac7d42..a68dabcd 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -18,15 +18,9 @@ pub enum GameVersionError { NotFound(String), } -fn path_from_string<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - Ok(PathBuf::from(s)) -} +// The serializers are for making sure that None goes to and from an empty string -fn optional_path_from_string<'de, D>(deserializer: D) -> Result, D::Error> +fn prefix_from_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { @@ -38,7 +32,7 @@ where } } -fn optional_path_to_string(value: &Option, serializer: S) -> Result +fn prefix_to_string(value: &Option, serializer: S) -> Result where S: serde::Serializer, { @@ -50,11 +44,10 @@ where #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GameInstallInfo { - #[serde(deserialize_with = "path_from_string")] pub path: PathBuf, #[serde( - deserialize_with = "optional_path_from_string", - serialize_with = "optional_path_to_string" + deserialize_with = "prefix_from_string", + serialize_with = "prefix_to_string" )] pub wine_prefix: Option, } From afa5bc1224b46e5243992a07f5ab84b41fb15f25 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:11:26 -0500 Subject: [PATCH 38/42] Register games with gameinfo system by registry detection --- maxima-lib/src/core/library.rs | 39 +++++++++++++++++++++++++++----- maxima-lib/src/gameinfo/mod.rs | 3 ++- maxima-lib/src/util/registry.rs | 40 +++++++++++++++++++++++++-------- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index e9a394af..f892e563 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -11,15 +11,17 @@ use super::{ SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, }, }; -use crate::util::registry::{parse_registry_path, RegistryError}; +use crate::gameinfo::GameInstallInfo; +use crate::util::registry::{parse_registry_path_json, RegistryError}; use crate::{ - gameinfo::load_game_version_from_json, + gameinfo::load_game_info_from_json, util::native::{maxima_dir, NativeError, SafeStr}, }; use derive_getters::Getters; use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; +use winapi::shared::cfg; #[derive(Error, Debug)] pub enum LibraryError { @@ -56,6 +58,24 @@ pub struct OwnedOffer { } impl OwnedOffer { + #[cfg(windows)] + pub async fn check_install_win_registry(&self) -> bool { + use crate::util::registry::parse_registry_path_regkey; + + let path = match self.offer.install_check_override() { + Some(path) => path, + None => return false, + }; + if let Ok(manifest_path) = parse_registry_path_regkey(path).await { + let gamedir = manifest_path.ancestors().nth(2).unwrap().to_path_buf(); // Strip off the manifest and just leave the game directory + let game_install_info: GameInstallInfo = GameInstallInfo::new(gamedir, None); + game_install_info.save_to_json(&self.slug); + manifest_path.exists() + } else { + false + } + } + pub async fn is_installed(&self) -> bool { let maxima_dir = match maxima_dir() { Ok(dir) => dir, @@ -65,11 +85,20 @@ impl OwnedOffer { let game_info_path = maxima_dir .join("gameinfo") .join(format!("{}.json", &self.slug)); + + #[cfg(windows)] + match game_info_path.exists() { + true => return true, + false => return self.check_install_win_registry().await, + } + + #[cfg(not(windows))] game_info_path.exists() } + // This is unused pub async fn install_check_path(&self) -> Result { - Ok(parse_registry_path( + Ok(parse_registry_path_json( &self .offer .install_check_override() @@ -95,7 +124,7 @@ impl OwnedOffer { }; if let Some(path) = path { - Ok(parse_registry_path(path, Some(&self.slug)).await?) + Ok(parse_registry_path_json(path, Some(&self.slug)).await?) } else { Err(LibraryError::NoPath(self.slug.clone())) } @@ -120,7 +149,7 @@ impl OwnedOffer { pub async fn local_manifest(&self) -> Result>, ManifestError> { info!("Checking local manifest for `{}`", self.slug); - let game_install_info = match load_game_version_from_json(&self.slug) { + let game_install_info = match load_game_info_from_json(&self.slug) { Ok(info) => info, Err(_) => return Ok(None), // No info file yet, placeholder for now }; diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index a68dabcd..698d148f 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -77,7 +77,8 @@ impl GameInstallInfo { } } } -pub fn load_game_version_from_json(slug: &str) -> Result { + +pub fn load_game_info_from_json(slug: &str) -> Result { let mut path = maxima_dir(); path.as_mut().unwrap().push("gameinfo"); path.as_mut().unwrap().push(format!("{}.json", slug)); diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index cdf0ff1a..3d76fd52 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -1,6 +1,7 @@ #[cfg(windows)] extern crate winapi; +use log::info; use std::{path::PathBuf, str::FromStr}; use thiserror::Error; @@ -32,7 +33,7 @@ use winreg::{ #[cfg(unix)] use std::{collections::HashMap, env, fs}; -use crate::gameinfo::load_game_version_from_json; +use crate::gameinfo::load_game_info_from_json; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; @@ -176,18 +177,39 @@ async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, Ok(None) } -#[cfg(false)] // Unused method for -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, slug).await?) +pub async fn parse_registry_path_regkey(key: &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, None).await? { + Some(path) => path.replace("\\", "/").replace("//", "/"), + None => return Ok(PathBuf::from(key.to_owned())), + }; + + let second = second.replace("\\", "/"); + let second = second.strip_prefix("/").unwrap_or(&second); + + return Ok([path, second.to_owned()].iter().collect()); + } else { + PathBuf::from(key.to_owned()) + }; + + #[cfg(unix)] + let path = case_insensitive_path(path); + Ok(path) } -pub async fn parse_registry_path(key: &str, slug: Option<&str>) -> Result { - let game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); +pub async fn parse_registry_path_json( + key: &str, + slug: Option<&str>, +) -> Result { + let game_install_info = + load_game_info_from_json(slug.unwrap()).map_err(|_| RegistryError::InvalidInstallKey)?; let idx = key.rfind(']'); - // Path looks like [HKEY_LOCAL_MACHINE\SOFTWARE\BioWare\Mass Effect Legendary Edition\Install Dir]Game\Launcher\MassEffectLauncher.exe + // Path looks like [HKEY_LOCAL_MACHINE\SOFTWARE\BioWare\Mass Effect Legendary Edition\Install Dir]Game\Launcher\MassEffectLauncher.exe (note this could be something other than the exe like the manifest) // Extract everything after the last ] and append it to the install path - // TODO: Maybe normalize path to OS? let after_bracket = &key[(idx.unwrap() + 1)..]; let path = game_install_info.path().join(after_bracket); #[cfg(unix)] From 1a5124607dfbf72b23f301af7e1ee2394b2c8c10 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:58:23 -0500 Subject: [PATCH 39/42] Fix linux compile errors --- maxima-lib/src/core/library.rs | 2 +- maxima-lib/src/unix/wine.rs | 4 ++-- maxima-lib/src/util/registry.rs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index f892e563..7bea23d5 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -21,7 +21,7 @@ use derive_getters::Getters; use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; -use winapi::shared::cfg; +// use winapi::shared::cfg; #[derive(Error, Debug)] pub enum LibraryError { diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 8f7179ee..11b1e8f4 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::{ - gameinfo::load_game_version_from_json, + gameinfo::load_game_info_from_json, util::{ github::{ fetch_github_release, fetch_github_releases, github_download_asset, GithubRelease, @@ -77,7 +77,7 @@ struct Versions { /// Returns internal proton pfx path pub fn wine_prefix_dir(slug: Option<&str>) -> Result { - let game_install_info = load_game_version_from_json(slug.unwrap()).unwrap(); + let game_install_info = load_game_info_from_json(slug.unwrap()).unwrap(); let prefix_path = game_install_info.wine_prefix().unwrap(); if !prefix_path.exists() {} diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 3d76fd52..39afbba3 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -177,6 +177,7 @@ async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, Ok(None) } +#[cfg(windows)] pub async fn parse_registry_path_regkey(key: &str) -> Result { let mut parts = key .split(|c| c == '[' || c == ']') From 99d71d1e26ec397e65d1db5f6689d4183afedb4b Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:07:15 -0500 Subject: [PATCH 40/42] Run touchup only when launching --- maxima-cli/src/main.rs | 2 -- maxima-lib/src/content/manager.rs | 13 +++++++++---- maxima-lib/src/core/launch.rs | 12 +++++++++++- maxima-lib/src/core/library.rs | 5 +++-- maxima-lib/src/core/manifest/dip.rs | 11 +++-------- maxima-lib/src/core/manifest/pre_dip.rs | 11 +++-------- maxima-lib/src/util/registry.rs | 2 +- maxima-ui/src/bridge_thread.rs | 4 +--- 8 files changed, 31 insertions(+), 29 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 992a8b70..3fe77414 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -805,8 +805,6 @@ async fn list_games(maxima_arc: LockedMaxima) -> 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, slug).await?; info!("Installed!"); Ok(()) } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 2ca7c96f..7cbc64a3 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -279,10 +279,15 @@ impl GameDownloader { let path = downloader_arc.path(); game_install_info.save_to_json(&slug); - info!("Files downloaded, running touchup..."); - - let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(path, &slug).await?; + info!("Files downloaded"); + + #[cfg(windows)] + // Touchup will be run on linux/mac when first running the game, so we don't need to run it here + { + info!("Running touchup..."); + let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; + manifest.run_touchup(path, &slug).await?; + } info!("Installation finished!"); diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 3953c89b..b68dfe9f 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -31,7 +31,11 @@ use crate::{ use thiserror::Error; #[cfg(unix)] -use crate::unix::fs::case_insensitive_path; +use crate::{ + core::manifest::{self, MANIFEST_RELATIVE_PATH}, + gameinfo::load_game_info_from_json, + unix::fs::case_insensitive_path, +}; use serde::{Deserialize, Serialize}; @@ -447,6 +451,12 @@ pub async fn mx_linux_setup(slug: Option<&str>) -> Result<(), NativeError> { } setup_wine_registry(slug).await?; + let install_path = load_game_info_from_json(slug.unwrap()).unwrap().path; + let manifest = manifest::read(install_path.join(MANIFEST_RELATIVE_PATH)) + .await + .unwrap(); + + let _result = manifest.run_touchup(&install_path, slug.unwrap()).await; Ok(()) } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 7bea23d5..82b2f3c8 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -20,8 +20,9 @@ use crate::{ use derive_getters::Getters; use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; -use thiserror::Error; -// use winapi::shared::cfg; + +#[cfg(windows)] +use winapi::shared::cfg; #[derive(Error, Debug)] pub enum LibraryError { diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 001b042d..76098163 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -196,16 +196,11 @@ impl DiPManifest { install_path: &PathBuf, slug: &str, ) -> Result<(), ManifestError> { - use crate::{ - core::launch::mx_linux_setup, - unix::{ - fs::case_insensitive_path, - wine::{invalidate_mx_wine_registry, run_wine_command, CommandType}, - }, + use crate::unix::{ + fs::case_insensitive_path, + wine::{invalidate_mx_wine_registry, run_wine_command, CommandType}, }; - mx_linux_setup(Some(slug)).await?; - let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, )); diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 1f71c972..f549a016 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -111,16 +111,11 @@ impl PreDiPManifest { install_path: &PathBuf, slug: &str, ) -> Result<(), ManifestError> { - use crate::{ - core::launch::mx_linux_setup, - unix::{ - fs::case_insensitive_path, - wine::{invalidate_mx_wine_registry, run_wine_command, CommandType}, - }, + use crate::unix::{ + fs::case_insensitive_path, + wine::{invalidate_mx_wine_registry, run_wine_command, CommandType}, }; - mx_linux_setup(Some(slug)).await?; - let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, )); diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 39afbba3..9f926cf9 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -177,7 +177,7 @@ async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, Ok(None) } -#[cfg(windows)] +#[cfg(windows)] // This is only used for checking if a game is installed on windows. Just keep it out of the way on unix pub async fn parse_registry_path_regkey(key: &str) -> Result { let mut parts = key .split(|c| c == '[' || c == ']') diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index b63bac7b..d47715f7 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -443,10 +443,8 @@ impl BridgeThread { } MaximaLibRequest::LocateGameRequest(slug, path, wine_prefix) => { let game_install_info = - GameInstallInfo::new(PathBuf::from(path.clone()), wine_prefix); // Bit of a hack here, the wine_prefix path is pulled from a json so we create it here + GameInstallInfo::new(PathBuf::from(path.clone()), wine_prefix); game_install_info.save_to_json(&slug); - #[cfg(unix)] - 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); From 142a782f5c6a80d641f62a1b3c167d5c382bd462 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:10:10 -0500 Subject: [PATCH 41/42] Fix accidently removed import --- maxima-cli/src/main.rs | 3 --- maxima-lib/src/core/library.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 3fe77414..325ce18a 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -32,8 +32,6 @@ use maxima::{ clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, - library::OwnedTitle, - manifest::{self, MANIFEST_RELATIVE_PATH}, service_layer::{ ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceLegacyOffer, ServicePlayer, SERVICE_REQUEST_GETBASICPLAYER, @@ -41,7 +39,6 @@ use maxima::{ }, LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, }, - gameinfo::GameInstallInfo, ooa, rtm::client::BasicPresence, util::{ diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 82b2f3c8..6eca996b 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -11,7 +11,6 @@ use super::{ SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, }, }; -use crate::gameinfo::GameInstallInfo; use crate::util::registry::{parse_registry_path_json, RegistryError}; use crate::{ gameinfo::load_game_info_from_json, @@ -20,6 +19,7 @@ use crate::{ use derive_getters::Getters; use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; +use thiserror::Error; #[cfg(windows)] use winapi::shared::cfg; From c02a148c268fe200395ba59255fc3069455959a1 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:54:23 -0500 Subject: [PATCH 42/42] Missing import --- maxima-lib/src/core/library.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 6eca996b..b8a09c3a 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -68,6 +68,8 @@ impl OwnedOffer { None => return false, }; if let Ok(manifest_path) = parse_registry_path_regkey(path).await { + use crate::gameinfo::GameInstallInfo; + let gamedir = manifest_path.ancestors().nth(2).unwrap().to_path_buf(); // Strip off the manifest and just leave the game directory let game_install_info: GameInstallInfo = GameInstallInfo::new(gamedir, None); game_install_info.save_to_json(&self.slug);