Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fafd226
File Exclusion
sjp761 Jan 24, 2026
c9b5b5b
Ability to use SLR and custom proton (could be very much refined)
sjp761 Jan 25, 2026
2c4612d
Partial work for settings implementation
sjp761 Jan 27, 2026
c607d42
Update UI code to account for GameSettingsManager
sjp761 Jan 27, 2026
eb16cae
Installation based on JSON settings
sjp761 Jan 30, 2026
fbc452a
Check for boolean in is_installed, fix wrong method when reading game…
sjp761 Jan 30, 2026
50b9632
Only write settings file if game is installed
sjp761 Jan 30, 2026
07dc67e
Run cargo fmt
sjp761 Jan 30, 2026
a2a0d08
Seperate wine prefixes protype
sjp761 Feb 4, 2026
42ae6c9
Revert "Ability to use SLR and custom proton (could be very much refi…
sjp761 Feb 4, 2026
c1b4756
Use slug instead of offer id for exclusion file
sjp761 Feb 5, 2026
0aba2e1
Use string slice instead of String for exclusion
sjp761 Feb 5, 2026
7f0aa73
Correct more String parameters to &str
sjp761 Feb 5, 2026
9d3dabe
Implement universal hashmap to for custom prefix location (read from …
sjp761 Feb 6, 2026
63353e8
Cargo fmt run
sjp761 Feb 6, 2026
815b916
Fix some warnings
sjp761 Feb 6, 2026
60e1276
Ensure consistency of prefix hashmap
sjp761 Feb 6, 2026
a26c4ea
Store game path and wine prefix in file, detect version based on inst…
sjp761 Feb 17, 2026
5bc6c32
Slighly refactor game settings struct, PCSign uses default installati…
sjp761 Feb 18, 2026
b16c10e
Make install check see if game info file exists (Still need to create…
sjp761 Feb 18, 2026
3644819
Create wine prefix folder if it does not exist for slug
sjp761 Feb 18, 2026
b796bd2
Query executable path from JSON
sjp761 Feb 19, 2026
d78a6d0
Exclude unused methods from build
sjp761 Feb 19, 2026
899a502
Pass in slug where needed in maxima-cli
sjp761 Feb 19, 2026
0a7f5fa
Fix game installation with maxima-cli
sjp761 Feb 19, 2026
d59e701
Fix compile errors on WIndows
sjp761 Feb 19, 2026
b16d296
Separate game settings refactor from wine prefix support
sjp761 Feb 23, 2026
5213c0c
Prep work for UI options
sjp761 Feb 23, 2026
a96409a
UI stuff
sjp761 Feb 23, 2026
b93ea2b
Implement deserializers, pass in wine prefix to locate game request
sjp761 Feb 23, 2026
6a436a6
Fix a couple of things for windows, handle null if wine_prefix is None
sjp761 Feb 23, 2026
77b1032
Create gameinfo just before touchup
sjp761 Feb 23, 2026
d5097da
Remove accidental import
sjp761 Feb 23, 2026
c869a7d
Fix wording for wine prefix prompt
sjp761 Feb 23, 2026
c17c6d7
Cargo fmt run
sjp761 Feb 23, 2026
88cabed
Dont create gameinfo file before installation is finished for maxima-cli
sjp761 Feb 23, 2026
854d63c
Remove unnecessary gameinfo serializer
sjp761 Feb 23, 2026
afa5bc1
Register games with gameinfo system by registry detection
sjp761 Mar 9, 2026
1a51246
Fix linux compile errors
sjp761 Mar 11, 2026
99d71d1
Run touchup only when launching
sjp761 Mar 11, 2026
142a782
Fix accidently removed import
sjp761 Mar 11, 2026
c02a148
Missing import
sjp761 Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions maxima-bootstrap/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> {
None,
false,
CommandType::WaitForExitAndRun,
Some(&args.slug),
)
.await?;

Expand Down
83 changes: 57 additions & 26 deletions maxima-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -43,7 +41,11 @@ use maxima::{
},
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! {
Expand All @@ -70,6 +72,7 @@ enum Mode {
ListGames,
LocateGame {
path: String,
slug: String,
},
CloudSync {
game_slug: String,
Expand Down Expand Up @@ -295,13 +298,21 @@ 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 } => 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
}
Expand Down Expand Up @@ -355,10 +366,9 @@ 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 owned_games = Vec::new();
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;
Expand All @@ -367,25 +377,27 @@ 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::<Vec<String>>();

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::<Vec<String>>()
};

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();
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(())
}

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 {
Expand All @@ -402,10 +414,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()
Expand All @@ -423,6 +441,18 @@ 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()?,
);

#[cfg(unix)]
let wine_prefix = {
let input =
Text::new("Where do you want to store the Wine prefix? (must be an absolute path)")
.prompt()?;
PathBuf::from(input)
};

#[cfg(not(unix))]
let wine_prefix = PathBuf::new();

if !path.is_absolute() {
error!("Path {:?} is not absolute.", path);
return Ok(());
Expand All @@ -432,6 +462,8 @@ 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
.wine_prefix(Some(wine_prefix))
.build()?;

let start_time = Instant::now();
Expand Down Expand Up @@ -622,7 +654,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

Expand Down Expand Up @@ -666,8 +698,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()),
Expand Down Expand Up @@ -768,10 +800,8 @@ 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?;
info!("Installed!");
Ok(())
}
Expand Down Expand Up @@ -809,6 +839,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<String>,
game_args: Vec<String>,
login: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions maxima-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion maxima-lib/src/content/downloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions maxima-lib/src/content/exclusion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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};

pub fn get_exclusion_list(slug: &str) -> 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(&slug); // Path to exclusion file
info!("Loading exclusion file from {}", filepath.display());

if let Ok(file) = File::open(&filepath) {
let reader = BufReader::new(file);
for line in reader.lines().flatten() {
let entry = line.trim();
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 {
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
}
37 changes: 33 additions & 4 deletions maxima-lib/src/content/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use tokio_util::sync::CancellationToken;
use crate::{
content::{
downloader::{DownloadError, ZipDownloader},
exclusion::get_exclusion_list,
zip::{self, CompressionType, ZipError, ZipFileEntry},
ContentService,
},
Expand All @@ -28,6 +29,7 @@ use crate::{
service_layer::ServiceLayerError,
MaximaEvent,
},
gameinfo::GameInstallInfo,
util::native::{maxima_dir, NativeError},
};

Expand All @@ -38,6 +40,8 @@ pub struct QueuedGame {
offer_id: String,
build_id: String,
path: PathBuf,
slug: String,
wine_prefix: Option<PathBuf>,
}

#[derive(Default, Getters, Serialize, Deserialize)]
Expand Down Expand Up @@ -124,6 +128,9 @@ impl DownloadQueue {

pub struct GameDownloader {
offer_id: String,
slug: String,
path: PathBuf,
wine_prefix: Option<PathBuf>,

downloader: Arc<ZipDownloader>,
entries: Vec<ZipFileEntry>,
Expand All @@ -149,8 +156,15 @@ impl GameDownloader {
let downloader = ZipDownloader::new(&game.offer_id, &url.url(), &game.path).await?;

let mut entries = Vec::new();

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()); Spams if a lot of files are excluded
continue;
}
entries.push(ele.clone());
}

Expand All @@ -163,7 +177,9 @@ 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(),
Expand All @@ -178,6 +194,8 @@ 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,
Expand All @@ -186,6 +204,8 @@ impl GameDownloader {
cancel_token,
completed_bytes,
notify,
slug,
game_install_info,
)
.await;
if let Err(err) = dl {
Expand Down Expand Up @@ -219,6 +239,8 @@ impl GameDownloader {
cancel_token: CancellationToken,
completed_bytes: Arc<AtomicUsize>,
notify: Arc<Notify>,
slug: String,
game_install_info: GameInstallInfo,
) -> Result<(), DownloaderError> {
let mut handles = Vec::with_capacity(total_count);

Expand Down Expand Up @@ -256,10 +278,17 @@ impl GameDownloader {

let path = downloader_arc.path();

info!("Files downloaded, running touchup...");
let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?;
game_install_info.save_to_json(&slug);
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?;
}

manifest.run_touchup(path).await?;
info!("Installation finished!");

completed_bytes.fetch_add(1, Ordering::SeqCst);
Expand Down
1 change: 1 addition & 0 deletions maxima-lib/src/content/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::core::{
};

pub mod downloader;
pub mod exclusion;
pub mod manager;
pub mod zip;
pub mod zlib;
Expand Down
Loading