From e57f232ef6d5ec47533920f2168552fe68fd4fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Mon, 4 May 2026 15:47:44 +0200 Subject: [PATCH 01/57] Refactor inGamePresenceData GraphQL query From igoMain chunk --- .../src/core/graphql/inGamePresenceData.gql | 70 ++++++++----------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/maxima-lib/src/core/graphql/inGamePresenceData.gql b/maxima-lib/src/core/graphql/inGamePresenceData.gql index c3c351e4..80844131 100644 --- a/maxima-lib/src/core/graphql/inGamePresenceData.gql +++ b/maxima-lib/src/core/graphql/inGamePresenceData.gql @@ -1,50 +1,38 @@ query inGamePresenceData($offerIds: [String!]!, $locale: Locale = "en-us") { - gameProducts(locale: $locale, offerIds: $offerIds) { - items { - id - gameSlug - baseItem(availabilities: [VISIBLE]) { + gameProducts(locale: $locale, offerIds: $offerIds) { + items { id - title - shortDescription - baseGameSlug - lifecycleStatus { - revealDate - __typename - } - gameType - playableInPlatforms - prereleaseGameType - keyArt { + gameSlug + baseItem(availabilities: [VISIBLE]) { + id title - aspect1x1Image { - path - height - width - __typename - } - aspect16x9Image { - path - height - width - __typename + shortDescription + baseGameSlug + gameType + playableInPlatforms + prereleaseGameType + keyArt { + title + aspect1x1Image { + path + height + width + } + aspect16x9Image { + path + height + width + } } - __typename - } - primaryLogo { - title - largestImage { - path - height - width - __typename + primaryLogo { + title + largestImage { + path + height + width + } } - __typename } - __typename } - __typename } - __typename } -} From 0d67350049040462d36417f123b0141b5f8d7285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Mon, 4 May 2026 15:52:39 +0200 Subject: [PATCH 02/57] Add connection directive to SearchPlayer query --- maxima-lib/src/core/graphql/SearchPlayer.gql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxima-lib/src/core/graphql/SearchPlayer.gql b/maxima-lib/src/core/graphql/SearchPlayer.gql index dd67bbf0..923516d3 100644 --- a/maxima-lib/src/core/graphql/SearchPlayer.gql +++ b/maxima-lib/src/core/graphql/SearchPlayer.gql @@ -1,5 +1,5 @@ query SearchPlayer($searchText: String!, $pageNumber: Int!, $pageSize: Int!, $isMutualFriendsEnabled: Boolean = false) { - players(searchText: $searchText, paging: {pageNumber: $pageNumber, pageSize: $pageSize}) { + players(searchText: $searchText, paging: {pageNumber: $pageNumber, pageSize: $pageSize}) @connection(key: "playersBySearchText", filter: ["searchText"]) { items { ...PlayerWithMutualFriendsCount __typename From 88e02eb95f2636405d46cd53c317c392b7a92710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Mon, 4 May 2026 15:56:52 +0200 Subject: [PATCH 03/57] Add GraphQL query for AddOnGameCard --- maxima-lib/src/core/graphql/AddOnGameCard.gql | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 maxima-lib/src/core/graphql/AddOnGameCard.gql diff --git a/maxima-lib/src/core/graphql/AddOnGameCard.gql b/maxima-lib/src/core/graphql/AddOnGameCard.gql new file mode 100644 index 00000000..e338fca6 --- /dev/null +++ b/maxima-lib/src/core/graphql/AddOnGameCard.gql @@ -0,0 +1,28 @@ +query AddOnGameCard($locale: Locale!, $slug: String!) { + game(locale: $locale, slug: $slug) { + id + title + keyArt { + aspect1x1Image { + path + } + } + baseGame { + id + keyArt { + aspect1x1Image { + path + } + } + } + products { + items { + originOfferId + lifecycleStatus { + ...lifecycleStatusFragment + } + } + } + ...gamePlatformsFragment + } +} From 10725a745401575031eeadbe2e143927f1ea83f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Mon, 4 May 2026 15:58:35 +0200 Subject: [PATCH 04/57] Add lifecycleStatusFragment to GraphQL schema --- maxima-lib/src/core/graphql/AddOnGameCard.gql | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/maxima-lib/src/core/graphql/AddOnGameCard.gql b/maxima-lib/src/core/graphql/AddOnGameCard.gql index e338fca6..a655d4e6 100644 --- a/maxima-lib/src/core/graphql/AddOnGameCard.gql +++ b/maxima-lib/src/core/graphql/AddOnGameCard.gql @@ -26,3 +26,16 @@ query AddOnGameCard($locale: Locale!, $slug: String!) { ...gamePlatformsFragment } } + +fragment lifecycleStatusFragment on LifecycleStatus { + lifecycleType + downloadDate + playableStartDate + playableEndDate + acquisitionStartDate + acquisitionEndDate + revealDate + sunsetDate + isEarlyAccess + __typename +} From 8c16ecb2a724eaf0499b2e552057e616fb7c0162 Mon Sep 17 00:00:00 2001 From: Omega Date: Mon, 1 Jun 2026 11:31:35 +0200 Subject: [PATCH 05/57] use stable functions + game download safeguard --- maxima-lib/src/lib.rs | 6 ------ maxima-lib/src/lsx/connection.rs | 6 ++++-- maxima-lib/src/ooa/mod.rs | 4 +++- maxima-ui/src/bridge/get_games.rs | 11 ++++++++++- maxima-ui/src/main.rs | 1 - maxima-ui/src/ui_image.rs | 8 ++++---- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/maxima-lib/src/lib.rs b/maxima-lib/src/lib.rs index 0d878dbe..7236c077 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -1,9 +1,3 @@ -#![feature(type_ascription)] -#![feature(slice_pattern)] -#![feature(string_remove_matches)] -#![feature(trait_alias)] -#![feature(type_alias_impl_trait)] - pub mod content; pub mod core; pub mod lsx; diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index ec7b6839..e60d1fe6 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -269,7 +269,7 @@ impl Connection { // State - pub async fn maxima(&self) -> MutexGuard { + pub async fn maxima(&self) -> MutexGuard<'_, Maxima> { self.maxima.lock().await } @@ -348,7 +348,9 @@ impl Connection { debug!("Received LSX Message: {}", message); let mut message = message.to_string(); - message.remove_matches("version=\"\" "); + // replace unstable remove_matches (when most of the devs seem to think that it's stable since 2024 ??) w/ regex + let re = regex::Regex::new(r#"version="" "#).unwrap(); + message = re.replace_all(&message, "").to_string(); let lsx_message: LSX = quick_xml::de::from_str(message.as_str())?; let state = self.state.clone(); diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index c61cf76e..a55afd13 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -317,7 +317,9 @@ pub async fn save_license( data.push_str(quick_xml::se::to_string(license)?.as_str()); if !data.contains("") { - data.remove_matches(""); + // replace unstable remove_matches (when most of the devs seem to think that it's stable since 2024 ??) w/ regex + let re = Regex::new(r#""#).unwrap(); + data = re.replace_all(&data, "").to_string(); } let encrypted_data = encrypt_license(&data)?; diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index f2a62cae..305a3480 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -209,7 +209,16 @@ pub async fn get_games_request( let opt = if downloads.len() == 1 { &downloads[0] } else { - downloads.iter().find(|item| item.download_type() == "LIVE").unwrap() + match downloads.iter().find(|item| item.download_type() == "LIVE") { + Some(item) => item, + None => { + error!( + "Cannot find LIVE download for game with multiple downloads: {}", + &slug + ); + continue; + } + } }; let version = if let Ok(version) = game.base_offer().installed_version().await { diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index e3e0278b..d0cacd2f 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -1,4 +1,3 @@ -#![feature(slice_pattern)] use clap::{arg, command, Parser}; use desktop::check_desktop_icon; use egui::{ diff --git a/maxima-ui/src/ui_image.rs b/maxima-ui/src/ui_image.rs index 58e40e74..cc7fce4f 100644 --- a/maxima-ui/src/ui_image.rs +++ b/maxima-ui/src/ui_image.rs @@ -12,7 +12,6 @@ use std::{ }; use tokio::{fs::File, io}; -use core::slice::SlicePattern; use log::{debug, error, info}; use image::io::Reader as ImageReader; @@ -137,7 +136,7 @@ impl UIImageCache { // not sure why it *wouldn't* have a parent but i'm just being safe // i don't have a way to catch-all the slugs atm, so this is the better solution, it's infrequent and a non-ui thread anyway if !fs::metadata(&parent).is_ok() { - let res = fs::create_dir_all(&parent)?; + fs::create_dir_all(&parent)?; } } if let Some(remote) = remotes.get(&needle) { @@ -146,9 +145,10 @@ impl UIImageCache { let mut file = File::create(&path).await?; let body = result.bytes().await?; - io::copy(&mut body.as_slice(), &mut file).await?; + let mut body_sliced: &[u8] = body.as_ref(); + io::copy(&mut body_sliced, &mut file).await?; - let ci = load_image_bytes(&body)?; + let ci = load_image_bytes(body.as_ref())?; cache.lock().unwrap().insert( needle, Some(context.load_texture( From 078e28eeff15b159427d04aa29e940a3565a59e8 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:13:47 +0200 Subject: [PATCH 06/57] upd deps --- Cargo.toml | 4 +- maxima-bootstrap/Cargo.toml | 20 +- maxima-bootstrap/src/main.rs | 6 +- maxima-cli/Cargo.toml | 26 +- maxima-cli/src/main.rs | 13 +- maxima-lib/Cargo.toml | 125 ++--- maxima-lib/src/content/downloader.rs | 75 ++- maxima-lib/src/content/manager.rs | 10 +- maxima-lib/src/content/mod.rs | 4 +- maxima-lib/src/content/zip.rs | 13 +- maxima-lib/src/core/auth/context.rs | 2 +- maxima-lib/src/core/auth/hardware.rs | 21 +- maxima-lib/src/core/auth/mod.rs | 2 +- maxima-lib/src/core/auth/storage.rs | 6 +- maxima-lib/src/core/background_service_nix.rs | 6 +- maxima-lib/src/core/background_service_win.rs | 2 +- maxima-lib/src/core/cloudsync.rs | 26 +- maxima-lib/src/core/concurrency.rs | 2 +- maxima-lib/src/core/ecommerce.rs | 4 +- maxima-lib/src/core/launch.rs | 10 +- maxima-lib/src/core/library.rs | 8 +- maxima-lib/src/core/manifest/dip.rs | 4 +- maxima-lib/src/core/manifest/mod.rs | 4 +- maxima-lib/src/core/manifest/pre_dip.rs | 4 +- maxima-lib/src/core/mod.rs | 13 +- maxima-lib/src/core/service_layer.rs | 12 +- maxima-lib/src/lsx/connection.rs | 24 +- maxima-lib/src/lsx/request/account.rs | 7 +- maxima-lib/src/lsx/request/license.rs | 4 +- maxima-lib/src/lsx/request/offer.rs | 2 +- maxima-lib/src/lsx/request/profile.rs | 6 +- maxima-lib/src/lsx/types.rs | 4 +- maxima-lib/src/ooa/mod.rs | 45 +- maxima-lib/src/rtm/client.rs | 11 +- maxima-lib/src/rtm/connection.rs | 22 +- maxima-lib/src/unix/wine.rs | 10 +- maxima-lib/src/util/github.rs | 19 +- maxima-lib/src/util/native.rs | 8 +- maxima-lib/src/util/registry.rs | 6 +- maxima-lib/src/util/service_win.rs | 52 ++- maxima-lib/src/util/simple_crypto.rs | 15 +- maxima-resources/Cargo.toml | 2 +- maxima-service/Cargo.toml | 2 +- maxima-service/src/service/error.rs | 2 +- maxima-service/src/service/mod.rs | 4 +- maxima-tui/Cargo.toml | 32 +- maxima-tui/src/main.rs | 54 ++- maxima-tui/src/service.rs | 6 +- maxima-ui/Cargo.toml | 31 +- maxima-ui/src/bridge/game_details.rs | 8 +- maxima-ui/src/bridge/get_games.rs | 8 +- maxima-ui/src/bridge/login_oauth.rs | 2 +- maxima-ui/src/bridge/start_game.rs | 4 +- maxima-ui/src/bridge_processor.rs | 2 +- maxima-ui/src/bridge_thread.rs | 9 +- maxima-ui/src/desktop.rs | 2 +- maxima-ui/src/enum_locale_map.rs | 2 +- maxima-ui/src/event_processor.rs | 2 +- maxima-ui/src/event_thread.rs | 4 +- maxima-ui/src/main.rs | 442 ++++++++++-------- maxima-ui/src/renderers/app_bg_renderer.rs | 31 +- .../src/renderers/game_view_bg_renderer.rs | 2 +- maxima-ui/src/ui_image.rs | 4 +- maxima-ui/src/views/downloads_view.rs | 45 +- maxima-ui/src/views/friends_view.rs | 37 +- maxima-ui/src/views/game_view.rs | 94 ++-- maxima-ui/src/views/settings_view.rs | 4 +- maxima-ui/src/widgets/enum_dropdown.rs | 18 +- 68 files changed, 773 insertions(+), 737 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 491cbaf7..792a8d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,5 @@ members = [ strip = true [patch.crates-io] -flate2 = { git = 'https://github.com/ArmchairDevelopers/flate2-rs.git' } -async-compression = { git = 'https://github.com/ArmchairDevelopers/async-compression.git' } +flate2 = { git = 'https://github.com/sjp761/flate2-rs.git' } +async-compression = { git = 'https://github.com/sjp761/async-compression.git' } diff --git a/maxima-bootstrap/Cargo.toml b/maxima-bootstrap/Cargo.toml index 4efd25dc..5a36eecb 100644 --- a/maxima-bootstrap/Cargo.toml +++ b/maxima-bootstrap/Cargo.toml @@ -3,25 +3,25 @@ name = "maxima-bootstrap" description = "Maxima handler for custom EA protocols" version = "0.1.0" authors = ["Sean Kahler "] -edition = "2021" +edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -tokio = { version = "1.28.2", features = [ +tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net", ] } -serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.97" -reqwest = { version = "0.11.18", features = ["stream", "rustls-tls"], default-features = false } -base64 = "0.21.2" -url = "2.4.0" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +reqwest = { version = "0.13.4", features = ["stream", "rustls", "query"], default-features = false } +base64 = "0.22.1" +url = "2.5.8" querystring = "1.1.0" urlencoding = "2.1.3" -thiserror = "2.0.12" +thiserror = "2.0.18" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = [ @@ -31,8 +31,8 @@ winapi = { version = "0.3.9", features = [ "wincon", "consoleapi", ] } -winreg = "0.50.0" -windows-service = "0.6.0" +winreg = "0.56.0" +windows-service = "0.8.1" [target.'cfg(target_os = "macos")'.dependencies] cacao = "0.3.2" diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index 9cd16c2f..629f0c04 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -8,12 +8,12 @@ use std::string::FromUtf8Error; use thiserror::Error; use tokio::process::Command; -use base64::{engine::general_purpose, Engine}; +use base64::{Engine, engine::general_purpose}; use maxima::core::launch::BootstrapLaunchArgs; +use maxima::util::BackgroundServiceControlError; use maxima::util::native::NativeError; #[cfg(windows)] use maxima::util::service::{is_service_valid, register_service}; -use maxima::util::BackgroundServiceControlError; use url::Url; #[cfg(target_os = "macos")] @@ -123,8 +123,8 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { #[cfg(unix)] async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { - use maxima::unix::wine::run_wine_command; use maxima::unix::wine::CommandType; + use maxima::unix::wine::run_wine_command; run_wine_command( args.path, diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index 7bbedf68..1a6bdf61 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -2,25 +2,25 @@ name = "maxima-cli" version = "0.1.0" authors = ["Sean Kahler "] -edition = "2021" +edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -clap = { version = "4.3.5", features = [ "derive" ] } -tokio = { version = "1.28.2", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } -serde = { version = "1.0.164", features = [ "derive" ] } -serde_json = "1.0.97" -anyhow = "1.0.71" -lazy_static = "1.4.0" -base64 = "0.21.2" -log = "0.4.19" -regex = "1.8.4" -inquire = "0.6.2" -futures = "0.3.30" +clap = { version = "4.6.1", features = [ "derive" ] } +tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } +serde = { version = "1.0.228", features = [ "derive" ] } +serde_json = "1.0.150" +anyhow = "1.0.102" +lazy_static = "1.5.0" +base64 = "0.22.1" +log = "0.4.30" +regex = "1.12.3" +inquire = "0.9.4" +futures = "0.3.32" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi" ] } -winreg = "0.50.0" +winreg = "0.56.0" is_elevated = "0.1.2" [build-dependencies] diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index de2ce960..af823a56 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -1,6 +1,6 @@ use clap::{Parser, Subcommand}; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use inquire::{Select, Text}; use lazy_static::lazy_static; use log::{debug, error, info, warn}; @@ -19,15 +19,17 @@ use maxima::{ use maxima::{ content::{ + ContentService, downloader::ZipDownloader, manager::{QueuedGame, QueuedGameBuilder}, - ContentService, }, core::{ + LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, auth::{ + TokenResponse, context::AuthContext, login::{begin_oauth_login_flow, manual_login}, - nucleus_auth_exchange, nucleus_token_exchange, TokenResponse, + nucleus_auth_exchange, nucleus_token_exchange, }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, @@ -35,11 +37,10 @@ use maxima::{ library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH}, service_layer::{ + SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, - ServiceLegacyOffer, ServicePlayer, SERVICE_REQUEST_GETBASICPLAYER, - SERVICE_REQUEST_GETLEGACYCATALOGDEFS, + ServiceLegacyOffer, ServicePlayer, }, - LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, }, ooa, rtm::client::BasicPresence, diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 9f3a6e77..abf028b1 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "maxima-lib" version = "1.0.0" -edition = "2021" +edition = "2024" authors = ["Sean Kahler "] categories = ["games"] description = "A free and open-source replacement for the EA Desktop Launcher" @@ -10,81 +10,86 @@ license = "GPL-3.0" readme = "README.md" repository = "https://github.com/ArmchairDevelopers/Maxima" documentation = "https://docs.rs/maxima" -rust-version = "1.81.0" +rust-version = "1.96.0" [dependencies] -tokio = { version = "1.28.2", features = [ +tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net", ] } -moka = { version = "0.12.1", features = ["sync"] } -serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.97" -quick-xml = { version = "0.30.0", features = ["serialize"] } -paste = "1.0.12" +moka = { version = "0.12.15", features = ["sync"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +quick-xml = { version = "0.40.1", features = ["serialize"] } +pastey = "0.2.3" hex = "0.4.3" -lazy_static = "1.4.0" -reqwest = { version = "0.11.18", features = ["stream", "rustls-tls", "gzip", "json"], default-features = false } -base64 = "0.21.2" -base62 = "2.2.0" -log = "0.4.19" -enable-ansi-support = "0.2.1" -chrono = "0.4.26" -regex = "1.8.4" -directories = "5.0.1" -open = "5.0.0" +lazy_static = "1.5.0" +reqwest = { version = "0.13.4", features = ["stream", "form", "rustls", "gzip", "json"], default-features = false } +base64 = "0.22.1" +base62 = "2.2.4" +log = "0.4.30" +enable-ansi-support = "0.3.1" +chrono = "0.4.44" +regex = "1.12.3" +directories = "6.0.0" +open = "5.3.5" querystring = "1.1.0" -ureq = { version = "2.9.1", features = ["tls"] } -sysinfo = "0.29.7" -strum_macros = "0.25.2" -sha2-const = "0.1.2" -tar = "0.4" -flate2 = { version = "=1.0.28", default-features = false, features = ["zlib-default"] } +ureq = { version = "3.3.0", features = ["_tls"] } +sysinfo = "0.39.3" +strum_macros = "0.28.0" +sha2-const = "0.1.3" +tar = "0.4.46" +flate2 = { version = "=1.1.10", default-features = false, features = ["zlib-default"] } xz2 = "0.1.7" -zstd = "0.13.0" -toml = "0.8.8" -bytebuffer = "2.2.0" -derive-getters = "0.3.0" -derive_builder = "0.12.0" -mac_address = "1.1.5" -filetime = "0.2.23" -rand = "0.9.0" -ring = "0.17.7" -futures = "0.3.30" -prost = "0.12.3" -encoding = "0.2.33" -async-compression = { version = "0.4.5", features = ["tokio", "deflate"] } -tokio-util = { version = "0.7.10", features = ["io", "compat"] } -uuid = "1.7.0" -tokio-rustls = "0.23.1" -rustls = "0.20.0" -webpki-roots = "0.22.0" -aes = "0.8.4" -ecb = { version = "0.1.2", features = ["std"] } -cbc = { version = "0.1.2", features = ["std"] } -glob = "0.3.1" -md5 = "0.7.0" -bytes = "1.5.0" -async-trait = "0.1.81" -crc32fast = "1.4.2" +zstd = "0.13.3" +toml = "1.1.2" +bytebuffer = "2.3.0" +derive-getters = "0.5.0" +derive_builder = "0.20.2" +mac_address = "1.1.8" +filetime = "0.2.29" +rand = "0.10.1" +ring = "0.17.14" +futures = "0.3.32" +prost = "0.14.4" +encoding_rs = "0.8.35" +async-compression = { version = "=0.4.42", features = ["tokio", "deflate"] } +tokio-util = { version = "0.7.18", features = ["io", "compat"] } +uuid = "1.23.2" +tokio-rustls = "0.26.4" +rustls = "0.23.40" +webpki-roots = "1.0.7" +aes = "0.9.1" +ecb = { version = "0.2.0", features = ["alloc", "block-padding", "default", "zeroize"] } +cbc = { version = "0.2.1", features = ["alloc", "block-padding", "default", "zeroize"] } +glob = "0.3.3" +md5 = "0.8.0" +bytes = "1.11.1" +async-trait = "0.1.89" +crc32fast = "1.5.0" async-recursion = "1.1.1" pelite = "0.10.0" -gethostname = "0.5.0" -thiserror = "2.0.12" -url = "2.5.2" -http = "0.2.12" +gethostname = "1.1.0" +thiserror = "2.0.18" +url = "2.5.8" +http = "1.4.1" +generic-array = "1.4.3" +rustls-pki-types = "1.14.1" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = [ "memoryapi", + "errhandlingapi", "handleapi", "synchapi", "wincon", "consoleapi", + "shellapi", "psapi", + "sddl", "wininet", "winerror", "iphlpapi", @@ -92,17 +97,17 @@ winapi = { version = "0.3.9", features = [ "winsvc", "impl-default" ] } -winreg = "0.51.0" -windows-service = "0.6.0" +winreg = "0.56.0" +windows-service = "0.8.1" is_elevated = "0.1.2" -widestring = "1.0.2" -wmi = "0.13.1" +widestring = "1.2.1" +wmi = "0.18.4" [target.'cfg(target_os = "macos")'.dependencies] -smbios-lib = "0.9.1" +smbios-lib = "0.9.2" [build-dependencies] -prost-build = "0.12.3" +prost-build = "0.14.3" [lib] name = "maxima" diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index e982a190..d0734c12 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -1,6 +1,5 @@ use std::{ - cmp, - io::{self, Cursor, Read, Seek, SeekFrom, Write}, + io::{self, Cursor, Read, SeekFrom}, path::{Path, PathBuf}, pin::Pin, prelude, @@ -8,30 +7,25 @@ use std::{ task, }; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + use crate::{ content::{ manager::DownloaderError, zip::{CompressionType, ZipFile, ZipFileEntry}, - zlib::{restore_zlib_state, write_zlib_state}, - }, - util::{ - hash::hash_file_crc32, - native::{maxima_dir, NativeError, SafeParent, SafeStr}, }, + util::native::{SafeParent, maxima_dir}, }; -use async_compression::tokio::write::DeflateDecoder; use async_trait::async_trait; -use bytes::{Buf, BufMut, Bytes, BytesMut}; use derive_getters::Getters; -use flate2::bufread::DeflateDecoder as BufreadDeflateDecoder; +use flate2::{Decompress, 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; use tokio::{ - fs::{create_dir, create_dir_all, File, OpenOptions}, - io::{AsyncReadExt, AsyncSeekExt, AsyncWrite, BufReader, BufWriter}, + fs::{File, OpenOptions, create_dir, create_dir_all}, + io::{AsyncSeekExt, AsyncWrite, BufReader, BufWriter}, runtime::Handle, }; use tokio_util::compat::FuturesAsyncReadCompatExt; @@ -53,17 +47,19 @@ trait DownloadDecoder: Send { fn write_in_pos(&self) -> u64; fn write_out_pos(&self) -> u64; - fn get_mut<'b>(&mut self) -> Arc>; + fn get_mut(&mut self) -> Arc>>; } struct ZLibDeflateDecoder { - decoder: Arc>>>, + decompress: flate2::Decompress, + writer: Arc>>, } impl ZLibDeflateDecoder { fn new(writer: BufWriter) -> Self { Self { - decoder: Arc::new(Mutex::new(DeflateDecoder::new(writer))), + decompress: Decompress::new(true), + writer: Arc::new(Mutex::new(writer)), } } } @@ -71,22 +67,18 @@ impl ZLibDeflateDecoder { #[async_trait] impl DownloadDecoder for ZLibDeflateDecoder { fn save_state(&mut self, buf: &mut BytesMut) { - let mut decoder = self.decoder.lock().unwrap(); - let zstream = decoder.inner_mut().decoder_mut().inner.decompress.get_raw(); - write_zlib_state(buf, zstream); + // State serialization is disabled - flate2 doesn't expose raw zlib state + let _ = buf; } fn restore_state(&mut self, buf: &mut Bytes) { - let mut decoder = self.decoder.lock().unwrap(); - let decompress = &mut decoder.inner_mut().decoder_mut().inner.decompress; - decompress.reset(false); - let zstream = decompress.get_raw(); - restore_zlib_state(buf, zstream); + // State deserialization is disabled - flate2 doesn't expose raw zlib state + self.decompress.reset(false); + let _ = buf; } fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError> { - let mut decoder = self.decoder.lock().unwrap(); - let file = decoder.get_mut(); + let mut file = self.writer.lock().unwrap(); let handle = Handle::current(); let _ = handle.enter(); @@ -96,21 +88,15 @@ impl DownloadDecoder for ZLibDeflateDecoder { } fn write_in_pos(&self) -> u64 { - let mut decoder = self.decoder.lock().unwrap(); - let decompress = &mut decoder.inner_mut().decoder_mut().inner.decompress; - let zstream = decompress.get_raw(); - zstream.total_in as u64 + self.decompress.total_in() } fn write_out_pos(&self) -> u64 { - let mut decoder = self.decoder.lock().unwrap(); - let decompress = &mut decoder.inner_mut().decoder_mut().inner.decompress; - let zstream = decompress.get_raw(); - zstream.total_out as u64 + self.decompress.total_out() } - fn get_mut(&mut self) -> Arc> { - self.decoder.clone() + fn get_mut(&mut self) -> Arc>> { + self.writer.clone() } } @@ -131,7 +117,6 @@ impl NoopDecoder { #[async_trait] impl DownloadDecoder for NoopDecoder { fn save_state(&mut self, buf: &mut BytesMut) { - self.pos = self.writer.lock().unwrap().buffer().len() as u64; buf.put_u64(self.pos); } @@ -157,20 +142,17 @@ impl DownloadDecoder for NoopDecoder { self.pos } - fn get_mut<'b>(&mut self) -> Arc> { + fn get_mut(&mut self) -> Arc>> { self.writer.clone() } } -trait AsyncWriteWrapper: AsyncWrite + Unpin + Send {} -impl AsyncWriteWrapper for T {} - struct AsyncWriterWrapper<'a> { id: String, path: String, zlib_state_file: std::fs::File, decoder: &'a mut Box, - inner: Arc>, + inner: Arc>>, } impl<'a> AsyncWriterWrapper<'a> { @@ -195,7 +177,7 @@ impl<'a> AsyncWriterWrapper<'a> { impl<'a> AsyncWrite for AsyncWriterWrapper<'a> { fn poll_write( - mut self: Pin<&mut Self>, + self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8], ) -> task::Poll> { @@ -482,16 +464,13 @@ impl ZipDownloader { Bytes::copy_from_slice(&compressed_data[..available_length as usize]) } CompressionType::Deflate => { - let mut decoder = BufreadDeflateDecoder::new(Cursor::new(&compressed_data)); + let decoder = BufreadDeflateDecoder::new(Cursor::new(&compressed_data)); let mut limited_reader = decoder.take(length); let mut decompressed_data = Vec::with_capacity(length as usize); limited_reader.read_to_end(&mut decompressed_data)?; Bytes::from(decompressed_data) } - any => { - return Err(DownloaderError::CompressionType(any.to_owned())); - } }; Ok(decompressed_data) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f16116db..f77a5253 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -1,8 +1,8 @@ use std::{ path::PathBuf, sync::{ - atomic::{AtomicUsize, Ordering}, Arc, + atomic::{AtomicUsize, Ordering}, }, }; @@ -18,17 +18,17 @@ use tokio_util::sync::CancellationToken; use crate::{ content::{ + ContentService, downloader::{DownloadError, ZipDownloader}, zip::{self, CompressionType, ZipError, ZipFileEntry}, - ContentService, }, core::{ + MaximaEvent, auth::storage::LockedAuthStorage, - manifest::{self, ManifestError, MANIFEST_RELATIVE_PATH}, + manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::ServiceLayerError, - MaximaEvent, }, - util::native::{maxima_dir, NativeError}, + util::native::{NativeError, maxima_dir}, }; const QUEUE_FILE: &str = "download_queue.json"; diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 4a98e432..1bdd4b8f 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -4,10 +4,10 @@ use crate::core::{ auth::storage::LockedAuthStorage, cache::DynamicCache, service_layer::{ - ServiceAvailableBuild, ServiceAvailableBuilds, ServiceAvailableBuildsBuilder, + SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, + ServiceAvailableBuilds, ServiceAvailableBuildsBuilder, ServiceAvailableBuildsRequestBuilder, ServiceDownloadUrlMetadata, ServiceDownloadUrlRequestBuilder, ServiceLayerClient, ServiceLayerError, - SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, }, }; diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index be5e8b01..33dacf66 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -1,9 +1,9 @@ use bytebuffer::{ByteBuffer, Endian}; use derive_getters::Getters; -use encoding::{all::WINDOWS_1252, DecoderTrap, Encoding}; +use encoding_rs::WINDOWS_1252; use log::{debug, warn}; -use reqwest::header::ToStrError; use reqwest::Client; +use reqwest::header::ToStrError; use std::cmp; use std::string::FromUtf8Error; use thiserror::Error; @@ -165,10 +165,11 @@ impl ZipFileEntry { debug!("Using UTF-8..."); String::from_utf8(name_bytes)? } else { - match WINDOWS_1252.decode(&name_bytes, DecoderTrap::Strict) { - Ok(s) => s, - Err(_) => return Err(EntryError::Decode), + let (s, _, had_errors) = WINDOWS_1252.decode(&name_bytes); + if had_errors { + return Err(EntryError::Decode); } + s.to_string() }; entry.extra_field = data.read_bytes(extra_field_len as usize)?; @@ -418,7 +419,7 @@ impl ZipFile { idx: i, total: eocd.total_entries, err: format!("{:?}", err).to_string(), - }) + }); } Ok(e) => e, }; diff --git a/maxima-lib/src/core/auth/context.rs b/maxima-lib/src/core/auth/context.rs index df782719..b488976e 100644 --- a/maxima-lib/src/core/auth/context.rs +++ b/maxima-lib/src/core/auth/context.rs @@ -5,7 +5,7 @@ use crate::core::{ clients::JUNO_PC_CLIENT_ID, endpoints::API_NUCLEUS_AUTH, }; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use rand::random; use ring::hmac::HMAC_SHA256; use sha2_const::Sha256; diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index c11df25d..fad63a2e 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -57,13 +57,12 @@ impl HardwareInfo { use std::collections::HashMap; use log::warn; - use wmi::{COMLibrary, FilterValue, WMIConnection}; + use wmi::{FilterValue, WMIConnection}; use crate::util::wmi_utils; let wmi_thread = std::thread::spawn(move || { - let com_con = COMLibrary::new().unwrap(); - let wmi_con = WMIConnection::new(com_con).unwrap(); + let wmi_con = WMIConnection::new().unwrap(); let os_data: Vec = wmi_con.query().unwrap(); let bios_data: Vec = wmi_con.query().unwrap(); @@ -250,7 +249,7 @@ impl HardwareInfo { use std::process::Command; use smbioslib::{ - table_load_from_device, SMBiosBaseboardInformation, SMBiosSystemInformation, + SMBiosBaseboardInformation, SMBiosSystemInformation, table_load_from_device, }; use crate::util::system_profiler_utils::SPDisplaysDataType; @@ -282,7 +281,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 +298,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 @@ -348,8 +351,8 @@ impl HardwareInfo { pub fn get_cpu_details() -> CpuDetails { use core::arch::x86_64::__cpuid; - let m = unsafe { __cpuid(0) }; - let flags = unsafe { __cpuid(1) }; + let m = { __cpuid(0) }; + let flags = { __cpuid(1) }; let mut man = Vec::new(); for val in [m.ebx, m.edx, m.ecx] { @@ -360,7 +363,7 @@ impl HardwareInfo { let mut brand_name = Vec::with_capacity(47); 'outer: for eax in [0x80000002, 0x80000003, 0x80000004] { - let part = unsafe { __cpuid(eax) }; + let part = { __cpuid(eax) }; for val in [part.eax, part.ebx, part.ecx, part.edx] { for b in val.to_ne_bytes() { if b == 0 { diff --git a/maxima-lib/src/core/auth/mod.rs b/maxima-lib/src/core/auth/mod.rs index b8852804..f8f61abf 100644 --- a/maxima-lib/src/core/auth/mod.rs +++ b/maxima-lib/src/core/auth/mod.rs @@ -12,7 +12,7 @@ use super::{ use crate::core::auth::storage::{AuthError, TokenError}; use context::AuthContext; use derive_getters::Getters; -use reqwest::{redirect, Client, Url}; +use reqwest::{Client, Url, redirect}; use serde::Deserialize; use thiserror::Error; diff --git a/maxima-lib/src/core/auth/storage.rs b/maxima-lib/src/core/auth/storage.rs index 546534f1..9416351e 100644 --- a/maxima-lib/src/core/auth/storage.rs +++ b/maxima-lib/src/core/auth/storage.rs @@ -1,12 +1,12 @@ use super::{ - nucleus_connect_token_refresh, token_info::NucleusTokenInfo, TokenRefreshError, TokenResponse, + TokenRefreshError, TokenResponse, nucleus_connect_token_refresh, token_info::NucleusTokenInfo, }; use crate::core::auth::hardware::HardwareHashError; use crate::ooa::LicenseError; -use crate::util::native::{maxima_dir, NativeError}; +use crate::util::native::{NativeError, maxima_dir}; use log::info; -use reqwest::header::ToStrError; use reqwest::Client; +use reqwest::header::ToStrError; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, diff --git a/maxima-lib/src/core/background_service_nix.rs b/maxima-lib/src/core/background_service_nix.rs index d383350c..d8fe2e1e 100644 --- a/maxima-lib/src/core/background_service_nix.rs +++ b/maxima-lib/src/core/background_service_nix.rs @@ -1,12 +1,12 @@ -use base64::{engine::general_purpose, Engine}; +use base64::{Engine, engine::general_purpose}; use lazy_static::lazy_static; use log::debug; use regex::Regex; use serde::Serialize; use crate::{ - unix::wine::{run_wine_command, CommandType}, - util::native::{module_path, NativeError, SafeParent, SafeStr}, + unix::wine::{CommandType, run_wine_command}, + util::native::{NativeError, SafeParent, SafeStr, module_path}, }; lazy_static! { diff --git a/maxima-lib/src/core/background_service_win.rs b/maxima-lib/src/core/background_service_win.rs index 0a31d372..59832575 100644 --- a/maxima-lib/src/core/background_service_win.rs +++ b/maxima-lib/src/core/background_service_win.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::core::error::BackgroundServiceClientError; use crate::util::dll_injector::DllInjector; use crate::util::native::NativeError; -use crate::util::registry::{set_up_registry, RegistryError}; +use crate::util::registry::{RegistryError, set_up_registry}; use is_elevated::is_elevated; pub const BACKGROUND_SERVICE_PORT: u16 = 13021; diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 41e4cd75..b76d5d68 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, @@ -173,7 +172,7 @@ async fn calc_file_md5(file: File, mode: HashMode) -> Result Ok(format!("{:x}", digest)), @@ -211,7 +210,7 @@ impl<'a> CloudSyncLock<'a> { let res = client.get(manifest_url).send().await?; let manifest: CloudSyncManifest = { - let mut manifest = if let Ok(text) = res.text().await { + let manifest = if let Ok(text) = res.text().await { let result = quick_xml::de::from_str(&text); if let Ok(manifest) = result { manifest @@ -278,7 +277,7 @@ impl<'a> CloudSyncLock<'a> { let file = OpenOptions::new().read(true).open(path.clone()).await; - let md5 = if let Ok(file) = file { + let _md5 = if let Ok(file) = file { let md5 = calc_file_md5(file, HashMode::Hex).await?; if let Some(_) = self.manifest.file_by_md5(&md5) { debug!("Skipping CloudSync read {}", &path.display()); @@ -305,7 +304,9 @@ impl<'a> CloudSyncLock<'a> { } let (token, user_id) = acquire_auth(self.auth).await?; - let body = quick_xml::se::to_string(&value)?.replace("CloudSyncRequests", "requests"); + let body = quick_xml::se::to_string(&value) + .unwrap() + .replace("CloudSyncRequests", "requests"); let res = self .client @@ -322,7 +323,7 @@ impl<'a> CloudSyncLock<'a> { for i in 0..authorizations.request.len() { let auth_req = &authorizations.request[i]; - let mut req = self.client.get(&auth_req.url); + let req = self.client.get(&auth_req.url); let res = req.send().await?; if !res.status().is_success() { // If the request is invalid, S3 will return an error *and* include that error in the body. @@ -462,8 +463,9 @@ impl<'a> CloudSyncLock<'a> { } } - let manifest = - quick_xml::se::to_string(&manifest)?.replace("CloudSyncManifest", "manifest"); + let manifest = quick_xml::se::to_string(&manifest) + .unwrap() + .replace("CloudSyncManifest", "manifest"); data.insert( 0, WriteData::Text { @@ -474,7 +476,9 @@ impl<'a> CloudSyncLock<'a> { } let (token, user_id) = acquire_auth(self.auth).await?; - let body = quick_xml::se::to_string(&auth_reqs)?.replace("CloudSyncRequests", "requests"); + let body = quick_xml::se::to_string(&auth_reqs) + .unwrap() + .replace("CloudSyncRequests", "requests"); let res = self .client @@ -506,7 +510,7 @@ impl<'a> CloudSyncLock<'a> { len as u64 } - WriteData::Text {text, .. } => { + WriteData::Text { text, .. } => { req = req.body(text.to_owned()); text.len() as u64 } @@ -689,7 +693,7 @@ macro_rules! cloudsync_type { ),* $(,)? } ) => { - paste::paste! { + pastey::paste! { // Main struct definition $(#[$message_attr])* #[derive(Default, Debug, Clone, Serialize, Deserialize, Getters, PartialEq)] diff --git a/maxima-lib/src/core/concurrency.rs b/maxima-lib/src/core/concurrency.rs index 856806a6..6f3104fb 100644 --- a/maxima-lib/src/core/concurrency.rs +++ b/maxima-lib/src/core/concurrency.rs @@ -1,4 +1,4 @@ -use futures::{stream, Future, StreamExt}; +use futures::{Future, StreamExt, stream}; pub async fn execute_batch_concurrent( buffer: usize, diff --git a/maxima-lib/src/core/ecommerce.rs b/maxima-lib/src/core/ecommerce.rs index 3fb7109f..bf61a1b6 100644 --- a/maxima-lib/src/core/ecommerce.rs +++ b/maxima-lib/src/core/ecommerce.rs @@ -80,7 +80,7 @@ macro_rules! ecommerce_type { ),* $(,)? } ) => { - paste::paste! { + pastey::paste! { // Main struct definition $(#[$message_attr])* #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -102,7 +102,7 @@ macro_rules! ecommerce_type { macro_rules! ecommerce_enum { ($name:ident, { $($field:tt)* }) => { - paste::paste! { + pastey::paste! { #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum [] { diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 6281672c..c58435ae 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -1,4 +1,4 @@ -use base64::{engine::general_purpose, Engine}; +use base64::{Engine, engine::general_purpose}; use derive_getters::Getters; use log::{error, info}; use std::{env, fmt::Display, path::PathBuf, sync::Arc}; @@ -10,6 +10,7 @@ use uuid::Uuid; use crate::{ core::{ + Maxima, auth::{ context::AuthContext, nucleus_auth_exchange, @@ -19,9 +20,8 @@ use crate::{ cloudsync::{CloudSyncError, CloudSyncLockMode}, library::{LibraryError, OwnedOffer}, service_layer::ServiceLayerError, - Maxima, }, - ooa::{needs_license_update, request_and_save_license, LicenseAuth, LicenseError}, + ooa::{LicenseAuth, LicenseError, needs_license_update, request_and_save_license}, util::{ native::{NativeError, SafeParent, SafeStr}, registry::bootstrap_path, @@ -67,9 +67,7 @@ pub enum LaunchError { NotInstalled(String), #[error("bootstrap was not found! Please re-install maxima")] BootstrapMissing, - #[error( - "content ID (`{0}`) was specified as an offer ID when launching in OnlineOffline mode" - )] + #[error("content ID (`{0}`) was specified as an offer ID when launching in OnlineOffline mode")] ContentIdAsOfferId(String), } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index faa730cc..dcee61af 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -1,20 +1,20 @@ use super::{ auth::storage::LockedAuthStorage, locale::Locale, - manifest::{self, GameManifest, ManifestError, MANIFEST_RELATIVE_PATH}, + manifest::{self, GameManifest, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ + SERVICE_REQUEST_GETLEGACYCATALOGDEFS, SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, ServiceGameProductType, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceGetPreloadedOwnedGamesRequest, ServiceGetPreloadedOwnedGamesRequestBuilder, ServiceGetPreloadedOwnedGamesRequestBuilderError, ServiceLayerClient, ServiceLayerError, ServiceLegacyOffer, ServicePlatform, ServiceStorefront, ServiceUser, - ServiceUserGameProduct, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, - SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, + ServiceUserGameProduct, }, }; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; use crate::util::native::{NativeError, SafeStr}; -use crate::util::registry::{parse_partial_registry_path, parse_registry_path, RegistryError}; +use crate::util::registry::{RegistryError, parse_partial_registry_path, parse_registry_path}; use derive_getters::Getters; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index b8da5ceb..dedbf79c 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -23,7 +23,7 @@ macro_rules! dip_type { ),* $(,)? } ) => { - paste::paste! { + pastey::paste! { // Main struct definition $(#[$message_attr])* #[derive(Default, Debug, Clone, Deserialize, PartialEq, Getters)] @@ -196,7 +196,7 @@ impl DiPManifest { core::launch::mx_linux_setup, unix::{ fs::case_insensitive_path, - wine::{invalidate_mx_wine_registry, run_wine_command, CommandType}, + wine::{CommandType, invalidate_mx_wine_registry, run_wine_command}, }, }; diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index 42fa80f4..9acf730c 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -20,7 +20,9 @@ pub enum ManifestError { #[error("failed to decode DiPManifest. Weird encoding?")] Decode, - #[error("Unsupported Manifest.\nDiP Attempt: `{dip_attempt:?}`\nPreDiP Attempt: `{pre_dip_attempt:?}`")] + #[error( + "Unsupported Manifest.\nDiP Attempt: `{dip_attempt:?}`\nPreDiP Attempt: `{pre_dip_attempt:?}`" + )] Unsupported { dip_attempt: Box, pre_dip_attempt: Box, diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 0ec9e1b4..04070110 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -22,7 +22,7 @@ macro_rules! predip_type { ),* $(,)? } ) => { - paste::paste! { + pastey::paste! { // Main struct definition $(#[$message_attr])* #[derive(Default, Debug, Clone, Deserialize, PartialEq, Getters)] @@ -111,7 +111,7 @@ impl PreDiPManifest { core::launch::mx_linux_setup, unix::{ fs::case_insensitive_path, - wine::{invalidate_mx_wine_registry, run_wine_command, CommandType}, + wine::{CommandType, invalidate_mx_wine_registry, run_wine_command}, }, }; diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 88ff044b..4039629d 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -29,7 +29,7 @@ pub mod background_service { use std::{ env, - fs::{create_dir_all, File}, + fs::{File, create_dir_all}, io, os::raw::c_char, path::PathBuf, @@ -53,20 +53,19 @@ use self::{ library::GameLibrary, locale::Locale, service_layer::{ - ServiceAvatarListBuilder, ServiceAvatarListBuilderError, ServiceFriends, - ServiceGetBasicPlayerRequestBuilder, ServiceGetMyFriendsRequestBuilder, + SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETMYFRIENDS, + SERVICE_REQUEST_GETUSERPLAYER, ServiceAvatarListBuilder, ServiceAvatarListBuilderError, + ServiceFriends, ServiceGetBasicPlayerRequestBuilder, ServiceGetMyFriendsRequestBuilder, ServiceGetUserPlayerRequest, ServiceImage, ServiceImageBuilder, ServiceImageBuilderError, ServiceLayerClient, ServiceLayerError, ServicePlayer, ServicePlayerBuilder, ServicePlayerBuilderError, ServiceUser, ServiceUserBuilder, ServiceUserBuilderError, - SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETMYFRIENDS, - SERVICE_REQUEST_GETUSERPLAYER, }, }; use crate::{ content::manager::{ContentManager, ContentManagerError}, lsx::{self, service::LSXServerError, types::LSXRequestType}, rtm::client::{BasicPresence, RtmClient}, - util::native::{maxima_dir, NativeError}, + util::native::{NativeError, maxima_dir}, }; #[derive(Clone, IntoStaticStr)] @@ -433,7 +432,7 @@ impl Maxima { pub fn set_player_started(&mut self) { match &mut self.playing { - Some(ref mut playing) => playing.set_started(), + Some(playing) => playing.set_started(), None => return, } } diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index 8d4fc467..c75f06b0 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -4,7 +4,7 @@ use log::debug; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sha2_const::Sha256; +use sha2_const; use thiserror::Error; use derive_builder::Builder; @@ -98,7 +98,9 @@ pub struct ServiceLayerGraphQLRequest { macro_rules! load_graphql_request { ($type:ident, $operation:expr, $key:expr) => {{ let content = include_str!(concat!("graphql/", $operation, ".gql")); - let hash = Sha256::new().update(content.as_bytes()).finalize(); + let hash = sha2_const::Sha256::new(); + hash.update(content.as_bytes()); + let hash = hash.finalize(); ServiceLayerGraphQLRequest { query: content, operation: $operation, @@ -110,7 +112,7 @@ macro_rules! load_graphql_request { } macro_rules! define_graphql_request { - ($type:ident, $operation:expr, $key:expr) => { paste::paste! { + ($type:ident, $operation:expr, $key:expr) => { pastey::paste! { pub const []: &ServiceLayerGraphQLRequest = &load_graphql_request!($type, stringify!($operation), stringify!($key)); }} } @@ -271,7 +273,7 @@ impl ServiceLayerClient { macro_rules! service_layer_type { ($name:ident, { $($field:tt)* }) => { - paste::paste! { + pastey::paste! { #[derive(Clone, Debug, Serialize, Deserialize, Getters, Builder)] #[serde(rename_all = "camelCase")] #[repr(C)] @@ -284,7 +286,7 @@ macro_rules! service_layer_type { macro_rules! service_layer_enum { ($name:ident, { $($field:tt)* }) => { - paste::paste! { + pastey::paste! { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[repr(C)] diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index e60d1fe6..033cb131 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, time::Duration, }; -use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; +use sysinfo::{Pid, System}; use thiserror::Error; use tokio::sync::{MutexGuard, RwLock}; @@ -37,13 +37,13 @@ use super::{ voip::handle_voip_status_request, }, types::{ - create_lsx_message, LSXChallenge, LSXEvent, LSXEventType, LSXMessageType, LSXRequest, - LSXResponse, LSX, + LSX, LSXChallenge, LSXEvent, LSXEventType, LSXMessageType, LSXRequest, LSXResponse, + create_lsx_message, }, }; use crate::{ core::{ - auth::storage::TokenError, launch::ActiveGameContext, LockedMaxima, Maxima, MaximaEvent, + LockedMaxima, Maxima, MaximaEvent, auth::storage::TokenError, launch::ActiveGameContext, }, lsx::{request::LSXRequestError, types::LSXRequestType}, util::{ @@ -55,7 +55,9 @@ use crate::{ #[derive(Error, Debug)] pub enum LSXConnectionError { #[error(transparent)] - Xml(#[from] DeError), + XmlDeserialize(#[from] DeError), + #[error(transparent)] + XmlSerialize(#[from] quick_xml::se::SeError), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] @@ -86,7 +88,7 @@ macro_rules! lsx_message_matcher { $connection_var:expr, $message_var:expr, $message_type:ty; $($name:ident $handler:ident),* $(,)? ) => { - paste::paste! { + pastey::paste! { match $message_var { $( $message_type::$name(msg) => $handler($connection_var, msg).await, @@ -151,13 +153,12 @@ pub fn get_os_pid(context: &ActiveGameContext) -> Result { let mut pid = None; let sys = System::new_all(); - for e in sys.processes() { - let (p_pid, process) = e; + for (p_pid, process) in sys.processes() { if process.cmd().is_empty() { continue; } - let mut cmd = process.cmd()[0].to_owned(); + let mut cmd = process.cmd()[0].to_string_lossy().into_owned(); // Wine path handling if cfg!(unix) && cmd.starts_with("Z:") { @@ -169,7 +170,8 @@ pub fn get_os_pid(context: &ActiveGameContext) -> Result { } for ele in process.environ() { - let (key, value) = ele.split_once('=').unwrap_or((ele, "")); + let env = ele.to_string_lossy(); + let (key, value) = env.split_once('=').unwrap_or((&env, "")); if key != "MXLaunchId" || value != context.launch_id() { continue; } @@ -226,7 +228,7 @@ impl Connection { if let Some(process) = sys.process(Pid::from_u32(os_pid)) { let filename = PathBuf::from( process.cmd()[0] - .to_owned() + .to_string_lossy() .replace("Z:", "") .replace('\\', "/"), ) diff --git a/maxima-lib/src/lsx/request/account.rs b/maxima-lib/src/lsx/request/account.rs index 5ddad4f9..6d5b8658 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -1,7 +1,7 @@ use crate::{ core::service_layer::{ - ServiceEntitlement, ServiceSdkEntitlementsRequestBuilder, ServiceSdkEntitlementsResult, - SERVICE_REQUEST_SDKENTITLEMENTS, + SERVICE_REQUEST_SDKENTITLEMENTS, ServiceEntitlement, ServiceSdkEntitlementsRequestBuilder, + ServiceSdkEntitlementsResult, }, lsx::{ connection::LockedConnectionState, @@ -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..5a662d5c 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}, @@ -9,7 +9,7 @@ use crate::{ types::{LSXRequestLicense, LSXRequestLicenseResponse, LSXResponseType}, }, make_lsx_handler_response, - ooa::{request_license, LicenseAuth}, + ooa::{LicenseAuth, request_license}, }; pub async fn handle_license_request( diff --git a/maxima-lib/src/lsx/request/offer.rs b/maxima-lib/src/lsx/request/offer.rs index b3f9d1e4..145bfc15 100644 --- a/maxima-lib/src/lsx/request/offer.rs +++ b/maxima-lib/src/lsx/request/offer.rs @@ -1,6 +1,6 @@ use crate::{ core::service_layer::{ - ServiceAddonSearchRequestBuilder, ServiceAddonSearchResultRoot, SERVICE_REQUEST_ADDONSEARCH, + SERVICE_REQUEST_ADDONSEARCH, ServiceAddonSearchRequestBuilder, ServiceAddonSearchResultRoot, }, lsx::{ connection::LockedConnectionState, diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index e8759cd6..5918c7e0 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -1,8 +1,8 @@ use log::{debug, info}; use crate::core::service_layer::{ - ServiceFriends, ServiceGetMyFriendsRequestBuilder, ServiceLayerError, - SERVICE_REQUEST_GETMYFRIENDS, + SERVICE_REQUEST_GETMYFRIENDS, ServiceFriends, ServiceGetMyFriendsRequestBuilder, + ServiceLayerError, }; use crate::{ lsx::{ @@ -18,7 +18,7 @@ use crate::{ }, make_lsx_handler_response, rtm::client::{BasicPresence, RichPresenceBuilder}, - util::native::{platform_path, NativeError, SafeStr}, + util::native::{NativeError, SafeStr, platform_path}, }; pub async fn handle_profile_request( diff --git a/maxima-lib/src/lsx/types.rs b/maxima-lib/src/lsx/types.rs index 88d5feae..9f2d9134 100644 --- a/maxima-lib/src/lsx/types.rs +++ b/maxima-lib/src/lsx/types.rs @@ -22,7 +22,7 @@ macro_rules! lsx_message { ),* $(,)? } ) => { - paste::paste! { + pastey::paste! { // Main struct definition $(#[$message_attr])* #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -147,7 +147,7 @@ pub fn create_lsx_message(r#type: LSXMessageType) -> LSX { #[macro_export] macro_rules! make_lsx_handler_response { ($reply_type:ty, $reply_name:ident, $reply_initializer:tt) => { - paste::paste! { + pastey::paste! { Ok(Some([]::$reply_name( [] $reply_initializer ))) diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index a55afd13..0a3eafd2 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -1,28 +1,21 @@ -use aes::cipher::block_padding::UnpadError; -use aes::cipher::{ - block_padding::Pkcs7, generic_array::GenericArray, BlockDecryptMut, BlockEncryptMut, KeyIvInit, -}; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7}; use chrono::{DateTime, Duration, Utc}; use log::{debug, warn}; use std::string::FromUtf8Error; -use std::{ - fs::create_dir_all, - io::{Read, Write}, - path::PathBuf, -}; -use tokio::fs::{self, File}; +use std::{fs::create_dir_all, path::PathBuf}; +use tokio::fs; -use base64::{engine::general_purpose, DecodeError, Engine}; +use base64::{DecodeError, Engine, engine::general_purpose}; use crate::core::{auth::hardware::HardwareInfo, endpoints::API_PROXY_NOVAFUSION_LICENSES}; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; -use crate::util::native::{NativeError, SafeParent, SafeStr}; +use crate::util::native::{NativeError, SafeParent}; use lazy_static::lazy_static; use quick_xml::DeError; use regex::Regex; use reqwest::header::ToStrError; -use reqwest::{Client, StatusCode}; +use reqwest::{Client, StatusCode, Url}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -78,8 +71,6 @@ pub enum LicenseError { #[error(transparent)] ToStr(#[from] ToStrError), #[error(transparent)] - Unpad(#[from] UnpadError), - #[error(transparent)] Utf8(#[from] FromUtf8Error), #[error(transparent)] DeError(#[from] DeError), @@ -96,6 +87,9 @@ pub enum LicenseError { #[error("license request failed: `{0}`")] Http(String), + + #[error("xml parsing failed: `{0}`")] + SeError(#[from] quick_xml::se::SeError), } pub fn detect_ooa_state(game_path: PathBuf) -> OOAState { @@ -266,9 +260,11 @@ pub async fn request_license( query.push(("requestType", request_type)); } + let mut url = Url::parse(API_PROXY_NOVAFUSION_LICENSES).unwrap(); + url.query_pairs_mut().extend_pairs(query.iter().copied()); + let res = Client::new() - .get(API_PROXY_NOVAFUSION_LICENSES) - .query(&query) + .get(url) .header("X-Requester-Id", "Origin Online Activation") .header("User-Agent", "EACTransaction") .send() @@ -290,22 +286,19 @@ pub async fn request_license( } pub fn decrypt_license(data: &[u8]) -> Result { - let key = GenericArray::from_slice(&OOA_CRYPTO_KEY); - let iv = GenericArray::from_slice(&[0u8; 16]); - let cipher = Aes128CbcDec::new(key, iv); + let iv = [0u8; 16]; + let cipher = Aes128CbcDec::new((&OOA_CRYPTO_KEY).into(), (&iv).into()); - let decrypted_data = cipher.decrypt_padded_vec_mut::(data)?; + let decrypted_data = cipher.decrypt_padded_vec::(data).unwrap(); let data_str = String::from_utf8(decrypted_data)?; Ok(quick_xml::de::from_str(&data_str)?) } pub fn encrypt_license(data: &str) -> Result, LicenseError> { - let key = GenericArray::from_slice(&OOA_CRYPTO_KEY); - let iv = GenericArray::from_slice(&[0u8; 16]); - - let cipher = Aes128CbcEnc::new(key, iv); - Ok(cipher.encrypt_padded_vec_mut::(data.as_bytes())) + let iv = [0u8; 16]; + let cipher = Aes128CbcEnc::new((&OOA_CRYPTO_KEY).into(), (&iv).into()); + Ok(cipher.encrypt_padded_vec::(data.as_bytes())) } pub async fn save_license( diff --git a/maxima-lib/src/rtm/client.rs b/maxima-lib/src/rtm/client.rs index f5ad3288..10f2460a 100644 --- a/maxima-lib/src/rtm/client.rs +++ b/maxima-lib/src/rtm/client.rs @@ -6,15 +6,15 @@ use derive_getters::Getters; use log::{debug, error, info, warn}; use moka::sync::Cache; use serde::{Deserialize, Serialize}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use super::{ + RtmError, connection::RtmConnectionManager, proto::{ - communication_v1, success_v1, BasicPresenceType, HeartbeatV1, LoginV3Response, Player, - PresenceUpdateV1, RichPresenceType, RichPresenceV1, SessionCleanupV1, + BasicPresenceType, HeartbeatV1, LoginV3Response, Player, PresenceUpdateV1, + RichPresenceType, RichPresenceV1, SessionCleanupV1, communication_v1, success_v1, }, - RtmError, }; use crate::{ core::auth::storage::{AuthError, LockedAuthStorage, TokenError}, @@ -235,7 +235,8 @@ impl RtmClient { }).await?; for ele in res.connected_sessions { - let platform = PlatformV1::try_from(ele.platform)?; + let platform = + PlatformV1::try_from(ele.platform).unwrap_or(PlatformV1::UnknownPlatform); if platform != PlatformV1::Pc { continue; } diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index 9da67017..ff1bfcb4 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -6,14 +6,15 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -use super::proto::{communication_v1, Communication, CommunicationV1}; use super::RtmError; +use super::proto::{Communication, CommunicationV1, communication_v1}; use log::{error, warn}; use prost::{ - bytes::{Buf, BufMut, BytesMut}, Message, + bytes::{Buf, BufMut, BytesMut}, }; -use rustls::{ClientConfig, OwnedTrustAnchor}; +use rustls::{ClientConfig, RootCertStore}; +use rustls_pki_types::ServerName; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, @@ -92,25 +93,16 @@ impl RtmConnectionManager { request_rx: &mut mpsc::Receiver, update_presence_tx: &mut mpsc::Sender, ) -> Result<(), Box> { - let anchors = TLS_SERVER_ROOTS.0.iter().map(|ta| { - OwnedTrustAnchor::from_subject_spki_name_constraints( - ta.subject, - ta.spki, - ta.name_constraints, - ) - }); - - let mut store = rustls::RootCertStore::empty(); - store.add_server_trust_anchors(anchors); + let mut store = RootCertStore::empty(); + store.extend(TLS_SERVER_ROOTS.iter().cloned()); let config = ClientConfig::builder() - .with_safe_defaults() .with_root_certificates(store) .with_no_client_auth(); let connector = TlsConnector::from(Arc::new(config)); - let domain = rustls::ServerName::try_from(RTM_DOMAIN)?; + let domain = ServerName::try_from(RTM_DOMAIN)?; let mut tls_stream = connector.connect(domain, stream).await?; let mut pending_responses: HashMap> = HashMap::new(); diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 50c61a69..21c7e19d 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, env, ffi::OsStr, - fs::{create_dir_all, remove_dir_all, remove_file, File}, + fs::{File, create_dir_all, remove_dir_all, remove_file}, io::Read, path::PathBuf, process::{ExitStatus, Stdio}, @@ -23,8 +23,8 @@ 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}, + github::{GithubRelease, fetch_github_release, fetch_github_releases, github_download_asset}, + native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, registry::RegistryError, }; @@ -148,7 +148,7 @@ pub(crate) async fn check_runtime_validity( _ => { return Err(NativeError::Wine(WineError::UnimplementedRuntime( key.to_string(), - ))) + ))); } }; let path = wine_dir()?.join(key); @@ -179,7 +179,7 @@ pub(crate) async fn install_runtime( _ => { return Err(NativeError::Wine(WineError::UnimplementedRuntime( key.to_string(), - ))) + ))); } }; diff --git a/maxima-lib/src/util/github.rs b/maxima-lib/src/util/github.rs index ec79fcca..1f11d6af 100644 --- a/maxima-lib/src/util/github.rs +++ b/maxima-lib/src/util/github.rs @@ -30,13 +30,13 @@ pub fn fetch_github_releases( ); let res = ureq::get(&url) - .set("User-Agent", "ArmchairDevelopers/Maxima") + .header("User-Agent", "ArmchairDevelopers/Maxima") .call()?; if res.status() != StatusCode::OK { - return Err(DownloadError::Http(res.into_string()?)); + return Err(DownloadError::Http(res.into_body().read_to_string()?)); } - let text = res.into_string()?; + let text = res.into_body().read_to_string()?; let result = serde_json::from_str(text.as_str())?; Ok(result) } @@ -52,13 +52,13 @@ pub fn fetch_github_release( ); let res = ureq::get(&url) - .set("User-Agent", "ArmchairDevelopers/Maxima") + .header("User-Agent", "ArmchairDevelopers/Maxima") .call()?; if res.status() != StatusCode::OK { - return Err(DownloadError::Http(res.into_string()?)); + return Err(DownloadError::Http(res.into_body().read_to_string()?)); } - let text = res.into_string()?; + let text = res.into_body().read_to_string()?; let result = serde_json::from_str(text.as_str())?; Ok(result) } @@ -68,11 +68,14 @@ pub fn github_download_asset(asset: &GithubAsset, path: &PathBuf) -> Result<(), let res = ureq::get(&asset.browser_download_url).call()?; if res.status() != StatusCode::OK { - return Err(DownloadError::Http(res.into_string()?)); + return Err(DownloadError::Http(res.into_body().read_to_string()?)); } let mut body: Vec = vec![]; - res.into_reader().take(asset.size).read_to_end(&mut body)?; + res.into_body() + .into_reader() + .take(asset.size) + .read_to_end(&mut body)?; std::fs::write(path, body)?; Ok(()) diff --git a/maxima-lib/src/util/native.rs b/maxima-lib/src/util/native.rs index 5adfc60f..4a2a89ca 100644 --- a/maxima-lib/src/util/native.rs +++ b/maxima-lib/src/util/native.rs @@ -143,14 +143,14 @@ unsafe extern "system" fn enum_windows_proc( ) -> winapi::shared::minwindef::BOOL { let mut window_process_id: u32 = 0; - GetWindowThreadProcessId(hwnd, &mut window_process_id); + unsafe { GetWindowThreadProcessId(hwnd, &mut window_process_id) }; - if window_process_id != std::process::id() || IsWindowVisible(hwnd) == 0 { + if window_process_id != std::process::id() || unsafe { IsWindowVisible(hwnd) } == 0 { return winapi::shared::minwindef::TRUE; } - if IsWindowVisible(hwnd) != 0 { - SetForegroundWindow(hwnd); + if unsafe { IsWindowVisible(hwnd) } != 0 { + unsafe { SetForegroundWindow(hwnd) }; } winapi::shared::minwindef::TRUE diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 624fc0d0..5668a3fa 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -15,7 +15,7 @@ use winapi::{ shared::{minwindef::HKEY, winerror::ERROR_CANCELLED}, um::{ errhandlingapi::GetLastError, - shellapi::{ShellExecuteExW, SEE_MASK_NOCLOSEPROCESS, SEE_MASK_NO_CONSOLE}, + shellapi::{SEE_MASK_NO_CONSOLE, SEE_MASK_NOCLOSEPROCESS, ShellExecuteExW}, }, um::{ winnt::KEY_QUERY_VALUE, @@ -25,8 +25,8 @@ use winapi::{ #[cfg(windows)] use winreg::{ - enums::{HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE}, RegKey, + enums::{HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE}, }; #[cfg(unix)] @@ -35,7 +35,7 @@ use std::{collections::HashMap, env, fs}; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; -use super::native::{module_path, NativeError, SafeParent, SafeStr}; +use super::native::{NativeError, SafeParent, SafeStr, module_path}; #[cfg(target_pointer_width = "64")] pub const REG_ARCH_PATH: &str = "SOFTWARE\\WOW6432Node"; diff --git a/maxima-lib/src/util/service_win.rs b/maxima-lib/src/util/service_win.rs index 453d4743..8b344842 100644 --- a/maxima-lib/src/util/service_win.rs +++ b/maxima-lib/src/util/service_win.rs @@ -77,17 +77,17 @@ pub fn register_service() -> Result<(), BackgroundServiceControlError> { } pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlError> { - let hscm = OpenSCManagerA( + let hscm = unsafe { OpenSCManagerA( std::ptr::null(), CString::new("ServicesActive")?.as_ptr(), SC_MANAGER_ALL_ACCESS, - ); + ) }; - let hservice = OpenServiceA( + let hservice = unsafe { OpenServiceA( hscm, CString::new(SERVICE_NAME)?.as_ptr(), SERVICE_ALL_ACCESS, - ); + ) }; if hservice.is_null() { return Err(BackgroundServiceControlError::Absent); @@ -95,13 +95,13 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro // Query the service object security let mut bytes_required: u32 = 0; - let result = QueryServiceObjectSecurity( + let result = unsafe { QueryServiceObjectSecurity( hservice, DACL_SECURITY_INFORMATION, std::ptr::null_mut(), 0, &mut bytes_required, - ); + ) }; if result == 0 { // The initial call failed; check if the error was related to buffer size @@ -117,13 +117,13 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro let security_descriptor = security_descriptor_buffer.as_mut_ptr() as PSECURITY_DESCRIPTOR; // Query the service object security again with the correct buffer - let result = QueryServiceObjectSecurity( + let result = unsafe { QueryServiceObjectSecurity( hservice, DACL_SECURITY_INFORMATION, security_descriptor, bytes_required, &mut bytes_required, - ); + ) }; if result == 0 { return Err(BackgroundServiceControlError::ServiceObjectSecurity( @@ -134,13 +134,13 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro // Convert the security descriptor to a string let mut sddl_string: LPWSTR = std::ptr::null_mut(); let mut sddl_string_len: u32 = 0; - let result = ConvertSecurityDescriptorToStringSecurityDescriptorW( + let result = unsafe { ConvertSecurityDescriptorToStringSecurityDescriptorW( security_descriptor, SDDL_REVISION_1.into(), DACL_SECURITY_INFORMATION, &mut sddl_string, &mut sddl_string_len, - ); + ) }; if result == 0 { return Err(BackgroundServiceControlError::SecurityDescriptorToString( @@ -148,7 +148,7 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro )); } - let sddl = U16CString::from_ptr_str(sddl_string).to_string_lossy(); + let sddl = unsafe { U16CString::from_ptr_str(sddl_string).to_string_lossy() }; let sddl_to_add = "(A;;RPWPCR;;;BU)"; if sddl.contains(sddl_to_add) { return Ok(()); @@ -159,12 +159,12 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro let mut amended_security_descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut(); let mut amended_security_descriptor_len: u32 = 0; - let result = ConvertStringSecurityDescriptorToSecurityDescriptorW( + let result = unsafe { ConvertStringSecurityDescriptorToSecurityDescriptorW( U16CString::from_str(amended_sddl.as_str())?.as_ptr(), SDDL_REVISION_1.into(), &mut amended_security_descriptor, &mut amended_security_descriptor_len, - ); + ) }; if result == 0 { return Err(BackgroundServiceControlError::StringToSecurityDescriptor( @@ -173,11 +173,11 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro } // Set the service object security with the amended security descriptor - let result = SetServiceObjectSecurity( + let result = unsafe { SetServiceObjectSecurity( hservice, DACL_SECURITY_INFORMATION, amended_security_descriptor, - ); + ) }; if result == 0 { return Err(BackgroundServiceControlError::SecurityAttributes( @@ -233,12 +233,13 @@ pub fn is_service_running() -> Result { } pub async fn start_service() -> Result<(), BackgroundServiceControlError> { - let service_manager = service_manager(false)?; - - let service_result = - service_manager.open_service(OsString::from(SERVICE_NAME), ServiceAccess::START)?; + { + let service_manager = service_manager(false)?; + let service_result = + service_manager.open_service(OsString::from(SERVICE_NAME), ServiceAccess::START)?; - service_result.start(&[OsStr::new("")])?; + service_result.start(&[OsStr::new("")])?; + } while !is_service_running()? { tokio::time::sleep(Duration::from_millis(100)).await; @@ -248,12 +249,13 @@ pub async fn start_service() -> Result<(), BackgroundServiceControlError> { } pub async fn stop_service() -> Result<(), BackgroundServiceControlError> { - let service_manager = service_manager(false)?; - - let service_result = - service_manager.open_service(OsString::from(SERVICE_NAME), ServiceAccess::STOP)?; + { + let service_manager = service_manager(false)?; + let service_result = + service_manager.open_service(OsString::from(SERVICE_NAME), ServiceAccess::STOP)?; - service_result.stop()?; + service_result.stop()?; + } while is_service_running()? { tokio::time::sleep(Duration::from_millis(100)).await; diff --git a/maxima-lib/src/util/simple_crypto.rs b/maxima-lib/src/util/simple_crypto.rs index d07c338d..f2a40926 100644 --- a/maxima-lib/src/util/simple_crypto.rs +++ b/maxima-lib/src/util/simple_crypto.rs @@ -1,9 +1,8 @@ use std::{io::Write, num::Wrapping, str}; -use aes::cipher::{ - block_padding::Pkcs7, generic_array::GenericArray, BlockDecryptMut, BlockEncryptMut, KeyInit, -}; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyInit, block_padding::Pkcs7}; use chrono::Datelike; +use hex; const CRYPTO_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; @@ -17,17 +16,15 @@ const PRIME_30K: u32 = 350377; pub fn simple_decrypt(data: &[u8], key: &[u8; 16]) -> String { let data = get_array(str::from_utf8(data).unwrap()); - let key = GenericArray::from_slice(key); - let result = Aes128EcbDec::new(key) - .decrypt_padded_vec_mut::(&data) + let res = Aes128EcbDec::new(&(*key).into()) + .decrypt_padded_vec::(&data) .unwrap(); - String::from_utf8(result).unwrap() + String::from_utf8(res).unwrap() } pub fn simple_encrypt(data: &[u8], key: &[u8; 16]) -> String { - let key = GenericArray::from_slice(key); - let res = Aes128EcbEnc::new(key).encrypt_padded_vec_mut::(data); + let res = Aes128EcbEnc::new(&(*key).into()).encrypt_padded_vec::(&data); hex::encode(res) } diff --git a/maxima-resources/Cargo.toml b/maxima-resources/Cargo.toml index 5ef40c6f..23191a8f 100644 --- a/maxima-resources/Cargo.toml +++ b/maxima-resources/Cargo.toml @@ -2,7 +2,7 @@ name = "maxima-resources" version = "0.1.0" authors = ["Sean Kahler "] -edition = "2021" +edition = "2024" [dependencies] winres = "0.1.12" diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index aa68d90d..a361cc4e 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -2,7 +2,7 @@ name = "maxima-service" version = "0.1.0" authors = ["Sean Kahler "] -edition = "2021" +edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } diff --git a/maxima-service/src/service/error.rs b/maxima-service/src/service/error.rs index 673002be..232e7198 100644 --- a/maxima-service/src/service/error.rs +++ b/maxima-service/src/service/error.rs @@ -1,4 +1,4 @@ -use actix_web::{error, http::header::ContentType, HttpResponse}; +use actix_web::{HttpResponse, error, http::header::ContentType}; use reqwest::StatusCode; use thiserror::Error; diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index f8bfb689..c27d4515 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -1,6 +1,6 @@ use std::fs::File; -use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, get, post, web}; use log::info; use maxima::util::registry::set_up_registry; use maxima::util::service::SERVICE_NAME; @@ -20,7 +20,7 @@ use windows_service::{ use crate::service::error::ServerError; use crate::service::hash::get_sha256_hash_of_pid; -use maxima::core::background_service::{ServiceLibraryInjectionRequest, BACKGROUND_SERVICE_PORT}; +use maxima::core::background_service::{BACKGROUND_SERVICE_PORT, ServiceLibraryInjectionRequest}; use maxima::util::dll_injector::{DllInjector, InjectionError}; use maxima::util::native::SafeParent; diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index d468b23c..11fd8ee1 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -2,28 +2,28 @@ name = "maxima-tui" version = "0.1.0" authors = ["Sean Kahler "] -edition = "2021" +edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -clap = { version = "4.3.5", features = [ "derive" ] } -tokio = { version = "1.28.2", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } -serde = { version = "1.0.164", features = [ "derive" ] } -serde_json = "1.0.97" -anyhow = "1.0.71" -lazy_static = "1.4.0" -base64 = "0.21.2" -log = "0.4.19" -regex = "1.8.4" -inquire = "0.6.2" -futures = "0.3.30" -ratatui = "0.27.0" -strum = "0.26.0" -color-eyre = "0.6.3" +clap = { version = "4.6.1", features = [ "derive" ] } +tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } +serde = { version = "1.0.228", features = [ "derive" ] } +serde_json = "1.0.150" +anyhow = "1.0.102" +lazy_static = "1.5.0" +base64 = "0.22.1" +log = "0.4.30" +regex = "1.12.3" +inquire = "0.9.4" +futures = "0.3.32" +ratatui = "0.30.1" +strum = "0.28.0" +color-eyre = "0.6.5" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi" ] } -winreg = "0.50.0" +winreg = "0.56.0" is_elevated = "0.1.2" [build-dependencies] diff --git a/maxima-tui/src/main.rs b/maxima-tui/src/main.rs index f8eea414..1a982d53 100644 --- a/maxima-tui/src/main.rs +++ b/maxima-tui/src/main.rs @@ -22,26 +22,10 @@ use maxima::{ util::service::{is_service_running, is_service_valid, register_service_user, start_service}, }; -use maxima::{ - content::downloader::ZipDownloader, - core::{ - auth::{nucleus_token_exchange, TokenResponse}, - clients::JUNO_PC_CLIENT_ID, - launch::LaunchMode, - library::OwnedTitle, - service_layer::{ - ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, - ServiceLegacyOffer, ServicePlayer, SERVICE_REQUEST_GETBASICPLAYER, - SERVICE_REQUEST_GETLEGACYCATALOGDEFS, - }, - LockedMaxima, MaximaOptionsBuilder, - }, - ooa, - rtm::client::BasicPresence, -}; use maxima::{ content::ContentService, core::{ + Maxima, MaximaEvent, auth::{ context::AuthContext, login::{begin_oauth_login_flow, manual_login}, @@ -49,22 +33,38 @@ use maxima::{ }, launch::{self, LaunchOptions}, service_layer::ServiceUserGameProduct, - Maxima, MaximaEvent, }, util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, }; +use maxima::{ + content::downloader::ZipDownloader, + core::{ + LockedMaxima, MaximaOptionsBuilder, + auth::{TokenResponse, nucleus_token_exchange}, + clients::JUNO_PC_CLIENT_ID, + launch::LaunchMode, + library::OwnedTitle, + service_layer::{ + SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, + ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, + ServiceLegacyOffer, ServicePlayer, + }, + }, + ooa, + rtm::client::BasicPresence, +}; lazy_static! { static ref MANUAL_LOGIN_PATTERN: Regex = Regex::new(r"^(.*):(.*)$").unwrap(); } -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use color_eyre::config::HookBuilder; use ratatui::{ crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, + event::{self, Event, KeyCode, KeyEventKind}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }, prelude::*, style::palette::tailwind, @@ -124,7 +124,9 @@ impl App { } fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|frame| frame.render_widget(self, frame.size()))?; + terminal + .draw(|frame| frame.render_widget(self, frame.area())) + .unwrap(); Ok(()) } @@ -291,7 +293,13 @@ impl Widget for SelectedTab { impl SelectedTab { /// Return tab's name as a styled `Line` fn title(self) -> Line<'static> { - format!(" {self} ") + let title = match self { + Self::Games => "Games", + Self::Downloads => "Downloads", + Self::Settings => "Settings", + }; + + format!(" {title} ") .fg(tailwind::SLATE.c200) .bg(self.palette().c900) .into() diff --git a/maxima-tui/src/service.rs b/maxima-tui/src/service.rs index 412ffa24..9f048af7 100644 --- a/maxima-tui/src/service.rs +++ b/maxima-tui/src/service.rs @@ -1,12 +1,12 @@ use std::sync::mpsc::{self, Receiver, Sender}; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use log::info; use maxima::core::{ + LockedMaxima, Maxima, MaximaOptionsBuilder, auth::{ - context::AuthContext, login::begin_oauth_login_flow, nucleus_token_exchange, TokenResponse, + TokenResponse, context::AuthContext, login::begin_oauth_login_flow, nucleus_token_exchange, }, - LockedMaxima, Maxima, MaximaOptionsBuilder, }; pub struct InteractThreadLoginResponse { diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index 702e9c08..d294d1b1 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -2,31 +2,32 @@ name = "maxima-ui" version = "0.1.0" authors = ["Nick Whelan "] -edition = "2021" +edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi", "psapi", "wininet", "winerror", "iphlpapi", "tcpmib" ] } anyhow = "1.0.71" clap = { version = "4.3.8", features = ["derive"] } -eframe = { version = "0.28.0", features = [ "persistence" ] } -egui = "0.28.0" -egui_extras = {version = "0.28.0", features = [ "svg", "all_loaders" ] } -image = { version = "0.24.6", features = [ "png", "jpeg" ] } -reqwest = { version = "0.11.18", features = ["stream", "rustls-tls"], default-features = false } +eframe = { version = "0.34.3", features = [ "persistence", "glow" ] } +egui = "0.34.3" +egui_extras = {version = "0.34.3", features = [ "svg", "all_loaders" ] } +image = { version = "0.25.10", features = [ "png", "jpeg" ] } +reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } serde = "1.0.171" serde_json = "1.0.100" -tokio = { version = "1.28.2", features = ["full"] } +tokio = { version = "1.52.3", features = ["full"] } log = "0.4.19" -egui_demo_lib = "0.28.0" -puffin_http = "0.15.0" -puffin = "0.18.1" -strum = "0.25.0" -strum_macros = "0.25.3" -sys-locale = "0.3.1" -humansize = { version = "2.0.0", features = ["no_alloc"] } +egui_demo_lib = "0.34.3" +puffin_http = "0.17.0" +puffin = "0.20.0" +rustls = { version = "0.23", features = ["aws-lc-rs"] } +strum = { version = "0.28.0", features = ["derive"] } +strum_macros = "0.28.0" +sys-locale = "0.3.2" +humansize = { version = "2.1.3", features = ["no_alloc"] } fuzzy-matcher = "*" -thiserror = "2.0.12" +thiserror = "2.0.18" [target.'cfg(windows)'.dependencies] is_elevated = "0.1.2" diff --git a/maxima-ui/src/bridge/game_details.rs b/maxima-ui/src/bridge/game_details.rs index 3edcda31..a0d54ae1 100644 --- a/maxima-ui/src/bridge/game_details.rs +++ b/maxima-ui/src/bridge/game_details.rs @@ -2,16 +2,16 @@ use egui::Context; use std::sync::mpsc::Sender; use crate::{ + GameDetails, bridge_thread::{BackendError, InteractThreadGameDetailsResponse, MaximaLibResponse}, util::markdown::html_to_easymark, - GameDetails, }; use maxima::core::{ + LockedMaxima, service_layer::{ - ServiceGameSystemRequirements, ServiceGameSystemRequirementsRequestBuilder, - SERVICE_REQUEST_GAMESYSTEMREQUIREMENTS, + SERVICE_REQUEST_GAMESYSTEMREQUIREMENTS, ServiceGameSystemRequirements, + ServiceGameSystemRequirementsRequestBuilder, }, - LockedMaxima, }; pub async fn game_details_request( diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 305a3480..d0a7305c 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -1,18 +1,18 @@ use crate::{ + GameDetailsWrapper, GameInfo, GameVersionInfo, bridge_thread::{BackendError, InteractThreadGameListResponse, MaximaLibResponse}, ui_image::UIImageCacheLoaderCommand, - GameDetailsWrapper, GameInfo, GameVersionInfo, }; use egui::Context; use log::{debug, error, info}; use maxima::{ core::{ + LockedMaxima, service_layer::{ - ServiceGame, ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, + SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceGame, + ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, - SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, }, - LockedMaxima, }, util::native::maxima_dir, }; diff --git a/maxima-ui/src/bridge/login_oauth.rs b/maxima-ui/src/bridge/login_oauth.rs index 9813ee9d..a45e1cf5 100644 --- a/maxima-ui/src/bridge/login_oauth.rs +++ b/maxima-ui/src/bridge/login_oauth.rs @@ -2,9 +2,9 @@ use crate::bridge_thread::{BackendError, InteractThreadLoginResponse, MaximaLibR use egui::Context; use maxima::{ core::{ + LockedMaxima, auth::{context::AuthContext, login, nucleus_token_exchange}, service_layer::ServiceLayerError, - LockedMaxima, }, util::native::take_foreground_focus, }; diff --git a/maxima-ui/src/bridge/start_game.rs b/maxima-ui/src/bridge/start_game.rs index e5742e6c..91f8a5c5 100644 --- a/maxima-ui/src/bridge/start_game.rs +++ b/maxima-ui/src/bridge/start_game.rs @@ -1,8 +1,8 @@ -use crate::{bridge_thread::BackendError, GameInfo, GameSettings}; +use crate::{GameInfo, GameSettings, bridge_thread::BackendError}; use log::{debug, error, info}; use maxima::core::{ - launch::{self, LaunchError, LaunchMode, LaunchOptions}, LockedMaxima, + launch::{self, LaunchError, LaunchMode, LaunchOptions}, }; pub async fn start_game_request( diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 9a98c035..1d1da6a2 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -1,7 +1,7 @@ use crate::{ + BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, bridge_thread::{self, BackendError}, views::downloads_view::QueuedDownload, - BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, }; use log::{error, info, warn}; use std::sync::mpsc::TryRecvError; diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 456c1778..56f6fab0 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -2,6 +2,7 @@ use egui::Context; use log::{error, info, warn}; use crate::{ + GameDetails, GameInfo, GameSettings, bridge::{ game_details::game_details_request, get_friends::get_friends_request, get_games::get_games_request, login_oauth::login_oauth, start_game::start_game_request, @@ -9,28 +10,27 @@ use crate::{ event_thread::{EventThread, MaximaEventRequest, MaximaEventResponse}, ui_image::UIImageCacheLoaderCommand, views::friends_view::UIFriend, - GameDetails, GameInfo, GameSettings, }; use maxima::{ content::manager::{ ContentManager, ContentManagerError, QueuedGameBuilder, QueuedGameBuilderError, }, core::{ + LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, auth::storage::{AuthError, TokenError}, launch::LaunchError, library::LibraryError, - manifest::{self, ManifestError, MANIFEST_RELATIVE_PATH}, + manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ ServiceGameImagesRequestBuilderError, ServiceHeroBackgroundImageRequestBuilderError, ServiceLayerError, ServicePlayer, }, - LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, }, lsx::service::LSXServerError, rtm::RtmError, util::{ native::NativeError, - registry::{check_registry_validity, set_up_registry, RegistryError}, + registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; use std::sync::mpsc::{SendError, TryRecvError}; @@ -192,6 +192,7 @@ impl BridgeThread { let (rtm_responder, rtm_listener) = std::sync::mpsc::channel(); let context = ctx.clone(); + // you dare spawn a thread without catching my sneaky sneaky panics mister Potter ?? tokio::task::spawn(async move { let die_fallback_transmitter = backend_responder.clone(); //panic::set_hook(Box::new( |_| {})); diff --git a/maxima-ui/src/desktop.rs b/maxima-ui/src/desktop.rs index 2787456f..c4947173 100644 --- a/maxima-ui/src/desktop.rs +++ b/maxima-ui/src/desktop.rs @@ -1,5 +1,5 @@ use log::{error, info}; -use maxima::util::native::{maxima_dir, NativeError, SafeParent}; +use maxima::util::native::{NativeError, SafeParent, maxima_dir}; use std::collections::HashMap; use std::process::{Command, Output}; use thiserror::Error; diff --git a/maxima-ui/src/enum_locale_map.rs b/maxima-ui/src/enum_locale_map.rs index db0f4d0a..487694e5 100644 --- a/maxima-ui/src/enum_locale_map.rs +++ b/maxima-ui/src/enum_locale_map.rs @@ -1,10 +1,10 @@ use crate::{ + FrontendLanguage, translation_manager::TranslationManager, views::{ friends_view::{FriendsViewBarPage, FriendsViewBarStatusFilter}, game_view::{GameViewBarGenre, GameViewBarPlatform}, }, - FrontendLanguage, }; pub trait EnumToString { diff --git a/maxima-ui/src/event_processor.rs b/maxima-ui/src/event_processor.rs index d9fffc12..a38ff7a5 100644 --- a/maxima-ui/src/event_processor.rs +++ b/maxima-ui/src/event_processor.rs @@ -1,4 +1,4 @@ -use crate::{event_thread, MaximaEguiApp}; +use crate::{MaximaEguiApp, event_thread}; pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { puffin::profile_function!(); diff --git a/maxima-ui/src/event_thread.rs b/maxima-ui/src/event_thread.rs index f824e666..27609108 100644 --- a/maxima-ui/src/event_thread.rs +++ b/maxima-ui/src/event_thread.rs @@ -4,10 +4,10 @@ use std::sync::mpsc::{Receiver, Sender}; use crate::bridge_thread::BackendError; use log::info; use maxima::core::{ + LockedMaxima, service_layer::{ - ServiceFriends, ServiceGetMyFriendsRequestBuilder, SERVICE_REQUEST_GETMYFRIENDS, + SERVICE_REQUEST_GETMYFRIENDS, ServiceFriends, ServiceGetMyFriendsRequestBuilder, }, - LockedMaxima, }; // TODO(headassbtw): integrate this into the enum too (out of scope for the PR i wrote this in) diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index d0cacd2f..e614ac30 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -1,24 +1,24 @@ -use clap::{arg, command, Parser}; +use clap::Parser; use desktop::check_desktop_icon; use egui::{ - pos2, - style::{ScrollStyle, Spacing}, - style::{WidgetVisuals, Widgets}, - vec2, Align2, Color32, FontData, FontDefinitions, FontFamily, FontId, Layout, Margin, Rect, - Response, Rounding, Stroke, Style, TextureId, Ui, Vec2, ViewportBuilder, Visuals, Widget, + Align2, Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Layout, Margin, + Rect, Response, Stroke, Style, TextureId, Ui, UiBuilder, Vec2, ViewportBuilder, Visuals, + Widget, pos2, + style::{ScrollStyle, Spacing, WidgetVisuals, Widgets}, + vec2, }; use log::error; use maxima::{core::library::OwnedOffer, util::log::init_logger}; -use std::{collections::HashMap, default::Default, ops::RangeInclusive, path::PathBuf}; +use std::{collections::HashMap, ops::RangeInclusive, path::PathBuf, sync::Arc}; use strum_macros::EnumIter; use ui_image::{UIImageCache, UIImageType}; use views::{ debug_view::debug_view, - downloads_view::{downloads_view, QueuedDownload}, + downloads_view::{QueuedDownload, downloads_view}, friends_view::{ - friends_view, FriendsViewBar, FriendsViewBarPage, FriendsViewBarStatusFilter, UIFriend, + FriendsViewBar, FriendsViewBarPage, FriendsViewBarStatusFilter, UIFriend, friends_view, }, - game_view::{games_view, GameViewBar, GameViewBarGenre, GameViewBarPlatform}, + game_view::{GameViewBar, GameViewBarGenre, GameViewBarPlatform, games_view}, settings_view::settings_view, undefined_view::{coming_soon_view, undefined_view}, }; @@ -31,7 +31,7 @@ use app_bg_renderer::AppBgRenderer; 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 translation_manager::{TranslationManager, positional_replace}; pub mod bridge; pub mod util; @@ -65,12 +65,17 @@ struct Args { allow_bf3: bool, } -#[tokio::main] -async fn main() { +fn main() { + // Create the tokio runtime manually so eframe::run_native doesn't block + // an async worker thread (which can stall spawned async tasks). + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + let _rt_guard = rt.enter(); + + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); init_logger(); let mut args = Args::parse(); - if !std::env::var("MAXIMA_PACKAGED").is_ok_and(|var| var == "1") { + if std::env::var("MAXIMA_PACKAGED").is_ok_and(|var| var == "1") { if let Err(err) = check_desktop_icon() { error!("Failed to register desktop icon! {}", err); } @@ -107,12 +112,12 @@ async fn main() { .with_inner_size([1280.0, 720.0]) .with_min_inner_size([940.0, 480.0]) .with_app_id("io.github.ArmchairDevelopers.Maxima") - .with_icon( + .with_icon(Arc::new( eframe::icon_data::from_png_bytes( &include_bytes!("../../maxima-resources/assets/logo.png")[..], ) .unwrap(), - ), + )), ..Default::default() }; eframe::run_native( @@ -120,8 +125,7 @@ async fn main() { native_options, Box::new(move |cc| { let app = MaximaEguiApp::new(cc, args); - // Run initialization code that needs access to the UI here, but DO NOT run any long-runtime functions here, - // as it's before the UI is shown + // TODO: Implement no_login bypass (skip auth initialization) if args.no_login { return Ok(Box::new(app)); } @@ -297,7 +301,6 @@ pub struct MaximaEguiApp { backend: BridgeThread, /// what the backend doin? backend_state: BackendStallState, - /// what type of login we're using /// Slug of the game currently running, may not be fully accurate but it's good enough to let the user know the button was clicked playing_game: Option, /// Currently downloading game @@ -383,7 +386,7 @@ impl MaximaEguiApp { bg_fill: F9B233, bg_stroke: Stroke::NONE, fg_stroke: Stroke::new(1.0, Color32::BLACK), - rounding: Rounding::ZERO, + corner_radius: CornerRadius::ZERO, expansion: -1.0, }, inactive: WidgetVisuals { @@ -391,7 +394,7 @@ impl MaximaEguiApp { bg_fill: Color32::BLACK, bg_stroke: Stroke::new(2.0, Color32::WHITE), fg_stroke: Stroke::new(1.5, Color32::WHITE), - rounding: Rounding::same(2.0), + corner_radius: CornerRadius::same(2), expansion: -2.0, }, active: WidgetVisuals { @@ -399,7 +402,7 @@ impl MaximaEguiApp { bg_fill: WIDGET_HOVER.linear_multiply(0.6), bg_stroke: Stroke::NONE, fg_stroke: Stroke::new(2.0, WIDGET_HOVER.linear_multiply(0.6)), - rounding: Rounding::ZERO, + corner_radius: CornerRadius::ZERO, expansion: 0.0, }, open: WidgetVisuals { @@ -407,7 +410,7 @@ impl MaximaEguiApp { bg_fill: WIDGET_HOVER.linear_multiply(0.0), bg_stroke: Stroke::NONE, fg_stroke: Stroke::new(2.0, WIDGET_HOVER.linear_multiply(0.0)), - rounding: Rounding::ZERO, + corner_radius: CornerRadius::ZERO, expansion: 0.0, }, ..Default::default() @@ -421,7 +424,9 @@ impl MaximaEguiApp { fonts.font_data.insert( "ibm_plex".to_owned(), - FontData::from_static(include_bytes!("../fonts/IBMPlexSans-Regular.ttf")), + std::sync::Arc::new(FontData::from_static(include_bytes!( + "../fonts/IBMPlexSans-Regular.ttf" + ))), ); fonts @@ -432,7 +437,7 @@ impl MaximaEguiApp { fonts.families.get_mut(&FontFamily::Monospace).unwrap().push("ibm_plex".to_owned()); - cc.egui_ctx.set_style(style); + cc.egui_ctx.set_global_style(style); cc.egui_ctx.set_fonts(fonts); #[cfg(debug_assertions)] @@ -501,7 +506,7 @@ pub fn tab_bar_button(ui: &mut Ui, res: Response) { ); ui.painter().rect_filled( res2, - Rounding::ZERO, + CornerRadius::ZERO, if res.hovered() { ACCENT_COLOR } else { @@ -512,39 +517,39 @@ pub fn tab_bar_button(ui: &mut Ui, res: Response) { /// We used to have a semi-functional implementation that only worked on Mac, but, as i would say, we do not care 🗣️🗣️🗣️ fn custom_window_frame( - enabled: bool, // disables the entire app, used for if the bg thread crashes + enabled: bool, crash_text: String, - ctx: &egui::Context, - _: &mut eframe::Frame, + ui: &mut egui::Ui, + _frame: &mut eframe::Frame, _title: &str, add_contents: impl FnOnce(&mut egui::Ui), ) { puffin::profile_function!(); use egui::*; - let panel_frame = egui::Frame { - fill: Color32::RED, - rounding: 0.0.into(), - stroke: Stroke::NONE, - outer_margin: Margin { - left: APP_MARGIN.x, - right: APP_MARGIN.x, - top: APP_MARGIN.y + if !enabled { APP_MARGIN.y * 3.0 } else { 0.0 }, - bottom: APP_MARGIN.y, - }, - ..Default::default() - }; + let panel_frame = egui::Frame::NONE.inner_margin(Margin { + left: APP_MARGIN.x as i8, + right: APP_MARGIN.x as i8, + top: APP_MARGIN.y as i8 + + if !enabled { + (APP_MARGIN.y * 3.0) as i8 + } else { + 0 + }, + bottom: APP_MARGIN.y as i8, + }); - CentralPanel::default().frame(panel_frame).show(ctx, |ui| { + // senpai ? could you draw all over my panel uwu + egui::CentralPanel::default().frame(egui::Frame::NONE).show_inside(ui, |ui| { if !enabled { let warning_rect = Rect { - min: Pos2::ZERO, + min: ui.max_rect().min, max: pos2( - ui.available_width() + APP_MARGIN.x * 2.0, - APP_MARGIN.y * 3.0, + ui.max_rect().max.x, + ui.max_rect().min.y + APP_MARGIN.y * 3.0, ), }; - ui.painter().rect_filled(warning_rect, Rounding::same(0.0), Color32::RED); + ui.painter().rect_filled(warning_rect, CornerRadius::ZERO, Color32::RED); ui.painter().text( warning_rect.center(), Align2::CENTER_CENTER, @@ -553,16 +558,19 @@ fn custom_window_frame( Color32::BLACK, ); } - ui.add_enabled_ui(enabled, add_contents); + + panel_frame.show(ui, |ui| { + ui.add_enabled_ui(enabled, add_contents); + }); }); } /// Wrapper/helper for the tab buttons in the top left of the app fn tab_button(ui: &mut Ui, edit_var: &mut PageType, page: PageType, label: &str) { puffin::profile_function!(); - ui.style_mut().visuals.widgets.inactive.rounding = Rounding::ZERO; - ui.style_mut().visuals.widgets.active.rounding = Rounding::ZERO; - ui.style_mut().visuals.widgets.hovered.rounding = Rounding::ZERO; + ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::ZERO; + ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::ZERO; + ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::ZERO; ui.style_mut().visuals.widgets.inactive.expansion = -1.0; ui.style_mut().visuals.widgets.active.expansion = -1.0; ui.style_mut().visuals.widgets.hovered.expansion = -1.0; @@ -592,7 +600,7 @@ fn tab_button(ui: &mut Ui, edit_var: &mut PageType, page: PageType, label: &str) let test = ui.add_sized([120.0, 28.0], egui::Button::new(text)); if test.clicked() { - *edit_var = page.clone(); + *edit_var = page; } } @@ -629,14 +637,14 @@ impl MaximaEguiApp { puffin::profile_function!(); let navbar = egui::Frame::default() .stroke(Stroke::new(2.0, Color32::WHITE)) - .inner_margin(Margin::same(0.0)) - .outer_margin(Margin::same(2.0)) - .rounding(Rounding::same(4.0)); + .inner_margin(Margin::same(0)) + .outer_margin(Margin::same(2)) + .corner_radius(CornerRadius::same(4)); navbar.show(header, |ui| { ui.horizontal(|ui| { let loc = &self.locale.localization.menubar; ui.spacing_mut().item_spacing.x = 0.0; - ui.style_mut().visuals.widgets.inactive.rounding = Rounding::ZERO; + ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::ZERO; tab_button(ui, &mut self.page_view, PageType::Games, &loc.games); tab_button(ui, &mut self.page_view, PageType::Store, &loc.store); tab_button(ui, &mut self.page_view, PageType::Settings, &loc.settings); @@ -671,9 +679,10 @@ impl MaximaEguiApp { }); rtl.painter().rect( img_response.rect.expand(1.0), - Rounding::same(4.0), + CornerRadius::same(4), Color32::TRANSPARENT, stroke, + egui::StrokeKind::Inside, ); let point = img_response.rect.left_center() + vec2(-rtl.spacing().item_spacing.x, 2.0); @@ -712,7 +721,7 @@ impl MaximaEguiApp { } fn main(&mut self, app_rect: Rect, ui: &mut Ui) { - let outside_spacing = ui.spacing().item_spacing.x.clone(); + let outside_spacing = ui.spacing().item_spacing.x; ui.add_enabled_ui(self.modal.is_none(), |ui| { ui.spacing_mut().item_spacing.y = outside_spacing; let strip = StripBuilder::new(ui).size(Size::exact(38.0)).size(Size::remainder()); @@ -752,7 +761,7 @@ impl MaximaEguiApp { max: avail_rect.max, }; - main.allocate_ui_at_rect(bigmain_rect, |bigmain| { + main.scope_builder(UiBuilder::new().max_rect(bigmain_rect), |bigmain| { puffin::profile_scope!("main view"); match self.page_view { PageType::Games => games_view(self, bigmain), @@ -763,7 +772,7 @@ impl MaximaEguiApp { _ => undefined_view(self, bigmain), } }); - main.allocate_ui_at_rect(friends_rect, |friends| { + main.scope_builder(UiBuilder::new().max_rect(friends_rect), |friends| { friends_view(self, friends); }); }); @@ -771,12 +780,15 @@ impl MaximaEguiApp { }); let mut clear = false; if let Some(modal) = &self.modal { - ui.allocate_ui_at_rect(app_rect, |contents| { + ui.scope_builder(UiBuilder::new().max_rect(app_rect), |contents| { egui::Frame::default() .fill(Color32::from_black_alpha(200)) - .outer_margin(Margin::symmetric((app_rect.width() - 600.0) / 2.0, (app_rect.height() - 400.0) / 2.0)) - .inner_margin(Margin::same(12.0)) - .rounding(Rounding::same(8.0)) + .outer_margin(Margin::symmetric( + (((app_rect.width() - 600.0) / 2.0).clamp(i8::MIN as f32, i8::MAX as f32)) as i8, + (((app_rect.height() - 400.0) / 2.0).clamp(i8::MIN as f32, i8::MAX as f32)) as i8, + )) + .inner_margin(Margin::same(12)) + .corner_radius(CornerRadius::same(8)) .stroke(Stroke::new(4.0, Color32::WHITE)) .show(contents, |ui| { ui.style_mut().spacing.interact_size = vec2(100.0, 30.0); @@ -799,11 +811,14 @@ impl MaximaEguiApp { ui.add_enabled(game.has_cloud_saves, egui::Checkbox::new(&mut settings.cloud_saves, &self.locale.localization.modals.game_settings.cloud_saves)); ui.label(&self.locale.localization.modals.game_settings.launch_arguments); - ui.add_sized(vec2(ui.available_width(), ui.style().spacing.interact_size.y), egui::TextEdit::singleline(&mut settings.launch_args).vertical_align(egui::Align::Center)); + let launch_args_size = vec2(ui.available_width(), ui.style().spacing.interact_size.y); + ui.add_sized( + launch_args_size, + egui::TextEdit::singleline(&mut settings.launch_args).vertical_align(egui::Align::Center) + ); ui.separator(); - let button_size = vec2(100.0, 30.0); ui.label(&self.locale.localization.modals.game_settings.executable_override); @@ -815,7 +830,8 @@ impl MaximaEguiApp { ui.separator(); } - ui.allocate_space(ui.available_size_before_wrap() - vec2(0.0, ui.spacing().interact_size.y)); + let remaining_space = ui.available_size_before_wrap() - vec2(0.0, ui.spacing().interact_size.y); + ui.allocate_space(remaining_space); ui.horizontal(|ui| { ui.label(positional_replace!(self.locale.localization.modals.game_settings.version, "version", &game.version.installed)); @@ -903,7 +919,7 @@ impl MaximaEguiApp { ui.horizontal(|ui| { ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::new(2.0, F9B233); ui.style_mut().visuals.widgets.hovered.expansion = -2.0; - ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(2.0); + ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::same(2); ui.add_sized(size, egui::TextEdit::singleline(&mut self.installer_state.install_folder).vertical_align(egui::Align::Center)); }); let path = PathBuf::from(self.installer_state.install_folder.clone()); @@ -957,7 +973,11 @@ impl MaximaEguiApp { ui.label(positional_replace!(&self.locale.localization.modals.game_launch_out_of_date.comparison, "local", &game.version.installed, "online", &game.version.latest)); ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| { - if ui.add_sized([ui.available_size_before_wrap().x, ui.spacing().interact_size.y], egui::Button::new(&self.locale.localization.modals.game_launch_out_of_date.launch)).clicked() { + let launch_btn_size = [ui.available_size_before_wrap().x, ui.spacing().interact_size.y]; + if ui.add_sized( + launch_btn_size, + egui::Button::new(&self.locale.localization.modals.game_launch_out_of_date.launch) + ).clicked() { self.playing_game = Some(game.slug.clone()); let settings = self.settings.game_settings.get(&game.slug); let settings = if let Some(settings) = settings { @@ -978,7 +998,8 @@ impl MaximaEguiApp { }); } } - ui.allocate_space(ui.available_size_before_wrap()); + let final_fill_size = ui.available_size_before_wrap(); + ui.allocate_space(final_fill_size); }); }); } @@ -1015,42 +1036,39 @@ impl MaximaEguiApp { self.backend_state = BackendStallState::LoggingIn; } } -} - -impl eframe::App for MaximaEguiApp { - fn save(&mut self, storage: &mut dyn eframe::Storage) { - eframe::set_value(storage, "settings", &self.settings); - } - - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - puffin::profile_function!(); - bridge_processor::frontend_processor(self, ctx); - event_processor::frontend_processor(self, ctx); - custom_window_frame( - self.critical_error.is_none(), - self.locale.localization.errors.critical_thread_crashed.clone(), - ctx, - frame, - "Maxima", - |ui| { - if let Some(render) = &self.app_bg_renderer { - let mut fullrect = ui.available_rect_before_wrap().clone(); - fullrect.min -= APP_MARGIN; - fullrect.max += APP_MARGIN; - let has_game_img = self.backend_state == BackendStallState::BingChilling - && self.games.len() > 0; - let gaming = self.page_view == PageType::Games && has_game_img; - let how_game: f32 = ctx - .animate_bool(egui::Id::new("MainAppBackgroundGamePageFadeBool"), gaming); - if has_game_img { - if self.game_sel.is_empty() && self.games.len() > 0 { - if let Some(key) = self.games.keys().next() { - self.game_sel = key.clone() - } - } + fn render_main_ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + if let Some(render) = &self.app_bg_renderer { + let mut fullrect = ui.available_rect_before_wrap(); + fullrect.min -= APP_MARGIN; + fullrect.max += APP_MARGIN; + + let has_game_img = + self.backend_state == BackendStallState::BingChilling && !self.games.is_empty(); + let gaming = self.page_view == PageType::Games && has_game_img; + let how_game: f32 = + ctx.animate_bool(egui::Id::new("MainAppBackgroundGamePageFadeBool"), gaming); + + if has_game_img { + if self.game_sel.is_empty() && !self.games.is_empty() { + if let Some(key) = self.games.keys().next() { + self.game_sel = key.clone(); + } + } - match &self.img_cache.get(ui_image::UIImageType::Background( + match &self.img_cache.get(ui_image::UIImageType::Background( + self.games[&self.game_sel].slug.clone(), + )) { + Some(tex) => render.draw( + ui, + fullrect, + tex.size_vec2(), + tex.id(), + how_game, + self.settings.performance_settings, + ), + None => { + match &self.img_cache.get(ui_image::UIImageType::Hero( self.games[&self.game_sel].slug.clone(), )) { Some(tex) => render.draw( @@ -1062,113 +1080,105 @@ impl eframe::App for MaximaEguiApp { self.settings.performance_settings, ), None => { - match &self.img_cache.get(ui_image::UIImageType::Hero( - self.games[&self.game_sel].slug.clone(), - )) { - Some(tex) => render.draw( - ui, - fullrect, - tex.size_vec2(), - tex.id(), - how_game, - self.settings.performance_settings, - ), - None => { - render.draw( - ui, - fullrect, - fullrect.size(), - TextureId::Managed(1), - 0.0, - self.settings.performance_settings, - ); - } - } + render.draw( + ui, + fullrect, + fullrect.size(), + TextureId::Managed(1), + 0.0, + self.settings.performance_settings, + ); } } - } else { - render.draw( - ui, - fullrect, - fullrect.size(), - TextureId::Managed(1), - 0.0, - self.settings.performance_settings, - ); } } - let app_rect = ui.available_rect_before_wrap().clone(); - match self.backend_state { - BackendStallState::Starting => { - ui.painter().text( - app_rect.center(), - Align2::CENTER_CENTER, - &self.locale.localization.startup_flow.starting, - FontId::proportional(30.0), - Color32::WHITE, - ); - ui.put(app_rect, egui::Spinner::new().size(300.0)); - } - BackendStallState::UserNeedsToInstallService => { - let main_block_rect = ui.painter().text( - app_rect.center(), - Align2::CENTER_BOTTOM, - &self.locale.localization.startup_flow.service_installer_description, - egui::FontId::proportional(20.0), - Color32::GRAY, - ); - ui.painter().text( - main_block_rect.center_top() - vec2(0.0, 4.0), - Align2::CENTER_BOTTOM, - &self.locale.localization.startup_flow.service_installer_header, - egui::FontId::proportional(30.0), - Color32::WHITE, - ); - let button_rect = Rect { - min: main_block_rect.center_bottom() + vec2(-60.0, 4.0), - max: main_block_rect.center_bottom() + vec2(60.0, 34.0), - }; - if ui - .put( - button_rect, - egui::Button::new( - &self - .locale - .localization - .startup_flow - .service_installer_button - .to_ascii_uppercase(), - ), - ) - .clicked() - { - self.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::StartService) - .unwrap(); - self.backend_state = BackendStallState::Starting; - } - } - BackendStallState::UserNeedsToLogIn => { - self.login(app_rect, ui); - } - BackendStallState::LoggingIn => { - ui.painter().text( - app_rect.center(), - Align2::CENTER_CENTER, - &self.locale.localization.startup_flow.logging_in, - FontId::proportional(30.0), - Color32::WHITE, - ); - ui.put(app_rect, egui::Spinner::new().size(300.0)); - } - BackendStallState::BingChilling => { - self.main(app_rect, ui); - } + } else { + render.draw( + ui, + fullrect, + fullrect.size(), + TextureId::Managed(1), + 0.0, + self.settings.performance_settings, + ); + } + } + + let app_rect = ui.available_rect_before_wrap(); + match self.backend_state { + BackendStallState::Starting => { + ui.painter().text( + app_rect.center(), + Align2::CENTER_CENTER, + &self.locale.localization.startup_flow.starting, + FontId::proportional(30.0), + Color32::WHITE, + ); + ui.put(app_rect, egui::Spinner::new().size(300.0)); + } + BackendStallState::UserNeedsToInstallService => { + let main_block_rect = ui.painter().text( + app_rect.center(), + Align2::CENTER_BOTTOM, + &self.locale.localization.startup_flow.service_installer_description, + egui::FontId::proportional(20.0), + Color32::GRAY, + ); + ui.painter().text( + main_block_rect.center_top() - vec2(0.0, 4.0), + Align2::CENTER_BOTTOM, + &self.locale.localization.startup_flow.service_installer_header, + egui::FontId::proportional(30.0), + Color32::WHITE, + ); + let button_rect = Rect { + min: main_block_rect.center_bottom() + vec2(-60.0, 4.0), + max: main_block_rect.center_bottom() + vec2(60.0, 34.0), }; - }, - ); - puffin::GlobalProfiler::lock().new_frame(); + if ui + .put( + button_rect, + egui::Button::new( + &self + .locale + .localization + .startup_flow + .service_installer_button + .to_ascii_uppercase(), + ), + ) + .clicked() + { + self.backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::StartService) + .unwrap(); + self.backend_state = BackendStallState::Starting; + } + } + BackendStallState::UserNeedsToLogIn => { + self.login(app_rect, ui); + } + BackendStallState::LoggingIn => { + ui.painter().text( + app_rect.center(), + Align2::CENTER_CENTER, + &self.locale.localization.startup_flow.logging_in, + FontId::proportional(30.0), + Color32::WHITE, + ); + ui.put(app_rect, egui::Spinner::new().size(300.0)); + } + BackendStallState::BingChilling => { + self.main(app_rect, ui); + } + }; + } +} + +impl eframe::App for MaximaEguiApp { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value(storage, "settings", &self.settings); } fn on_exit(&mut self, _gl: Option<&glow::Context>) { @@ -1177,4 +1187,26 @@ impl eframe::App for MaximaEguiApp { .send(bridge_thread::MaximaLibRequest::ShutdownRequest) .unwrap(); } + + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + puffin::profile_function!(); + bridge_processor::frontend_processor(self, ui.ctx()); + event_processor::frontend_processor(self, ui.ctx()); + + let ctx = ui.ctx().clone(); + custom_window_frame( + self.critical_error.is_none(), + if let Some(err) = &self.critical_error { + format!("{:?}", err) + } else { + String::new() + }, + ui, + frame, + "Maxima", + |ui_inner| { + self.render_main_ui(ui_inner, &ctx); + }, + ); + } } diff --git a/maxima-ui/src/renderers/app_bg_renderer.rs b/maxima-ui/src/renderers/app_bg_renderer.rs index 3e0d492f..33c21fd4 100644 --- a/maxima-ui/src/renderers/app_bg_renderer.rs +++ b/maxima-ui/src/renderers/app_bg_renderer.rs @@ -1,12 +1,11 @@ -use eframe::egui_glow; use eframe::glow::{BLEND, TEXTURE_2D}; use egui::mutex::Mutex; use egui::{TextureId, Vec2}; -use egui_glow::glow; use log::error; use std::sync::Arc; use crate::FrontendPerformanceSettings; +use crate::egui_glow; /// FUCK pub struct AppBgRenderer { @@ -55,15 +54,15 @@ impl AppBgRenderer { #[allow(unsafe_code)] //MOM COME PICK ME UP, THEY'RE USING UNSAFE CODE struct ABGUnsafe { //I say this despite having used C++ for years before rust - program: glow::Program, - app_dimensions: glow::NativeUniformLocation, - img_dimensions: glow::NativeUniformLocation, - flags: glow::NativeUniformLocation, + program: egui_glow::glow::Program, + app_dimensions: egui_glow::glow::NativeUniformLocation, + img_dimensions: egui_glow::glow::NativeUniformLocation, + flags: egui_glow::glow::NativeUniformLocation, } impl ABGUnsafe { - fn new(gl: &glow::Context) -> Option { - use glow::HasContext as _; + fn new(gl: &egui_glow::glow::Context) -> Option { + use egui_glow::glow::HasContext as _; let glsl_version = egui_glow::ShaderVersion::get(gl); @@ -80,8 +79,8 @@ impl ABGUnsafe { let fsource = include_str!("../../shaders/abg.frag"); let shader_src = [ - (glow::VERTEX_SHADER, vsource), - (glow::FRAGMENT_SHADER, fsource), + (egui_glow::glow::VERTEX_SHADER, vsource), + (egui_glow::glow::FRAGMENT_SHADER, fsource), ]; let shaders: Vec<_> = shader_src @@ -130,15 +129,15 @@ impl ABGUnsafe { fn paint( &self, - gl: &glow::Context, + gl: &egui_glow::glow::Context, size: Vec2, img_size: Vec2, - img: glow::Texture, + img: egui_glow::glow::Texture, game_fade: f32, flags: FrontendPerformanceSettings, ) { puffin::profile_function!(); - use glow::HasContext as _; + use egui_glow::glow::HasContext as _; unsafe { gl.use_program(Some(self.program)); gl.uniform_3_f32(Some(&self.app_dimensions), size.x, size.y, game_fade); @@ -158,10 +157,10 @@ impl ABGUnsafe { //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //glBlendFuncSeparate(); /* - gl.blend_func(glow::FUNC_ADD, glow::ONE_MINUS_SRC_ALPHA); + gl.blend_func(egui_glow::glow::FUNC_ADD, egui_glow::glow::ONE_MINUS_SRC_ALPHA); */ - //gl.draw_arrays(glow::TRIANGLES, 6, 6); - gl.draw_arrays(glow::TRIANGLES, 0, 12); + //gl.draw_arrays(egui_glow::glow::TRIANGLES, 6, 6); + gl.draw_arrays(egui_glow::glow::TRIANGLES, 0, 12); } } } diff --git a/maxima-ui/src/renderers/game_view_bg_renderer.rs b/maxima-ui/src/renderers/game_view_bg_renderer.rs index f7c8690a..7b95d25b 100644 --- a/maxima-ui/src/renderers/game_view_bg_renderer.rs +++ b/maxima-ui/src/renderers/game_view_bg_renderer.rs @@ -2,7 +2,7 @@ // :) use eframe::egui_glow; -use eframe::glow::{NativeUniformLocation, BLEND, TEXTURE_2D}; +use eframe::glow::{BLEND, NativeUniformLocation, TEXTURE_2D}; use egui::mutex::Mutex; use egui::{TextureId, Vec2}; use egui_glow::glow; diff --git a/maxima-ui/src/ui_image.rs b/maxima-ui/src/ui_image.rs index cc7fce4f..d0bbd002 100644 --- a/maxima-ui/src/ui_image.rs +++ b/maxima-ui/src/ui_image.rs @@ -6,8 +6,8 @@ use std::{ fs, path::PathBuf, sync::{ - mpsc::{Receiver, Sender}, Arc, Mutex, + mpsc::{Receiver, Sender}, }, }; use tokio::{fs::File, io}; @@ -15,7 +15,7 @@ use tokio::{fs::File, io}; use log::{debug, error, info}; use image::io::Reader as ImageReader; -use maxima::util::native::{maxima_dir, NativeError, SafeStr}; +use maxima::util::native::{NativeError, SafeStr, maxima_dir}; #[derive(Clone, PartialEq, Eq, Hash, std::fmt::Debug)] pub enum UIImageType { diff --git a/maxima-ui/src/views/downloads_view.rs b/maxima-ui/src/views/downloads_view.rs index 65cc692b..f3d93ad5 100644 --- a/maxima-ui/src/views/downloads_view.rs +++ b/maxima-ui/src/views/downloads_view.rs @@ -1,7 +1,10 @@ -use egui::{pos2, vec2, Align2, Color32, FontId, Mesh, Rect, Rounding, Shape, Stroke, Ui, Widget}; +use egui::{ + Align2, Color32, CornerRadius, FontId, Mesh, Rect, Shape, Stroke, StrokeKind, Ui, Widget, pos2, + vec2, +}; use humansize::DECIMAL; -use crate::{MaximaEguiApp, APP_MARGIN}; +use crate::{APP_MARGIN, MaximaEguiApp}; #[derive(Clone)] pub struct QueuedDownload { @@ -32,19 +35,23 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is max: ui.cursor().min + container_size, }; - let corner_radius = 8.0; + let corner_radius = 8; - ui.painter().rect_filled(rect, Rounding::same(corner_radius), Color32::BLACK); + ui.painter().rect_filled(rect, CornerRadius::same(corner_radius), Color32::BLACK); - let img_rounding = Rounding { + let img_rounding = CornerRadius { nw: corner_radius, - ne: 0.0, + ne: 0, sw: corner_radius, - se: 0.0, + se: 0, }; let img_response = if let Some(img) = hero { let img_size = vec2((160.0 / img.size_vec2().y) * img.size_vec2().x, 160.0); - ui.add(egui::Image::new((img.id(), img_size)).rounding(img_rounding).max_size(img_size)) + ui.add( + egui::Image::new((img.id(), img_size)) + .corner_radius(img_rounding) + .max_size(img_size), + ) } else { let response = ui.allocate_response(vec2(284.0, 16.0), egui::Sense::click()); ui.painter().rect_filled(response.rect, img_rounding, Color32::GRAY); @@ -84,9 +91,10 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is ui.painter().rect( rect, - Rounding::same(corner_radius), + CornerRadius::same(corner_radius), Color32::TRANSPARENT, Stroke::new(2.0, Color32::WHITE), + StrokeKind::Outside, ); ui.painter().text( @@ -103,20 +111,21 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is let button_size = 40.0; let right_button_rect = Rect { min: pos2( - rect.max.x - ((corner_radius - 1.0) + button_size), - rect.min.y + (corner_radius - 1.0), + rect.max.x - ((corner_radius as f32 - 1.0) + button_size), + rect.min.y + (corner_radius as f32 - 1.0), ), max: pos2( - rect.max.x - (corner_radius - 1.0), - rect.min.y + (corner_radius - 1.0) + button_size, + rect.max.x - (corner_radius as f32 - 1.0), + rect.min.y + (corner_radius as f32 - 1.0) + button_size, ), }; - let left_button_rect = right_button_rect.translate(vec2(0.0, button_size + corner_radius)); + let left_button_rect = + right_button_rect.translate(vec2(0.0, button_size + corner_radius as f32)); if is_current { let progress_bar_rect = Rect { min: img_response.rect.max + vec2(0.0, -18.0), - max: rect.max - vec2(corner_radius, corner_radius), + max: rect.max - vec2(corner_radius as f32, corner_radius as f32), }; let progress_bar_progress = Rect { @@ -131,11 +140,11 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is ui.painter().rect_filled( progress_bar_rect, - Rounding::same(0.0), + CornerRadius::same(0), Color32::from_white_alpha(60), ); - ui.painter().rect_filled(progress_bar_progress, Rounding::same(0.0), Color32::WHITE); + ui.painter().rect_filled(progress_bar_progress, CornerRadius::same(0), Color32::WHITE); ui.painter().text( progress_bar_rect.min - vec2(0.0, 8.0), @@ -166,7 +175,7 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is } } else { ui.painter().text( - img_response.rect.max + vec2(18.0, -corner_radius), + img_response.rect.max + vec2(18.0, -(corner_radius as f32)), Align2::LEFT_BOTTOM, "Queued", FontId::proportional(22.0), diff --git a/maxima-ui/src/views/friends_view.rs b/maxima-ui/src/views/friends_view.rs index 1afcf154..7f280653 100644 --- a/maxima-ui/src/views/friends_view.rs +++ b/maxima-ui/src/views/friends_view.rs @@ -1,10 +1,10 @@ -use egui::{pos2, vec2, Align2, Color32, FontId, Id, Rect, Rounding, Stroke, Ui, Vec2}; -use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use egui::{Align2, Color32, CornerRadius, FontId, Id, Rect, Stroke, Ui, Vec2, pos2, vec2}; +use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use maxima::rtm::client::BasicPresence; use crate::{ - translation_manager::positional_replace, widgets::enum_dropdown::enum_dropdown, MaximaEguiApp, - FRIEND_INGAME_COLOR, + FRIEND_INGAME_COLOR, MaximaEguiApp, translation_manager::positional_replace, + widgets::enum_dropdown::enum_dropdown, }; use strum_macros::EnumIter; @@ -50,11 +50,12 @@ const DARK_GREY: Color32 = Color32::from_rgb(64, 64, 64); const PFP_SIZE: f32 = 36.0; const PFP_CORNER_RADIUS: f32 = 2.0; const PFP_ELEMENT_SIZE: f32 = PFP_SIZE + PFP_CORNER_RADIUS * 2.0; -const FRIEND_HIGHLIGHT_ROUNDING: Rounding = Rounding { - nw: 6.0, - ne: 4.0, - sw: 6.0, - se: 4.0, +const FRIEND_HIGHLIGHT_ROUNDING: CornerRadius = CornerRadius { + // le corner radius oui oui, le rounding is no exist non non + nw: 6, + ne: 4, + sw: 6, + se: 4, }; // the status border is flawed somehow, this "fixes" it slightly more than if i didn't const ITEM_SPACING: Vec2 = vec2(5.0, 5.0); @@ -106,25 +107,25 @@ pub fn friends_view(app: &mut MaximaEguiApp, ui: &mut Ui) { ui.visuals_mut().widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::WHITE); ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::new(2.0, DARK_GREY); - ui.visuals_mut().widgets.inactive.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.inactive.corner_radius = CornerRadius::same(2); ui.visuals_mut().widgets.active.bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.active.weak_bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); ui.visuals_mut().widgets.active.bg_stroke = Stroke::new(2.0, DARK_GREY); - ui.visuals_mut().widgets.active.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.active.corner_radius = CornerRadius::same(2); ui.visuals_mut().widgets.hovered.bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.hovered.weak_bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.hovered.fg_stroke = Stroke::new(2.0, F9B233); ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::new(2.0, F9B233); - ui.visuals_mut().widgets.hovered.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.hovered.corner_radius = CornerRadius::same(2); ui.visuals_mut().widgets.open.bg_fill = DARK_GREY; ui.visuals_mut().widgets.open.weak_bg_fill = DARK_GREY; ui.visuals_mut().widgets.open.fg_stroke = Stroke::new(2.0, Color32::WHITE); ui.visuals_mut().widgets.open.bg_stroke = Stroke::new(2.0, DARK_GREY); - ui.visuals_mut().widgets.open.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.open.corner_radius = CornerRadius::same(2); if ui.add_sized( [ui.available_width(), 20.0], @@ -188,13 +189,13 @@ pub fn friends_view(app: &mut MaximaEguiApp, ui: &mut Ui) { // scrollbar ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::WHITE; - ui.style_mut().visuals.widgets.inactive.rounding = Rounding::same(4.0); - ui.style_mut().visuals.widgets.active.rounding = Rounding::same(4.0); - ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(4.0); + ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::same(4); + ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::same(4); + ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::same(4); ui.style_mut().spacing.scroll.floating = false; let clip_rect = ui.available_rect_before_wrap().clone(); egui::ScrollArea::new([false,friend_rect_hovered]) - .id_source("FriendsListFriendListScrollArea") //hmm yes, the friends list is made of friends list + .id_salt("FriendsListFriendListScrollArea") //hmm yes, the friends list is made of friends list .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) .auto_shrink([false, false]) .max_width(max_width) @@ -299,7 +300,7 @@ pub fn friends_view(app: &mut MaximaEguiApp, ui: &mut Ui) { main_painter.image(app.img_cache.placeholder_avatar.id(), pfp_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); } - main_painter.rect(outline_rect, Rounding::same(4.0), Color32::TRANSPARENT, Stroke::new(2.0, friend_color)); + main_painter.rect(outline_rect, CornerRadius::same(4), Color32::TRANSPARENT, Stroke::new(2.0, friend_color), egui::StrokeKind::Outside); let text_col = if main_res.is_pointer_button_down_on() || main_res.hovered() || buttons { Color32::BLACK diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index 5caae1ea..3fcc624c 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -1,13 +1,13 @@ use crate::{ - bridge_thread, set_app_modal, translation_manager::TranslationManager, - widgets::enum_dropdown::enum_dropdown, GameDetails, GameDetailsWrapper, GameInfo, - InstallModalState, MaximaEguiApp, PageType, PopupModal, + GameDetails, GameDetailsWrapper, GameInfo, InstallModalState, MaximaEguiApp, PageType, + PopupModal, bridge_thread, set_app_modal, translation_manager::TranslationManager, + widgets::enum_dropdown::enum_dropdown, }; use egui::{ - pos2, vec2, Color32, Margin, Mesh, Pos2, Rect, RichText, Rounding, ScrollArea, Shape, Stroke, - Ui, Widget, + Color32, CornerRadius, Margin, Mesh, Pos2, Rect, RichText, ScrollArea, Shape, Stroke, Ui, + UiBuilder, UiKind, Widget, pos2, vec2, }; -use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use strum_macros::EnumIter; @@ -41,7 +41,7 @@ fn skeleton_text_block(ui: &mut egui::Ui, width: f32, height: f32) { let mut skeleton_rect = ui.available_rect_before_wrap(); skeleton_rect.set_width(width); skeleton_rect.set_height(height); - ui.painter().rect_filled(skeleton_rect, Rounding::same(2.0), SKELETON_TEXT_COLOR); + ui.painter().rect_filled(skeleton_rect, CornerRadius::same(2), SKELETON_TEXT_COLOR); ui.allocate_space(vec2(width, height)); } @@ -49,16 +49,16 @@ fn skeleton_text_block1(ui: &mut egui::Ui, width: f32, width1: f32, height: f32) let mut rect = ui.available_rect_before_wrap(); rect.set_width(width); rect.set_height(height); - ui.painter().rect_filled(rect, Rounding::same(2.0), SKELETON_INFO_COLOR); + ui.painter().rect_filled(rect, CornerRadius::same(2), SKELETON_INFO_COLOR); rect.min.x = rect.max.x + ui.spacing().item_spacing.x; rect.set_width(width1); - ui.painter().rect_filled(rect, Rounding::same(2.0), SKELETON_TEXT_COLOR); + ui.painter().rect_filled(rect, CornerRadius::same(2), SKELETON_TEXT_COLOR); ui.allocate_space(vec2(width + width1 + ui.spacing().item_spacing.x, height)); } fn game_view_action_button(text: String, ui: &mut Ui) -> bool { egui::Button::new(egui::RichText::new(text).size(20.0).color(Color32::WHITE)) - .rounding(Rounding::same(2.0)) + .corner_radius(CornerRadius::same(2)) .min_size(vec2(50.0, 40.0)) .ui(ui) .clicked() @@ -67,7 +67,7 @@ fn game_view_action_button(text: String, ui: &mut Ui) -> bool { fn game_view_action_buttons(app: &mut MaximaEguiApp, game: &GameInfo, ui: &mut Ui) { let localization = &app.locale.localization.games_view.main; let buttons_frame = egui::Frame::default() - .outer_margin(Margin::symmetric(0.0, 8.0)) + .outer_margin(Margin::symmetric(0, 8)) .fill(Color32::TRANSPARENT); buttons_frame.show(ui, |buttons| { puffin::profile_scope!("action buttons"); @@ -239,14 +239,14 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ui.vertical(|ui| { // scrollbar ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::WHITE; - ui.style_mut().visuals.widgets.inactive.rounding = Rounding::same(4.0); - ui.style_mut().visuals.widgets.active.rounding = Rounding::same(4.0); - ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(4.0); + ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::same(4); + ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::same(4); + ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::same(4); let mut logo_transition_frac = 0.0; ScrollArea::vertical() .auto_shrink(false) - .id_source("GameViewPanel_ScrollerArea") + .id_salt("GameViewPanel_ScrollerArea") .show(ui, |ui| { puffin::profile_scope!("details"); let hero_height_capped = hero_rect.size().y.max(0.0); @@ -278,7 +278,7 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ); } } else { - ui.painter().rect_filled(hero_rect, Rounding::same(0.0), Color32::TRANSPARENT); + ui.painter().rect_filled(hero_rect, CornerRadius::same(0), Color32::TRANSPARENT); } if hero_vis_frac < 1.0 @@ -308,26 +308,26 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { } let avoid_scrollbar_margin = Margin { - left: 0.0, - right: ui.style().spacing.scroll.bar_width - + ui.style().spacing.scroll.bar_inner_margin, - top: 0.0, - bottom: 0.0, + left: 0, + right: (ui.style().spacing.scroll.bar_width + + ui.style().spacing.scroll.bar_inner_margin) as i8, + top: 0, + bottom: 0, }; - let mut bar_rounding = Rounding::same(3.0); - bar_rounding.nw = 0.0; - bar_rounding.ne = 0.0; + let mut bar_rounding = CornerRadius::same(3); + bar_rounding.nw = 0; + bar_rounding.ne = 0; let play_bar_frame = egui::Frame::default() .outer_margin(avoid_scrollbar_margin) - .rounding(Rounding::ZERO); + .corner_radius(CornerRadius::ZERO); play_bar_frame.show(ui, |ui| { ui.vertical(|ui| { ui.spacing_mut().item_spacing.y = 0.0; let stats_frame = egui::Frame::default() .fill(Color32::WHITE) - .rounding(bar_rounding) - .inner_margin(Margin::same(4.0)); + .corner_radius(bar_rounding) + .inner_margin(Margin::same(4)); stats_frame.show(ui, |stats| { puffin::profile_scope!("stats"); stats.horizontal(|stats| { @@ -368,7 +368,7 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { skeleton_rect.set_width(126.0); stats.painter().rect_filled( skeleton_rect, - Rounding::same(2.0), + CornerRadius::same(2), SKELETON_TEXT_COLOR, ); stats.allocate_space(vec2(126.0, 0.0)); @@ -377,7 +377,7 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { skeleton_rect.set_width(126.0); stats.painter().rect_filled( skeleton_rect, - Rounding::same(2.0), + CornerRadius::same(2), SKELETON_TEXT_COLOR, ); } @@ -403,10 +403,10 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { dlc.allocate_exact_size(vec2(400.0, 200.0), egui::Sense::click()); dlc.painter().rect_filled( rect, - Rounding::same(4.0), + CornerRadius::same(4), Color32::from_black_alpha(128), ); - dlc.allocate_ui_at_rect(rect, |dlc| { + dlc.scope_builder(UiBuilder::new().max_rect(rect), |dlc| { let _ = match package.product().status() { maxima::core::service_layer::ServiceOwnershipStatus::Active => true, maxima::core::service_layer::ServiceOwnershipStatus::Disabled => false, @@ -467,12 +467,12 @@ fn game_list_button_context_menu(app: &MaximaEguiApp, game: &GameInfo, ui: &mut let _ = app.backend.backend_commander.send( crate::bridge_thread::MaximaLibRequest::StartGameRequest(game.clone(), settings), ); - play_button.close_menu(); + play_button.close_kind(UiKind::Menu); } }); ui.separator(); if ui.button("UNINSTALL").clicked() { - ui.close_menu(); + ui.close_kind(UiKind::Menu); } } @@ -487,8 +487,8 @@ fn show_game_list_buttons(app: &mut MaximaEguiApp, ui: &mut Ui) { 10. * app.game_view_bar.game_size, ); ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::WHITE; //scroll bar - //create a rect that takes up all the vertical space in the window, and prohibits anything from going beyond that without us knowing, so we can add a scroll bar - //because apparently some dumb fucks (me) buy EA games and can overflow the list on the default window size + //create a rect that takes up all the vertical space in the window, and prohibits anything from going beyond that without us knowing, so we can add a scroll bar + //because apparently some dumb fucks (me) buy EA games and can overflow the list on the default window size ui.vertical(|ui| { ui.vertical(|filter_chunk| { filter_chunk.visuals_mut().extreme_bg_color = Color32::TRANSPARENT; @@ -499,25 +499,25 @@ fn show_game_list_buttons(app: &mut MaximaEguiApp, ui: &mut Ui) { filter_chunk.visuals_mut().widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::WHITE); filter_chunk.visuals_mut().widgets.inactive.bg_stroke = Stroke::new(2.0, DARK_GREY); - filter_chunk.visuals_mut().widgets.inactive.rounding = Rounding::same(2.0); + filter_chunk.visuals_mut().widgets.inactive.corner_radius = CornerRadius::same(2); filter_chunk.visuals_mut().widgets.active.bg_fill = Color32::TRANSPARENT; filter_chunk.visuals_mut().widgets.active.weak_bg_fill = Color32::TRANSPARENT; filter_chunk.visuals_mut().widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); filter_chunk.visuals_mut().widgets.active.bg_stroke = Stroke::new(2.0, DARK_GREY); - filter_chunk.visuals_mut().widgets.active.rounding = Rounding::same(2.0); + filter_chunk.visuals_mut().widgets.active.corner_radius = CornerRadius::same(2); filter_chunk.visuals_mut().widgets.hovered.bg_fill = Color32::TRANSPARENT; filter_chunk.visuals_mut().widgets.hovered.weak_bg_fill = Color32::TRANSPARENT; filter_chunk.visuals_mut().widgets.hovered.fg_stroke = Stroke::new(2.0, F9B233); filter_chunk.visuals_mut().widgets.hovered.bg_stroke = Stroke::new(2.0, F9B233); - filter_chunk.visuals_mut().widgets.hovered.rounding = Rounding::same(2.0); + filter_chunk.visuals_mut().widgets.hovered.corner_radius = CornerRadius::same(2); filter_chunk.visuals_mut().widgets.open.bg_fill = DARK_GREY; filter_chunk.visuals_mut().widgets.open.weak_bg_fill = DARK_GREY; filter_chunk.visuals_mut().widgets.open.fg_stroke = Stroke::new(2.0, Color32::WHITE); filter_chunk.visuals_mut().widgets.open.bg_stroke = Stroke::new(2.0, DARK_GREY); - filter_chunk.visuals_mut().widgets.open.rounding = Rounding::same(2.0); + filter_chunk.visuals_mut().widgets.open.corner_radius = CornerRadius::same(2); filter_chunk.spacing_mut().item_spacing = egui::vec2(4.0, 4.0); @@ -555,9 +555,9 @@ fn show_game_list_buttons(app: &mut MaximaEguiApp, ui: &mut Ui) { // scrollbar ui.style_mut().visuals.widgets.inactive.bg_fill = Color32::WHITE; - ui.style_mut().visuals.widgets.inactive.rounding = Rounding::same(4.0); - ui.style_mut().visuals.widgets.active.rounding = Rounding::same(4.0); - ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(4.0); + ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::same(4); + ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::same(4); + ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::same(4); egui::ScrollArea::vertical() .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) @@ -641,7 +641,7 @@ fn show_game_list_buttons(app: &mut MaximaEguiApp, ui: &mut Ui) { (egui::TextureId::Managed(0), vec2(0.0, 0.0)), name, ) - .rounding(Rounding::same(0.0)), + .corner_radius(CornerRadius::same(0)), ); list_response .context_menu(|ui| game_list_button_context_menu(app, game, ui)); @@ -679,11 +679,11 @@ pub fn games_view(app: &mut MaximaEguiApp, ui: &mut Ui) { max: ui.available_rect_before_wrap().max, }; - ui.allocate_ui_at_rect(list_rect, |list| { - show_game_list_buttons(app, list); + ui.scope_builder(UiBuilder::new().max_rect(list_rect), |ui| { + show_game_list_buttons(app, ui); }); - ui.allocate_ui_at_rect(game_rect, |games| { - game_view_details_panel(app, games); + ui.scope_builder(UiBuilder::new().max_rect(game_rect), |ui| { + game_view_details_panel(app, ui); }); } } diff --git a/maxima-ui/src/views/settings_view.rs b/maxima-ui/src/views/settings_view.rs index 4f36b128..8d0c9b94 100644 --- a/maxima-ui/src/views/settings_view.rs +++ b/maxima-ui/src/views/settings_view.rs @@ -1,6 +1,6 @@ -use egui::{vec2, Ui}; +use egui::{Ui, vec2}; -use crate::{widgets::enum_dropdown::enum_dropdown, MaximaEguiApp}; +use crate::{MaximaEguiApp, widgets::enum_dropdown::enum_dropdown}; pub fn settings_view(app: &mut MaximaEguiApp, ui: &mut Ui) { let localization = &app.locale.localization.settings_view; diff --git a/maxima-ui/src/widgets/enum_dropdown.rs b/maxima-ui/src/widgets/enum_dropdown.rs index fc4efea7..c537b994 100644 --- a/maxima-ui/src/widgets/enum_dropdown.rs +++ b/maxima-ui/src/widgets/enum_dropdown.rs @@ -1,4 +1,4 @@ -use egui::{Color32, Rounding, Stroke}; +use egui::{Color32, CornerRadius, Rounding, Stroke}; use crate::enum_locale_map::EnumToString; use strum::IntoEnumIterator; @@ -25,34 +25,34 @@ where ui.visuals_mut().widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::WHITE); ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::new(2.0, DARK_GREY); - ui.visuals_mut().widgets.inactive.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.inactive.corner_radius = CornerRadius::same(2); ui.visuals_mut().widgets.active.bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.active.weak_bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); ui.visuals_mut().widgets.active.bg_stroke = Stroke::new(2.0, DARK_GREY); - ui.visuals_mut().widgets.active.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.active.corner_radius = CornerRadius::same(2); ui.visuals_mut().widgets.hovered.bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.hovered.weak_bg_fill = Color32::TRANSPARENT; ui.visuals_mut().widgets.hovered.fg_stroke = Stroke::new(2.0, F9B233); ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::new(2.0, F9B233); - ui.visuals_mut().widgets.hovered.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.hovered.corner_radius = CornerRadius::same(2); ui.visuals_mut().widgets.open.bg_fill = DARK_GREY; ui.visuals_mut().widgets.open.weak_bg_fill = DARK_GREY; ui.visuals_mut().widgets.open.fg_stroke = Stroke::new(2.0, Color32::WHITE); ui.visuals_mut().widgets.open.bg_stroke = Stroke::new(2.0, DARK_GREY); - ui.visuals_mut().widgets.open.rounding = Rounding::same(2.0); + ui.visuals_mut().widgets.open.corner_radius = CornerRadius::same(2); egui::ComboBox::new(id + "combo", label) .width(width) .selected_text(enum_holder.get_string(val)) .show_ui(ui, |contents| { - let rounding = Rounding::same(2.0); - contents.visuals_mut().widgets.inactive.rounding = rounding; - contents.visuals_mut().widgets.active.rounding = rounding; - contents.visuals_mut().widgets.hovered.rounding = rounding; + let corner_radius = CornerRadius::same(2); + contents.visuals_mut().widgets.inactive.corner_radius = corner_radius; + contents.visuals_mut().widgets.active.corner_radius = corner_radius; + contents.visuals_mut().widgets.hovered.corner_radius = corner_radius; contents.visuals_mut().selection.bg_fill = F9B233; contents.visuals_mut().selection.stroke = Stroke::new(2.0, Color32::BLACK); contents.visuals_mut().widgets.inactive.bg_stroke = Stroke::new(2.0, F9B233); From fb42bea9c5c7721b1612d7c6090bef70e1d76838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:06:08 +0200 Subject: [PATCH 07/57] Add chat.proto with conversation and typing messages Define messages for conversation channels and events. --- maxima-lib/src/rtm/proto/chat.proto | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 maxima-lib/src/rtm/proto/chat.proto diff --git a/maxima-lib/src/rtm/proto/chat.proto b/maxima-lib/src/rtm/proto/chat.proto new file mode 100644 index 00000000..b3d75f47 --- /dev/null +++ b/maxima-lib/src/rtm/proto/chat.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +package eadp.rtm; + +// incomplete, wip + +message ConversationChannelV1 { + string channel_id = 1; // e.g. "--CrossGame" +} + +message ConversationHistoryRequestV1 { + string channel_id = 1; + string timestamp = 2; + int32 limit = 3; + string message_type = 4; // "TEXT_MESSAGE" +} + +message TypingEventV1 { + string channel_id = 1; + string timestamp = 2; +} + +message PlayerContextV1 { + PlayerRef player = 2; + int32 unknown3 = 3; +} + +message PlayerRef { + string player_id = 1; + string product_id = 2; // "origin" +} From 3ab2b3d8a4765e8c1d2fc2bbb2a6959011bb365c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:08:15 +0200 Subject: [PATCH 08/57] Add new fields to rtm.proto for player context and events --- maxima-lib/src/rtm/proto/rtm.proto | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/maxima-lib/src/rtm/proto/rtm.proto b/maxima-lib/src/rtm/proto/rtm.proto index 692b9e4e..29074e6f 100644 --- a/maxima-lib/src/rtm/proto/rtm.proto +++ b/maxima-lib/src/rtm/proto/rtm.proto @@ -16,10 +16,15 @@ message CommunicationV1 { PresenceSubscribeV1 presenceSubscribe = 2; PresenceUpdateV1 presenceUpdate = 5; PresenceV1 presence = 7; + PlayerContextV1 playerContext = 9; + ConversationChannelV1 conversationChannel = 11; + ConversationHistoryRequestV1 historyRequest = 12; // outer msg, not oneof ErrorV1 error = 13; ReconnectRequestV1 reconnectRequest = 14; SuccessV1 success = 15; HeartbeatV1 heartbeat = 20; + TypingEventV1 typingEvent = 43; + string unknown44 = 44; // unsure yet LoginRequestV3 loginRequestV3 = 46; SessionCleanupV1 sessionCleanupV1 = 49; } From 2b80a86da8fcce2d21752c75bda07fb7901488d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:09:46 +0200 Subject: [PATCH 09/57] Update rtm.proto --- maxima-lib/src/rtm/proto/rtm.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/maxima-lib/src/rtm/proto/rtm.proto b/maxima-lib/src/rtm/proto/rtm.proto index 29074e6f..b798fdab 100644 --- a/maxima-lib/src/rtm/proto/rtm.proto +++ b/maxima-lib/src/rtm/proto/rtm.proto @@ -4,6 +4,7 @@ package eadp.rtm; import "common.proto"; import "requests.proto"; import "responses.proto"; +import "chat.proto"; message Communication { CommunicationV1 v1 = 1; From 871cf578eeeff648401ec6d7de7e543a2b4bbbe7 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:01:42 +0200 Subject: [PATCH 10/57] rewrite w/ tokio async --- maxima-cli/Cargo.toml | 4 +- maxima-lib/Cargo.toml | 17 +- maxima-lib/src/content/downloader.rs | 6 +- maxima-lib/src/content/manager.rs | 28 +- maxima-lib/src/content/zip.rs | 8 +- maxima-lib/src/core/auth/context.rs | 2 +- maxima-lib/src/core/auth/hardware.rs | 12 +- maxima-lib/src/core/auth/login.rs | 2 +- maxima-lib/src/core/auth/mod.rs | 4 +- maxima-lib/src/core/cache.rs | 5 +- maxima-lib/src/core/cloudsync.rs | 44 ++- maxima-lib/src/core/ecommerce.rs | 2 +- maxima-lib/src/core/launch.rs | 15 +- maxima-lib/src/core/library.rs | 8 +- maxima-lib/src/core/manifest/dip.rs | 11 +- maxima-lib/src/core/manifest/mod.rs | 14 +- maxima-lib/src/core/manifest/pre_dip.rs | 48 +++- maxima-lib/src/core/mod.rs | 52 ++-- maxima-lib/src/core/service_layer.rs | 40 +-- maxima-lib/src/lsx/connection.rs | 46 ++-- maxima-lib/src/lsx/request/account.rs | 6 +- maxima-lib/src/lsx/request/auth.rs | 2 +- maxima-lib/src/lsx/request/challenge.rs | 4 +- maxima-lib/src/lsx/request/config.rs | 2 +- maxima-lib/src/lsx/request/core.rs | 4 +- maxima-lib/src/lsx/request/game.rs | 65 ++++- maxima-lib/src/lsx/request/offer.rs | 10 +- maxima-lib/src/lsx/request/profile.rs | 39 ++- maxima-lib/src/lsx/service.rs | 14 +- maxima-lib/src/ooa/mod.rs | 18 +- maxima-lib/src/rtm/client.rs | 41 ++- maxima-lib/src/rtm/connection.rs | 11 +- maxima-lib/src/util/registry.rs | 4 +- maxima-lib/src/util/simple_crypto.rs | 8 +- maxima-service/Cargo.toml | 31 +-- maxima-service/src/service/error.rs | 3 +- maxima-tui/Cargo.toml | 4 +- maxima-tui/src/service.rs | 136 +++++---- maxima-ui/Cargo.toml | 14 +- maxima-ui/src/bridge/game_details.rs | 8 +- maxima-ui/src/bridge/get_friends.rs | 10 +- maxima-ui/src/bridge/get_games.rs | 64 ++--- maxima-ui/src/bridge/login_oauth.rs | 6 +- maxima-ui/src/bridge_processor.rs | 263 +++++++++--------- maxima-ui/src/bridge_thread.rs | 351 ++++++++++++------------ maxima-ui/src/event_thread.rs | 87 +++--- maxima-ui/src/main.rs | 46 ++-- maxima-ui/src/ui_image.rs | 21 +- maxima-ui/src/views/game_view.rs | 2 +- maxima-ui/src/widgets/enum_dropdown.rs | 2 +- 50 files changed, 851 insertions(+), 793 deletions(-) diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index 1a6bdf61..1e74312f 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -13,8 +13,8 @@ serde_json = "1.0.150" anyhow = "1.0.102" lazy_static = "1.5.0" base64 = "0.22.1" -log = "0.4.30" -regex = "1.12.3" +log = "0.4.32" +regex = "1.12.4" inquire = "0.9.4" futures = "0.3.32" diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index abf028b1..3dcb79fd 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -27,13 +27,13 @@ quick-xml = { version = "0.40.1", features = ["serialize"] } pastey = "0.2.3" hex = "0.4.3" lazy_static = "1.5.0" -reqwest = { version = "0.13.4", features = ["stream", "form", "rustls", "gzip", "json"], default-features = false } +reqwest = { version = "0.13.4", features = ["stream", "form", "rustls", "gzip", "json", "query"], default-features = false } base64 = "0.22.1" base62 = "2.2.4" -log = "0.4.30" +log = "0.4.32" enable-ansi-support = "0.3.1" -chrono = "0.4.44" -regex = "1.12.3" +chrono = "0.4.45" +regex = "1.12.4" directories = "6.0.0" open = "5.3.5" querystring = "1.1.0" @@ -58,7 +58,7 @@ prost = "0.14.4" encoding_rs = "0.8.35" async-compression = { version = "=0.4.42", features = ["tokio", "deflate"] } tokio-util = { version = "0.7.18", features = ["io", "compat"] } -uuid = "1.23.2" +uuid = "1.23.3" tokio-rustls = "0.26.4" rustls = "0.23.40" webpki-roots = "1.0.7" @@ -75,7 +75,7 @@ pelite = "0.10.0" gethostname = "1.1.0" thiserror = "2.0.18" url = "2.5.8" -http = "1.4.1" +http = "1.4.2" generic-array = "1.4.3" rustls-pki-types = "1.14.1" @@ -87,6 +87,7 @@ winapi = { version = "0.3.9", features = [ "synchapi", "wincon", "consoleapi", + "libloaderapi", "shellapi", "psapi", "sddl", @@ -95,6 +96,8 @@ winapi = { version = "0.3.9", features = [ "iphlpapi", "tcpmib", "winsvc", + "winbase", + "winreg", "impl-default" ] } winreg = "0.56.0" @@ -107,7 +110,7 @@ wmi = "0.18.4" smbios-lib = "0.9.2" [build-dependencies] -prost-build = "0.14.3" +prost-build = "0.14.4" [lib] name = "maxima" diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index d0734c12..050c2d6f 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -168,6 +168,7 @@ impl<'a> AsyncWriterWrapper<'a> { zlib_state_file: std::fs::OpenOptions::new() .write(true) .create(true) + .truncate(true) .open(zstate_path(&id, &path)?)?, decoder, inner, @@ -344,7 +345,7 @@ impl<'a> EntryDownloadRequest<'a> { /// End is not inclusive pub async fn download_range(&mut self, start: i64, end: i64) -> Result<(), DownloaderError> { let offset = self.entry.data_offset(); - let range = format!("bytes={}-{}", offset + start as i64, offset + end - 1); + let range = format!("bytes={}-{}", offset + start, offset + end - 1); let data = match self .client @@ -509,6 +510,7 @@ impl ZipDownloader { let file = OpenOptions::new() .write(true) .create(true) + .truncate(false) .open(&file_path) .await?; @@ -538,7 +540,7 @@ impl ZipDownloader { }; if state == EntryDownloadState::Resumable { - let state_file = zstate_path(&self.id, &entry.name())?; + let state_file = zstate_path(&self.id, entry.name())?; if state_file.exists() { let mut buf = Bytes::from(tokio::fs::read(state_file).await?); decoder.restore_state(&mut buf); diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f77a5253..329f8ba7 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -20,7 +20,7 @@ use crate::{ content::{ ContentService, downloader::{DownloadError, ZipDownloader}, - zip::{self, CompressionType, ZipError, ZipFileEntry}, + zip::{CompressionType, ZipError, ZipFileEntry}, }, core::{ MaximaEvent, @@ -146,7 +146,7 @@ impl GameDownloader { debug!("URL: {}", url.url()); - let downloader = ZipDownloader::new(&game.offer_id, &url.url(), &game.path).await?; + let downloader = ZipDownloader::new(&game.offer_id, url.url(), &game.path).await?; let mut entries = Vec::new(); for ele in downloader.manifest().entries() { @@ -222,9 +222,9 @@ impl GameDownloader { ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(total_count); - for i in 0..total_count { + for ele in entries.iter().take(total_count) { let downloader = downloader_arc.clone(); - let ele = entries[i].clone(); + let ele = ele.clone(); let cancel_token = cancel_token.clone(); let completed_bytes = completed_bytes.clone(); @@ -316,7 +316,7 @@ impl ContentManager { } pub async fn add_install(&mut self, game: QueuedGame) -> Result<(), ContentManagerError> { - if self.queue.queued.is_empty() && self.queue.current == None && self.current.is_none() { + if self.queue.queued.is_empty() && self.queue.current.is_none() && self.current.is_none() { self.install_now(game).await?; } else { self.queue.queued.push(game); @@ -362,18 +362,16 @@ impl ContentManager { pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { let mut event = None; - if let Some(current) = &self.current { - if current.is_done() { - event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - self.current = None; - self.queue.current = None; - - if let Some(game) = self.queue.queued.pop() { - self.install_now(game).await?; - } + if let Some(current) = &self.current && current.is_done() { + event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); + self.current = None; + self.queue.current = None; - self.queue.save().await?; + if let Some(game) = self.queue.queued.pop() { + self.install_now(game).await?; } + + self.queue.save().await?; } Ok(event) diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index 33dacf66..7e14fcfd 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -248,7 +248,7 @@ impl EndOfCentralDirectory { } let signature = data.read_u32()?; - if signature as u32 != ZIP_EOCD_SIGNATURE { + if signature != ZIP_EOCD_SIGNATURE { return Err(EOCDError::Signature(signature)); } @@ -284,12 +284,12 @@ impl EndOfCentralDirectory { data.read_u16()?; data.read_u16()?; - self.disk_number = data.read_u32()? as u32; - self.disk_number_with_cd = data.read_u32()? as u32; + self.disk_number = data.read_u32()?; + self.disk_number_with_cd = data.read_u32()?; self.disk_entries = data.read_i64()? as u64; self.total_entries = data.read_i64()? as u64; self.cd_size = data.read_i64()? as u64; - self.cd_offset = data.read_i64()? as i64; + self.cd_offset = data.read_i64()?; self.comment_length = 0; Ok(()) diff --git a/maxima-lib/src/core/auth/context.rs b/maxima-lib/src/core/auth/context.rs index b488976e..b9ce0e3a 100644 --- a/maxima-lib/src/core/auth/context.rs +++ b/maxima-lib/src/core/auth/context.rs @@ -42,7 +42,7 @@ impl AuthContext<'_> { fn generate_code_verifier() -> String { let rand_bytes: [u8; 32] = random(); - URL_SAFE_NO_PAD.encode(&rand_bytes) + URL_SAFE_NO_PAD.encode(rand_bytes) } fn generate_challenge(code_verifier: &String) -> String { diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index fad63a2e..21007ee7 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -92,34 +92,34 @@ impl HardwareInfo { let mut board_manufacturer = "Microsoft Corporation"; let mut board_sn = "None"; - if let Some(board_info) = board_data.get(0) { + if let Some(board_info) = board_data.first() { board_manufacturer = board_info.manufacturer.as_str(); board_sn = board_info.serial_number.as_str(); } let mut bios_manufacturer = "Microsoft Corporation"; let mut bios_sn = "None"; - if let Some(bios_info) = bios_data.get(0) { + if let Some(bios_info) = bios_data.first() { bios_manufacturer = bios_info.manufacturer.as_str(); bios_sn = bios_info.serial_number.as_str(); } let mut os_install_date = "1970-01-0100:00:00.000000000+0000"; let mut os_sn = "None"; - if let Some(os_info) = os_data.get(0) { + if let Some(os_info) = os_data.first() { os_install_date = os_info.install_date.as_str(); os_sn = os_info.serial_number.as_str(); } let mut disk_sn = "None"; - if let Some(disk_info) = disk_data.get(0) { + if let Some(disk_info) = disk_data.first() { disk_sn = disk_info.serial_number.as_str(); } let volume_sn = format!("{:08x}", get_c_drive_volume_serial()); let mut gpu_pnp_id: Option = None; - if let Some(gpu_info) = gpu_data.get(0) { + if let Some(gpu_info) = gpu_data.first() { let device_id = &gpu_info.pnp_device_id; let device_id = device_id[..device_id.rfind('\\').unwrap_or(device_id.len())].to_string(); @@ -588,7 +588,7 @@ fn get_ea_mac_address() -> Option { match mac { Some(address) => { - let mac = hex::encode(&address.bytes()); + let mac = hex::encode(address.bytes()); Some("$".to_owned() + &mac) } None => None, diff --git a/maxima-lib/src/core/auth/login.rs b/maxima-lib/src/core/auth/login.rs index 8a7209f4..39354d18 100644 --- a/maxima-lib/src/core/auth/login.rs +++ b/maxima-lib/src/core/auth/login.rs @@ -45,7 +45,7 @@ pub async fn begin_oauth_login_flow<'a>(context: &mut AuthContext<'a>) -> Result } } - return Err(AuthError::NoAuthCode.into()); + return Err(AuthError::NoAuthCode); } } } diff --git a/maxima-lib/src/core/auth/mod.rs b/maxima-lib/src/core/auth/mod.rs index f8f61abf..b812061d 100644 --- a/maxima-lib/src/core/auth/mod.rs +++ b/maxima-lib/src/core/auth/mod.rs @@ -104,8 +104,8 @@ pub async fn nucleus_token_exchange( let query = vec![ ("grant_type", "authorization_code"), - ("code", &auth_context.code().ok_or(TokenError::Absent)?), - ("code_verifier", &auth_context.code_verifier()), + ("code", auth_context.code().ok_or(TokenError::Absent)?), + ("code_verifier", auth_context.code_verifier()), ("client_id", JUNO_PC_CLIENT_ID), ("client_secret", JUNO_PC_CLIENT_SECRET), ("redirect_uri", "qrc:///html/login_successful.html"), diff --git a/maxima-lib/src/core/cache.rs b/maxima-lib/src/core/cache.rs index b8c290dd..8cd79bfb 100644 --- a/maxima-lib/src/core/cache.rs +++ b/maxima-lib/src/core/cache.rs @@ -31,9 +31,6 @@ impl DynamicCache { Q: Hash + Eq + ?Sized, T: Sync + Send + Clone + 'static, { - match self.cache.get(key) { - None => None, - Some(cached) => Some((*cached.downcast::().unwrap()).clone()), - } + self.cache.get(key).map(|cached| (*cached.downcast::().unwrap()).clone()) } } diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index b76d5d68..01f235fd 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -1,7 +1,7 @@ /// Cloud sync is a process and a half. Here's a high-level overview of it: /// # Reading (pre-launch) /// - Call `/lock/read`, which gives us a link to an XML on S3: -/// a manifest of all files in the cloud, along with their MD5 hash and last modified date +/// a manifest of all files in the cloud, along with their MD5 hash and last modified date /// - If you feel the need to update the local files: /// - Call `/lock/authorize` with a `Vec` containing the corresponding `href` from the manifest /// - This will return an XML with S3 targets corresponding to the files. Download them (the response body is the file, verbatim) and write them. @@ -212,11 +212,7 @@ impl<'a> CloudSyncLock<'a> { let manifest: CloudSyncManifest = { let manifest = if let Ok(text) = res.text().await { let result = quick_xml::de::from_str(&text); - if let Ok(manifest) = result { - manifest - } else { - None - } + result.unwrap_or_default() } else { None }; @@ -261,10 +257,10 @@ impl<'a> CloudSyncLock<'a> { /// The file syncing functions are some real hastily written code at the moment. /// Lots of stuff could be better and merged between them. TODO: Clean it up. pub async fn sync_files(&self) -> Result<(), CloudSyncError> { - Ok(match self.mode { + match self.mode { CloudSyncLockMode::Read => self.sync_read_files().await, CloudSyncLockMode::Write => self.sync_write_files().await, - }?) + } } async fn sync_read_files(&self) -> Result<(), CloudSyncError> { @@ -279,7 +275,7 @@ impl<'a> CloudSyncLock<'a> { let _md5 = if let Ok(file) = file { let md5 = calc_file_md5(file, HashMode::Hex).await?; - if let Some(_) = self.manifest.file_by_md5(&md5) { + if self.manifest.file_by_md5(&md5).is_some() { debug!("Skipping CloudSync read {}", &path.display()); continue; } @@ -347,6 +343,7 @@ impl<'a> CloudSyncLock<'a> { tokio::fs::create_dir_all(path.safe_parent()?).await?; let mut file = OpenOptions::new() .write(true) + .truncate(true) .create(true) .open(path) .await?; @@ -405,13 +402,13 @@ impl<'a> CloudSyncLock<'a> { let md5 = calc_file_md5(file.try_clone().await?, HashMode::Hex).await?; let base62 = calc_file_md5(file.try_clone().await?, HashMode::Base62).await?; - if let Some(file) = self.manifest.file_by_md5(&md5) { + if let Some(existing) = self.manifest.file_by_md5(&md5) { debug!("Skipping CloudSync write {}", &path.display()); - skipped.push(file); + skipped.push(existing.clone()); continue; } - let name = unsubstitute_paths(&path)?; + let name = unsubstitute_paths(path)?; let write_data = WriteData::File { name, file, @@ -444,10 +441,10 @@ impl<'a> CloudSyncLock<'a> { manifest.file.clear(); for ele in skipped { - manifest.file.push(ele.clone()); + manifest.file.push(ele); } - for (_, write_data) in &data { + for write_data in data.values() { if let WriteData::File { name, file, base62, .. } = write_data @@ -496,7 +493,7 @@ impl<'a> CloudSyncLock<'a> { for i in 0..authorizations.request.len() { let auth_req = &authorizations.request[i]; let mut req = self.client.put(&auth_req.url); - for header in &auth_req.headers.header.clone().unwrap_or(Vec::new()) { + for header in &auth_req.headers.header.clone().unwrap_or_default() { req = req.header(&header.attr_key, &header.attr_value); } @@ -552,11 +549,11 @@ impl CloudSyncClient { } } - pub async fn obtain_lock<'a>( + pub async fn obtain_lock( &self, offer: &OwnedOffer, mode: CloudSyncLockMode, - ) -> Result { + ) -> Result, CloudSyncError> { let id = format!( "{}_{}", offer.offer().primary_master_title_id(), @@ -582,15 +579,15 @@ impl CloudSyncClient { return Err(CloudSyncError::NoConfig(offer.offer_id().clone())); } - Ok(self.obtain_lock_raw(&id, mode, allowed_files).await?) + self.obtain_lock_raw(&id, mode, allowed_files).await } - pub async fn obtain_lock_raw<'a>( + pub async fn obtain_lock_raw( &self, id: &str, mode: CloudSyncLockMode, allowed_files: Vec, - ) -> Result { + ) -> Result, CloudSyncError> { let (token, user_id) = acquire_auth(&self.auth).await?; let res = self @@ -610,15 +607,14 @@ impl CloudSyncClient { let text = res.text().await?; let sync: CloudSyncSync = quick_xml::de::from_str(&text)?; - Ok(CloudSyncLock::new( + CloudSyncLock::new( &self.auth, &self.client, sync.manifest, lock, mode, allowed_files, - ) - .await?) + ).await } } @@ -743,7 +739,7 @@ impl CloudSyncManifest { continue; } - return Some(&ele); + return Some(ele); } } diff --git a/maxima-lib/src/core/ecommerce.rs b/maxima-lib/src/core/ecommerce.rs index bf61a1b6..217e6ca4 100644 --- a/maxima-lib/src/core/ecommerce.rs +++ b/maxima-lib/src/core/ecommerce.rs @@ -50,7 +50,7 @@ pub async fn request_offer_data( locale: &str, ) -> Result { let res = Client::new() - .get(&format!("{}/public/{}/{}", API_ECOMMERCE, offer, locale)) + .get(format!("{}/public/{}/{}", API_ECOMMERCE, offer, locale)) .header("AuthToken", access_token) .send() .await?; diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index c58435ae..b7cf5ff0 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -100,10 +100,7 @@ pub enum LaunchMode { impl LaunchMode { // What an awful name pub fn is_online_offline(&self) -> bool { - match self { - LaunchMode::OnlineOffline(_, _, _) => true, - _ => false, - } + matches!(self, LaunchMode::OnlineOffline(_, _, _)) } } @@ -121,6 +118,10 @@ pub struct ActiveGameContext { } impl ActiveGameContext { + pub fn current_offer(&self) -> Option { + self.offer.clone() + } + pub fn new( launch_id: &str, game_path: &str, @@ -311,14 +312,14 @@ pub async fn start_game( child .current_dir(PathBuf::from(path).safe_parent()?) - .env("MXLaunchId", launch_id.to_owned()) + .env("MXLaunchId", &launch_id) .env("EAAuthCode", "unavailable") .env("EAEgsProxyIpcPort", "0") .env("EAEntitlementSource", "EA") .env("EAExternalSource", "EA") .env("EAFreeTrialGame", "false") .env("EAGameLocale", maxima.locale.full_str()) - .env("EAGenericAuthToken", access_token.to_owned()) + .env("EAGenericAuthToken", &access_token) .env("EALaunchCode", "unavailable") .env("EALaunchOwner", "EA") .env( @@ -378,7 +379,7 @@ pub async fn start_game( async fn request_opaque_ooa_token(access_token: &str) -> Result { let mut context = AuthContext::new()?; - context.set_access_token(&access_token); + context.set_access_token(access_token); context.set_token_format("OPAQUE"); context.set_expires_in(550); diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index dcee61af..f6777d4a 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -75,7 +75,7 @@ impl OwnedOffer { pub async fn install_check_path(&self) -> Result { Ok(parse_registry_path( - &self + self .offer .install_check_override() .as_ref() @@ -137,7 +137,7 @@ impl OwnedOffer { } else { let path = PathBuf::from( parse_partial_registry_path( - &self + self .offer .install_check_override() .as_ref() @@ -258,7 +258,7 @@ impl OwnedTitle { } pub fn base_game(&self) -> Option { - for offer in self.offers.iter() { + if let Some(offer) = self.offers.first() { if offer .product .product() @@ -423,7 +423,7 @@ impl GameLibrary { } let mut titles = group_offers(offers); - titles.sort_by(|a, b| a.name().to_lowercase().cmp(&b.name().to_lowercase())); + titles.sort_by_key(|a| a.name().to_lowercase()); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index dedbf79c..2761ee27 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::{core::manifest::ManifestError, util::native::platform_path}; use derive_getters::Getters; @@ -162,7 +162,6 @@ fn bytes_to_string(bytes: Vec) -> Option { let u16_bytes: Vec = bytes .chunks_exact(2) - .into_iter() .map(|a| u16::from_ne_bytes([a[0], a[1]])) .collect(); @@ -191,7 +190,7 @@ impl DiPManifest { } #[cfg(unix)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { use crate::{ core::launch::mx_linux_setup, unix::{ @@ -215,12 +214,12 @@ impl DiPManifest { } #[cfg(windows)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { use crate::util::native::NativeError; use tokio::process::Command; let args = self.collect_touchup_args(install_path)?; - let path = install_path.join(&self.touchup.path()); + let path = install_path.join(self.touchup.path()); let mut binding = Command::new(path); let child = binding.args(args); @@ -235,7 +234,7 @@ impl DiPManifest { Ok(()) } - fn collect_touchup_args(&self, install_path: &PathBuf) -> Result, ManifestError> { + fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { let mut args = Vec::new(); for arg in self.touchup.parameters.split(" ") { let arg = arg.replace("{locale}", "en_US").replace( diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index 9acf730c..dbfa2be1 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -4,7 +4,7 @@ pub mod pre_dip; use dip::DiPManifest; use pre_dip::PreDiPManifest; use quick_xml::DeError; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use thiserror::Error; #[derive(Error, Debug)] @@ -35,14 +35,15 @@ 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: &Path) -> 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: &Path) -> Result<(), ManifestError> { + let install_path = install_path.to_path_buf(); + self.run_touchup(&install_path).await } fn execute_path(&self, trial: bool) -> Option { @@ -56,8 +57,9 @@ 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: &Path) -> Result<(), ManifestError> { + let install_path = install_path.to_path_buf(); + self.run_touchup(&install_path).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 04070110..43ac871b 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -3,7 +3,7 @@ use crate::{core::manifest::ManifestError, util::native::platform_path}; use derive_getters::Getters; use serde::Deserialize; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; macro_rules! predip_type { ( @@ -48,6 +48,35 @@ predip_type!( data { file_path: String, parameters: String, + update_parameters: String, + repair_parameters: String, + } +); + +predip_type!( + Metadata; + attr {}, + data { + feature_flags: Vec, + os: String, + ignore: Vec, + locale_info: Vec, + file_path: String, + parameters: String, + update_parameters: String, + repair_parameters: String, + } +); + +predip_type!( + LocaleInfo; + attr { + locale: String, + }, + data { + title: String, + eula: String, + exclude: Vec, } ); @@ -66,11 +95,15 @@ fn remove_trailing_backslash(path: &str) -> &str { predip_type!( Manifest; attr { - gameVersion: String, - manifestVersion: String, + game_version: String, + manifest_version: String, }, data { + content_ids: Vec, + uninstall: String, + metadata: Vec, executable: PreDiPExecutable, + install_manifest: String, } ); @@ -82,7 +115,6 @@ fn bytes_to_string(bytes: Vec) -> Option { let u16_bytes: Vec = bytes .chunks_exact(2) - .into_iter() .map(|a| u16::from_ne_bytes([a[0], a[1]])) .collect(); @@ -102,11 +134,11 @@ impl PreDiPManifest { } pub fn version(&self) -> Option { - Some(self.attr_gameVersion.clone()) + Some(self.attr_game_version.clone()) } #[cfg(unix)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { use crate::{ core::launch::mx_linux_setup, unix::{ @@ -131,7 +163,7 @@ impl PreDiPManifest { } #[cfg(windows)] - pub async fn run_touchup(&self, install_path: &PathBuf) -> Result<(), ManifestError> { + pub async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { use crate::util::native::NativeError; use tokio::process::Command; @@ -151,7 +183,7 @@ impl PreDiPManifest { Ok(()) } - fn collect_touchup_args(&self, install_path: &PathBuf) -> Result, ManifestError> { + fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { let mut args = Vec::new(); for arg in self.executable.parameters.split(" ") { let arg = arg.replace("{locale}", "en_US").replace( diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 4039629d..fe5f1a1a 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -294,7 +294,7 @@ impl Maxima { let friends: Vec = friends .friends() .items() - .into_iter() + .iter() .map(|x| x.player().clone()) .collect(); @@ -313,8 +313,8 @@ impl Maxima { } pub async fn player_by_id(&self, id: &str) -> Result { - if let Some(user) = &self.dummy_local_user { - return Ok(user + if let Some(dummy) = &self.dummy_local_user { + return Ok(dummy .player() .as_ref() .ok_or(ServiceLayerError::MissingField)? @@ -340,9 +340,9 @@ impl Maxima { let avatars = data.avatar(); let avatars = avatars.as_ref().ok_or(ServiceLayerError::MissingField)?; - let _ = self.cache_avatar_image(&id, avatars.large()).await; - let _ = self.cache_avatar_image(&id, avatars.medium()).await; - let _ = self.cache_avatar_image(&id, avatars.small()).await; + let _ = self.cache_avatar_image(id, avatars.large()).await; + let _ = self.cache_avatar_image(id, avatars.medium()).await; + let _ = self.cache_avatar_image(id, avatars.small()).await; self.request_cache.insert(cache_key, data.clone()); Ok(data) @@ -383,7 +383,7 @@ impl Maxima { self.player_by_id(id).await?; } - if let Some(_) = &self.dummy_local_user { + if self.dummy_local_user.is_some() { return Ok(path); } @@ -431,10 +431,7 @@ impl Maxima { } pub fn set_player_started(&mut self) { - match &mut self.playing { - Some(playing) => playing.set_started(), - None => return, - } + if let Some(playing) = &mut self.playing { playing.set_started() } } /// Call this as often as possible from the loop you consume events from @@ -458,29 +455,24 @@ impl Maxima { } let playing = self.playing.as_mut().unwrap(); - match playing.process_mut().try_wait() { - Ok(None) => return, - _ => (), - } + if let Ok(None) = playing.process_mut().try_wait() { return } info!("Game stopped"); - if let Some(offer) = playing.offer() { - if *playing.cloud_saves() && offer.offer().has_cloud_save() { - let result = self - .cloud_sync - .obtain_lock(offer, CloudSyncLockMode::Write) - .await; - match result { - Err(err) => error!("Failed to obtain CloudSync write lock: {}", err), - Ok(lock) => { - let result = lock.sync_files().await; - if let Err(err) = result { - error!("Failed to write to CloudSync: {}", err); - } - - lock.release().await.ok(); + if let Some(offer) = playing.offer() && *playing.cloud_saves() && offer.offer().has_cloud_save() { + let result = self + .cloud_sync + .obtain_lock(offer, CloudSyncLockMode::Write) + .await; + match result { + Err(err) => error!("Failed to obtain CloudSync write lock: {}", err), + Ok(lock) => { + let result = lock.sync_files().await; + if let Err(err) = result { + error!("Failed to write to CloudSync: {}", err); } + + lock.release().await.ok(); } } } diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index c75f06b0..1754277a 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -5,6 +5,7 @@ use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; use sha2_const; +use std::fmt; use thiserror::Error; use derive_builder::Builder; @@ -98,9 +99,9 @@ pub struct ServiceLayerGraphQLRequest { macro_rules! load_graphql_request { ($type:ident, $operation:expr, $key:expr) => {{ let content = include_str!(concat!("graphql/", $operation, ".gql")); - let hash = sha2_const::Sha256::new(); - hash.update(content.as_bytes()); - let hash = hash.finalize(); + let hash = sha2_const::Sha256::new() + .update(content.as_bytes()) + .finalize(); ServiceLayerGraphQLRequest { query: content, operation: $operation, @@ -250,11 +251,7 @@ impl ServiceLayerClient { return Err(ServiceLayerError::GraphQL { operation: operation.operation.to_string(), - error: if let Some(error) = error.get("message") { - Some(error.to_string()) - } else { - None - }, + error: error.get("message").map(|error| error.to_string()) }); } @@ -507,12 +504,12 @@ service_layer_enum!(DownloadType, { None, }); -impl ServiceDownloadType { - pub fn to_string(&self) -> String { +impl fmt::Display for ServiceDownloadType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ServiceDownloadType::Staged => "Staged".to_owned(), - ServiceDownloadType::Live => "Live".to_owned(), - ServiceDownloadType::None => "None".to_owned(), + ServiceDownloadType::Staged => write!(f, "Staged"), + ServiceDownloadType::Live => write!(f, "Live"), + ServiceDownloadType::None => write!(f, "None"), } } } @@ -526,18 +523,25 @@ service_layer_type!(AvailableBuild, { }); impl ServiceAvailableBuild { - pub fn to_string(&self) -> String { - let mut str = self.game_version.to_owned().unwrap_or("UnkVer".to_owned()); +} + +impl fmt::Display for ServiceAvailableBuild { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + self.game_version.as_deref().unwrap_or("UnkVer") + )?; if let Some(download_type) = &self.download_type { - str += &("/".to_owned() + &*download_type.to_string()); + write!(f, "/{download_type}")?; } if let Some(build_live_date) = &self.build_live_date { - str += &(" - ".to_owned() + build_live_date); + write!(f, " - {build_live_date}")?; } - str + Ok(()) } } diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 033cb131..b35c097f 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -222,29 +222,27 @@ impl Connection { // The PID system is mainly for Kyber injection let mut pid = get_os_pid(context); - if cfg!(unix) { - if let Ok(os_pid) = pid { - let sys = System::new_all(); - if let Some(process) = sys.process(Pid::from_u32(os_pid)) { - let filename = PathBuf::from( - process.cmd()[0] - .to_string_lossy() - .replace("Z:", "") - .replace('\\', "/"), - ) - .file_name() - .ok_or(NativeError::FileName)? - .to_str() - .ok_or(NativeError::Stringify)? - .to_owned(); - - pid = get_wine_pid(&context.launch_id(), &filename).await; - } else { - warn!( - "Failed to find game process while looking for PID {}", - os_pid - ); - } + if cfg!(unix) && let Ok(os_pid) = pid { + let sys = System::new_all(); + if let Some(process) = sys.process(Pid::from_u32(os_pid)) { + let filename = PathBuf::from( + process.cmd()[0] + .to_string_lossy() + .replace("Z:", "") + .replace('\\', "/"), + ) + .file_name() + .ok_or(NativeError::FileName)? + .to_str() + .ok_or(NativeError::Stringify)? + .to_owned(); + + pid = get_wine_pid(context.launch_id(), &filename).await; + } else { + warn!( + "Failed to find game process while looking for PID {}", + os_pid + ); } } @@ -295,7 +293,7 @@ impl Connection { let mut buffer = [0; 1024 * 8]; let n = match self.stream.read(&mut buffer) { - Ok(n) if n == 0 => { + Ok(0) => { return Err(LSXConnectionError::Closed); } Ok(n) => n, diff --git a/maxima-lib/src/lsx/request/account.rs b/maxima-lib/src/lsx/request/account.rs index 6d5b8658..886f47f1 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -46,8 +46,8 @@ pub async fn handle_query_entitlements_request( lsx_entitlements.push(LSXEntitlement { attr_LastModifiedDate: "0000-00-00T00:00:00".to_string(), // it's like this in EAD too attr_EntitlementId: entitlement.id().parse::()?, - attr_UseCount: entitlement.use_count().clone(), - attr_Version: entitlement.version().clone(), + attr_UseCount: *entitlement.use_count(), + attr_Version: *entitlement.version(), attr_ItemId: entitlement.product_id().clone(), attr_ResourceId: String::new(), attr_GrantDate: entitlement.grant_date().to_string(), @@ -62,5 +62,5 @@ pub async fn handle_query_entitlements_request( }); } - make_lsx_handler_response!(Response, QueryEntitlementsResponse, { entitlement: lsx_entitlements }) + return make_lsx_handler_response!(Response, QueryEntitlementsResponse, { entitlement: lsx_entitlements }) } diff --git a/maxima-lib/src/lsx/request/auth.rs b/maxima-lib/src/lsx/request/auth.rs index ff1fd392..029ec832 100644 --- a/maxima-lib/src/lsx/request/auth.rs +++ b/maxima-lib/src/lsx/request/auth.rs @@ -35,5 +35,5 @@ pub async fn handle_auth_code_request( } }; - make_lsx_handler_response!(Response, AuthCode, { attr_value: auth_code }) + return make_lsx_handler_response!(Response, AuthCode, { attr_value: auth_code }) } diff --git a/maxima-lib/src/lsx/request/challenge.rs b/maxima-lib/src/lsx/request/challenge.rs index 2ad92fc5..21c3f739 100644 --- a/maxima-lib/src/lsx/request/challenge.rs +++ b/maxima-lib/src/lsx/request/challenge.rs @@ -14,7 +14,7 @@ pub async fn handle_challenge_response( state: LockedConnectionState, message: LSXChallengeResponse, ) -> Result, LSXRequestError> { - let valid = check_challenge_response(&message.attr_response, &state.read().await.challenge()); + let valid = check_challenge_response(&message.attr_response, state.read().await.challenge()); if !valid { return Err(LSXRequestError::InvalidChallengeResponse); } @@ -40,5 +40,5 @@ pub async fn handle_challenge_response( hex::encode(encryption_key), message.attr_version ); - make_lsx_handler_response!(Response, ChallengeAccepted, { attr_response: accept_key }) + return make_lsx_handler_response!(Response, ChallengeAccepted, { attr_response: accept_key }) } diff --git a/maxima-lib/src/lsx/request/config.rs b/maxima-lib/src/lsx/request/config.rs index b7d9134c..dc893de0 100644 --- a/maxima-lib/src/lsx/request/config.rs +++ b/maxima-lib/src/lsx/request/config.rs @@ -57,5 +57,5 @@ pub async fn handle_config_request( services.push(service.clone()); } - make_lsx_handler_response!(Response, GetConfigResponse, { service: services }) + return make_lsx_handler_response!(Response, GetConfigResponse, { service: services }) } diff --git a/maxima-lib/src/lsx/request/core.rs b/maxima-lib/src/lsx/request/core.rs index 21591c71..818f382c 100644 --- a/maxima-lib/src/lsx/request/core.rs +++ b/maxima-lib/src/lsx/request/core.rs @@ -21,7 +21,7 @@ pub async fn handle_settings_request( MaximaSetting::Environment => "production".to_string(), }; - make_lsx_handler_response!(Response, GetSettingResponse, { attr_Setting: setting }) + return make_lsx_handler_response!(Response, GetSettingResponse, { attr_Setting: setting }) } pub async fn handle_connectivity_request( @@ -29,7 +29,7 @@ pub async fn handle_connectivity_request( _: LSXGetInternetConnectedState, ) -> Result, LSXRequestError> { // TODO Actually check this - make_lsx_handler_response!(Response, InternetConnectedState, { attr_connected: 1 }) + return make_lsx_handler_response!(Response, InternetConnectedState, { attr_connected: 1 }) } pub async fn handle_set_downloader_util_request( diff --git a/maxima-lib/src/lsx/request/game.rs b/maxima-lib/src/lsx/request/game.rs index 6e90a5ad..1c98310e 100644 --- a/maxima-lib/src/lsx/request/game.rs +++ b/maxima-lib/src/lsx/request/game.rs @@ -25,32 +25,77 @@ pub async fn handle_game_info_request( LSXGameInfoId::InstalledLanguage => "en_US".to_string(), }; - make_lsx_handler_response!(Response, GetGameInfoResponse, { attr_GameInfo: game_info }) + return make_lsx_handler_response!(Response, GetGameInfoResponse, { attr_GameInfo: game_info }) } // -/// Just realized we're still telling every game that it's titanfall. -/// Should fix that at some point! pub async fn handle_all_game_info_request( - _: LockedConnectionState, - _: LSXGetAllGameInfo, + state: LockedConnectionState, + _request: LSXGetAllGameInfo, ) -> Result, LSXRequestError> { - make_lsx_handler_response!(Response, GetAllGameInfoResponse, { + let current_offer = { + let mut state = state.write().await; + let maxima = state.maxima().await; + + maxima + .playing() + .as_ref() + .and_then(|context| context.current_offer()) + }; + + let (display_name, available_version, installed_version, full_game_release_date) = + match current_offer.as_ref() { + Some(owned_offer) => { + let display_name = owned_offer.offer().display_name().to_string(); + let available_version = owned_offer + .offer() + .downloads() + .first() + .and_then(|download| download.game_version().clone()) + .unwrap_or_else(|| "0".to_string()); + let installed_version = owned_offer + .installed_version() + .await + .ok() + .unwrap_or_else(|| "0".to_string()); + let full_game_release_date = owned_offer + .offer() + .downloads() + .first() + .map(|download| download.build_live_date().clone()) + .unwrap_or_else(|| "0000-00-00T00:00:00".to_string()); + + ( + display_name, + available_version, + installed_version, + full_game_release_date, + ) + } + None => ( + "Unknown Game".to_string(), + "0".to_string(), + "0".to_string(), + "0000-00-00T00:00:00".to_string(), + ), + }; + + return make_lsx_handler_response!(Response, GetAllGameInfoResponse, { attr_FullGamePurchased: true, attr_FullGameReleased: true, - attr_InstalledVersion: "0".to_string(), + attr_InstalledVersion: installed_version, attr_MaxGroupSize: 16, attr_Languages: LANGUAGES.to_string(), attr_Expiration: "0000-00-00T00:00:00".to_string(), attr_UpToDate: true, attr_HasExpiration: false, attr_EntitlementSource: "STEAM".to_string(), - attr_AvailableVersion: "1.0.1.3".to_string(), - attr_DisplayName: "Titanfall® 2 Deluxe Edition".to_string(), + attr_AvailableVersion: available_version, + attr_DisplayName: display_name, attr_FreeTrial: false, attr_InstalledLanguage: "en_US".to_string(), - attr_FullGameReleaseDate: "2016-10-28T04:00:00".to_string(), + attr_FullGameReleaseDate: full_game_release_date, attr_SystemTime: "2023-06-22T04:00:00".to_string() }) } diff --git a/maxima-lib/src/lsx/request/offer.rs b/maxima-lib/src/lsx/request/offer.rs index 145bfc15..aa3c4327 100644 --- a/maxima-lib/src/lsx/request/offer.rs +++ b/maxima-lib/src/lsx/request/offer.rs @@ -48,21 +48,21 @@ pub async fn handle_query_offers_request( attr_InventoryAvailable: 0, attr_PurchaseDate: "0000-00-00T00:00:00".to_string(), attr_DownloadSize: 0, - attr_bCanPurchase: offer.user_can_purchase().clone(), + attr_bCanPurchase: *offer.user_can_purchase(), attr_Price: offer.list_price().to_string(), attr_Type: offer.origin_display_type().to_string(), attr_LocalizedOriginalPrice: offer.display_price().to_string(), attr_InventoryCap: 0, attr_Description: offer.long_description().to_string(), - attr_bHidden: !offer.is_published().clone(), + attr_bHidden: !*offer.is_published(), attr_PlayableDate: offer.published_date().to_string(), attr_Name: offer.display_name().to_string(), attr_ImageId: String::new(), - attr_bIsDiscounted: offer.is_discount().clone(), + attr_bIsDiscounted: *offer.is_discount(), attr_OfferId: offer.offer_id().clone(), - attr_bIsOwned: offer.is_owned().clone(), + attr_bIsOwned: *offer.is_owned(), }) } - make_lsx_handler_response!(Response, QueryOffersResponse, { offer: rtn }) + return make_lsx_handler_response!(Response, QueryOffersResponse, { offer: rtn }) } diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index 5918c7e0..a1c5aa19 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -29,7 +29,7 @@ pub async fn handle_profile_request( let maxima = arc.lock().await; let user = maxima.local_user().await?; - let path = platform_path(maxima.avatar_image(&user.id(), 208, 208).await?); + let path = platform_path(maxima.avatar_image(user.id(), 208, 208).await?); let player = user .player() @@ -38,7 +38,7 @@ pub async fn handle_profile_request( let name = player.unique_name(); debug!("Got profile for {} {:?}", &name, path); - make_lsx_handler_response!(Response, GetProfileResponse, { + return make_lsx_handler_response!(Response, GetProfileResponse, { attr_Persona: name.to_owned(), attr_SubscriberLevel: 0, attr_CommerceCurrency: "USD".to_string(), @@ -51,15 +51,15 @@ pub async fn handle_profile_request( attr_IsSteamSubscriber: false, attr_PersonaId: player.psd().parse::()?, attr_IsUnderAge: false, - attr_UserIndex: 0, - }) + attr_UserIndex: 0, + }); } pub async fn handle_presence_request( _: LockedConnectionState, _: LSXGetPresence, ) -> Result, LSXRequestError> { - make_lsx_handler_response!(Response, GetPresenceResponse, { + return make_lsx_handler_response!(Response, GetPresenceResponse, { attr_UserId: 1005663144213, attr_Presence: LSXPresence::Ingame, attr_Title: None, @@ -68,9 +68,9 @@ pub async fn handle_presence_request( attr_RichPresence: None, attr_GamePresence: None, attr_SessionId: None, - attr_Group: None, - attr_GroupId: None, - }) + attr_Group: None, + attr_GroupId: None, + }); } pub async fn handle_set_presence_request( @@ -80,11 +80,9 @@ pub async fn handle_set_presence_request( info!( "Setting Presence to {:?}: {}", request.attr_Presence, - request - .attr_RichPresence + request.attr_RichPresence .to_owned() - .or(Some(String::new())) - .unwrap() + .unwrap_or(String::new()) ); let arc = state.write().await.maxima_arc(); @@ -110,7 +108,7 @@ pub async fn handle_set_presence_request( .await?; } - make_lsx_handler_response!(Response, ErrorSuccess, { attr_Code: 0, attr_Description: String::new() }) + return make_lsx_handler_response!(Response, ErrorSuccess, { attr_Code: 0, attr_Description: String::new() }); } pub async fn handle_query_presence_request( @@ -152,7 +150,7 @@ pub async fn handle_query_presence_request( }); } - make_lsx_handler_response!(Response, QueryPresenceResponse, { friend: friends }) + return make_lsx_handler_response!(Response, QueryPresenceResponse, { friend: friends }); } pub async fn handle_query_friends_request( @@ -171,7 +169,7 @@ pub async fn handle_query_friends_request( continue; } - let mut presence = presence_store.get(ele.id()).unwrap_or_else(|| { + let presence = presence_store.get(ele.id()).unwrap_or_else(|| { RichPresenceBuilder::default() .basic(BasicPresence::Offline) .status(String::new()) @@ -215,7 +213,7 @@ pub async fn handle_query_friends_request( }); } - make_lsx_handler_response!(Response, QueryFriendsResponse, { friend: lsx_friends }) + return make_lsx_handler_response!(Response, QueryFriendsResponse, { friend: lsx_friends }); } pub async fn handle_get_block_list_request( @@ -251,7 +249,7 @@ pub async fn handle_get_block_list_request( }); } - make_lsx_handler_response!(Response, GetBlockListResponse, { attr_Return: "Success".to_string(), User: list}) + return make_lsx_handler_response!(Response, GetBlockListResponse, { attr_Return: "Success".to_string(), User: list}); } pub async fn handle_query_image_request( @@ -269,13 +267,12 @@ pub async fn handle_query_image_request( .await?, ); - let mut images = Vec::new(); - images.push(LSXImage { + let images = vec![LSXImage { attr_ImageId: request.attr_ImageId, attr_Width: request.attr_Width, attr_Height: request.attr_Height, attr_ResourcePath: path.safe_str()?.to_string(), - }); + }]; - make_lsx_handler_response!(Response, QueryImageResponse, { attr_Result: 1, image: images, }) + return make_lsx_handler_response!(Response, QueryImageResponse, { attr_Result: 1, image: images, }); } diff --git a/maxima-lib/src/lsx/service.rs b/maxima-lib/src/lsx/service.rs index e5cef99c..6a25642e 100644 --- a/maxima-lib/src/lsx/service.rs +++ b/maxima-lib/src/lsx/service.rs @@ -25,15 +25,15 @@ pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServ let mut connections: Vec = Vec::new(); loop { - let mut idx = 0 as usize; + let mut idx = 0_usize; while idx < connections.len() { let connection = &mut connections[idx]; - if let Err(_) = connection.process_queue().await { + if connection.process_queue().await.is_err() { warn!("Failed to process LSX message queue"); } - if let Err(_) = connection.listen().await { + if connection.listen().await.is_err() { warn!("LSX connection closed"); connections.remove(idx); maxima @@ -43,7 +43,7 @@ pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServ continue; } - idx = idx + 1; + idx += 1; } let (socket, addr) = match listener.accept() { @@ -61,12 +61,12 @@ pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServ info!("New LSX connection: {:?}", addr); let conn = Connection::new(maxima.clone(), socket).await; - if let Err(err) = conn { - warn!("Failed to establish LSX connection: {}", err); + if conn.is_err() { + warn!("Failed to establish LSX connection: {}", conn.err().unwrap()); continue; } - let mut conn = conn?; + let mut conn = conn.unwrap(); conn.send_challenge().await?; connections.push(conn); diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index 0a3eafd2..5e31c6ef 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -145,18 +145,18 @@ pub async fn detect_ooa_version(game_path: PathBuf) -> Result if major >= 5 { // >=5.0.0 - return Ok(4); + Ok(4) } else if major == 4 && (minor > 7 || (minor == 7 && patch >= 6)) { // >=4.7.6 - return Ok(3); + Ok(3) } else if major == 4 && minor == 7 { // >=4.7.0 - return Ok(2); + Ok(2) } else if major == 4 { // >=4.0.0 - return Ok(1); + Ok(1) } else { - return Ok(0); + Ok(0) } } @@ -337,14 +337,14 @@ pub async fn save_licenses(license: &License, state: OOAState) -> Result<(), Lic debug!("Saving the license {license:#?}"); save_license( - &license, + license, state, path.join(format!("{}.dlf", license.content_id)), ) .await?; save_license( - &license, + license, state, path.join(format!("{}_cached.dlf", license.content_id)), ) @@ -355,7 +355,7 @@ pub async fn save_licenses(license: &License, state: OOAState) -> Result<(), Lic #[cfg(windows)] pub fn get_license_dir() -> Result { - let path = format!("C:/{}", LICENSE_PATH.to_string()); + let path = format!("C:/{}", LICENSE_PATH); create_dir_all(&path)?; Ok(PathBuf::from(path)) } @@ -367,7 +367,7 @@ pub fn get_license_dir() -> Result { let path = format!( "{}/drive_c/{}", wine_prefix_dir()?.safe_str()?, - LICENSE_PATH.to_string() + LICENSE_PATH ); create_dir_all(&path)?; diff --git a/maxima-lib/src/rtm/client.rs b/maxima-lib/src/rtm/client.rs index 10f2460a..a2c0cb2d 100644 --- a/maxima-lib/src/rtm/client.rs +++ b/maxima-lib/src/rtm/client.rs @@ -28,29 +28,33 @@ macro_rules! send_and_forget_rtm_request { } macro_rules! send_rtm_request { - ($connection_manager: expr, $request_body_name: ident, $comm_name: ident, $response_body_name: ident, $response_comm_name: ident, $comm_initializer:tt) => { + ($connection_manager:expr, $request_body_name:ident, $comm_name:ident, $response_body_name:ident, $response_comm_name:ident, $comm_initializer:tt) => { { fn _rtm_transform( fut: impl Future> + Send, ) -> impl Future> + Send { async move { match fut.await? { - communication_v1::Body::Success(success) => match success.body { - Some(body) => match body { - success_v1::Body::$response_body_name(data) => Ok(data), - any => Err(RtmError::InvalidResponse(any)), - }, - None => Err(RtmError::NoBody), + communication_v1::Body::Success(success) => { + if let Some(body) = success.body { + if matches!(body, success_v1::Body::$response_body_name(_)) { + let success_v1::Body::$response_body_name(data) = body; + Ok(data) + } else { + Err(RtmError::InvalidResponse(body)) + } + } else { + Err(RtmError::NoBody) + } } communication_v1::Body::Error(err) => Err(RtmError::V1(err)), any => Err(RtmError::InvalidVariant(any)), } } } - _rtm_transform($connection_manager.send_request(communication_v1::Body::$request_body_name($comm_name $comm_initializer))) } - } + }; } #[derive(Serialize, Deserialize)] @@ -144,17 +148,12 @@ impl RtmClient { let cloned_presence_store = client.presence_store.clone(); tokio::spawn(async move { - loop { - match receiver_tx.recv().await { - Some(body) => { - if let Err(err) = - RtmClient::process_update(body, cloned_presence_store.clone()).await - { - error!("Failed to process update: {}", err); - } - } - None => break, - }; + while let Some(body) = receiver_tx.recv().await { + if let Err(err) = + RtmClient::process_update(body, cloned_presence_store.clone()).await + { + error!("Failed to process update: {}", err); + } } }); @@ -289,7 +288,7 @@ impl RtmClient { } /// Subscribe to a list of user IDs' presences - pub async fn subscribe(&mut self, players: &Vec) -> Result<(), RtmError> { + pub async fn subscribe(&mut self, players: &[String]) -> Result<(), RtmError> { send_and_forget_rtm_request!(self.conn_man, PresenceSubscribe, PresenceSubscribeV1, { players: players.iter().map(|id| Player{ player_id: id.to_owned(), product_id: String::from("origin"), }).collect() }) diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index ff1bfcb4..1b8b2722 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, error::Error, - io::{self, ErrorKind}, + io::{self}, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -140,10 +140,8 @@ impl RtmConnectionManager { pending_responses.remove(id) { tx.send(msg).unwrap(); - } else if id.is_empty() { - if let Some(body) = &msg.v1.as_ref().ok_or(RtmError::NoBody)?.body { - update_presence_tx.send(body.clone()).await?; - } + } else if id.is_empty() && let Some(body) = &msg.v1.as_ref().ok_or(RtmError::NoBody)?.body { + update_presence_tx.send(body.clone()).await?; } bytes.advance(expected_size as usize); @@ -204,8 +202,7 @@ impl RtmConnectionManager { .ok_or(RtmError::NoBody)? .body .ok_or(RtmError::NoBody)?), - Err(_) => Err(RtmError::Io(io::Error::new( - ErrorKind::Other, + Err(_) => Err(RtmError::Io(io::Error::other( "Failed to receive response", ))), } diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 5668a3fa..80467456 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -142,7 +142,7 @@ async fn read_reg_key(path: &str) -> Result, RegistryError> { }); } - if dw_size <= 0 { + if dw_size == 0 { RegCloseKey(handle); return Err(RegistryError::Value { value: value_name.to_string(), @@ -207,7 +207,7 @@ pub async fn parse_registry_path(key: &str) -> Result { pub async fn parse_partial_registry_path(key: &str) -> Result { let mut parts = key - .split(|c| c == '[' || c == ']') + .split(|c: char| c == '[' || c == ']') .filter(|s| !s.is_empty()); let path = if let (Some(first), Some(_second)) = (parts.next(), parts.next()) { diff --git a/maxima-lib/src/util/simple_crypto.rs b/maxima-lib/src/util/simple_crypto.rs index f2a40926..0c82de70 100644 --- a/maxima-lib/src/util/simple_crypto.rs +++ b/maxima-lib/src/util/simple_crypto.rs @@ -24,7 +24,7 @@ pub fn simple_decrypt(data: &[u8], key: &[u8; 16]) -> String { } pub fn simple_encrypt(data: &[u8], key: &[u8; 16]) -> String { - let res = Aes128EcbEnc::new(&(*key).into()).encrypt_padded_vec::(&data); + let res = Aes128EcbEnc::new(&(*key).into()).encrypt_padded_vec::(data); hex::encode(res) } @@ -47,8 +47,8 @@ pub fn make_lsx_key(seed: u16) -> [u8; 16] { crand.seed(seed); let mut result: [u8; 16] = [0; 16]; - for i in 0..16 { - result[i] = crand.rand() as u8; + for item in &mut result { + *item = crand.rand() as u8; } result } @@ -86,7 +86,7 @@ fn get_array(s: &str) -> Vec { for i in 0..(sb.len() / 2) { let byte_str = &s[(i * 2)..((i * 2) + 2)]; let byte = u8::from_str_radix(byte_str, 16).expect("Failed to parse byte from string"); - m.write(&[byte]).expect("Failed to write byte to stream"); + m.write_all(&[byte]).expect("Failed to write byte to stream"); } m.into_inner() } diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index a361cc4e..72f5aea1 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -6,26 +6,25 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -tokio = { version = "1.28.2", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } -serde = { version = "1.0.164", features = [ "derive" ] } -serde_json = "1.0.97" -paste = "1.0.12" -reqwest = { version = "0.11.18", features = ["stream", "rustls-tls"], default-features = false } -base64 = "0.21.2" -log = "0.4.19" -regex = "1.8.4" -directories = "5.0.1" -structured-logger = "1.0.1" -actix-web = "4.8.0" -sha2 = "0.10.7" -derive_more = "0.99.17" +tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } +serde = { version = "1.0.228", features = [ "derive" ] } +serde_json = "1.0.150" +reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } +base64 = "0.22.1" +log = "0.4.32" +regex = "1.12.4" +directories = "6.0.0" +structured-logger = "1.0.5" +actix-web = "4.13.0" +sha2 = "0.11.0" +derive_more = "2.1.1" hex = "0.4.3" -thiserror = "2.0.12" +thiserror = "2.0.18" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi" ] } -winreg = "0.50.0" -windows-service = "0.6.0" +winreg = "0.56.0" +windows-service = "0.8.1" [build-dependencies] maxima-resources = { path = "../maxima-resources" } diff --git a/maxima-service/src/service/error.rs b/maxima-service/src/service/error.rs index 232e7198..a5b251f8 100644 --- a/maxima-service/src/service/error.rs +++ b/maxima-service/src/service/error.rs @@ -1,5 +1,4 @@ -use actix_web::{HttpResponse, error, http::header::ContentType}; -use reqwest::StatusCode; +use actix_web::{HttpResponse, error, http::header::ContentType, http::StatusCode}; use thiserror::Error; #[derive(Error, Debug)] diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index 11fd8ee1..ec60e138 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -13,8 +13,8 @@ serde_json = "1.0.150" anyhow = "1.0.102" lazy_static = "1.5.0" base64 = "0.22.1" -log = "0.4.30" -regex = "1.12.3" +log = "0.4.32" +regex = "1.12.4" inquire = "0.9.4" futures = "0.3.32" ratatui = "0.30.1" diff --git a/maxima-tui/src/service.rs b/maxima-tui/src/service.rs index 9f048af7..657a8aa9 100644 --- a/maxima-tui/src/service.rs +++ b/maxima-tui/src/service.rs @@ -47,120 +47,110 @@ impl BridgeThread { let (tx1, rx0) = mpsc::channel(); tokio::task::spawn(async move { - let die_fallback_transmitter = tx1.clone(); - //panic::set_hook(Box::new( |_| {})); - let result = BridgeThread::run(rx1, tx1).await; - if result.is_err() { - die_fallback_transmitter - .send(MaximaLibResponse::InteractionThreadDiedResponse) - .unwrap(); - panic!("Interact thread failed! {}", result.err().unwrap()); - } else { - info!("Interact thread shut down") + let die_fallback = tx1.clone(); + + if let Err(err) = BridgeThread::run(rx1, tx1).await { + let _ = die_fallback.send(MaximaLibResponse::InteractionThreadDiedResponse); + panic!("Interact thread failed! {err}"); } + + info!("Interact thread shut down"); }); Self { rx: rx0, tx: tx0 } } - async fn run(rx1: Receiver, tx1: Sender) -> Result<()> { + async fn run( + rx1: Receiver, + tx1: Sender, + ) -> Result<()> { let maxima_arc: LockedMaxima = Maxima::new_with_options( MaximaOptionsBuilder::default() .dummy_local_user(false) .load_auth_storage(true) .build()?, - ) - .await?; + ).await?; { let maxima = maxima_arc.lock().await; - if maxima.start_lsx(maxima_arc.clone()).await.is_ok() { - info!("LSX started"); - } else { + + let Ok(()) = maxima.start_lsx(maxima_arc.clone()).await else { info!("LSX failed to start!"); - } + return Ok(()); + }; + info!("LSX started"); - let mut auth_storage = maxima.auth_storage().lock().await; - let logged_in = auth_storage.logged_in().await?; - if logged_in { - drop(auth_storage); + let logged_in = { + let mut auth_storage = maxima.auth_storage().lock().await; + auth_storage.logged_in().await? + }; + if logged_in { let user = maxima.local_user().await?; let message = MaximaLibResponse::LoginResponse(InteractThreadLoginResponse { success: true, name: user.player().as_ref().unwrap().display_name().to_owned(), }); - tx1.send(message)?; } else { tx1.send(MaximaLibResponse::LoginCacheEmpty)?; } } - 'outer: loop { - let request = rx1.try_recv(); - if request.is_err() { + 'main: loop { + let Ok(request) = rx1.try_recv() else { continue; - } + }; - match request? { + let result: Result<()> = match request { MaximaLibRequest::LoginRequest => { - let channel = tx1.clone(); - let maxima = maxima_arc.clone(); - async move { - let maxima = maxima.lock().await; - + let login = async || { + let maxima = maxima_arc.lock().await; let mut auth_storage = maxima.auth_storage().lock().await; - let logged_in = auth_storage.logged_in().await?; - if !logged_in { - let res = login_flow().await.unwrap(); - maxima.auth_storage().lock().await.add_account(&res); - }; - - channel - .send(MaximaLibResponse::LoginResponse( - InteractThreadLoginResponse { - success: true, - name: maxima - .local_user() - .await? - .player() - .as_ref() - .unwrap() - .display_name() - .to_owned(), - }, - )) - .unwrap(); - - Ok::<(), anyhow::Error>(()) - } - .await?; + + if !auth_storage.logged_in().await? { + let res = login_flow().await?; + auth_storage.add_account(&res); + } + + tx1.send(MaximaLibResponse::LoginResponse(InteractThreadLoginResponse { + success: true, + name: maxima + .local_user() + .await? + .player() + .as_ref() + .unwrap() + .display_name() + .to_owned(), + }))?; + + Ok(()) + }; + login().await } + MaximaLibRequest::GetGamesRequest => { - let channel = tx1.clone(); - let maxima = maxima_arc.clone(); + todo!("GetGamesRequest") } MaximaLibRequest::GetFriendsRequest => { - let channel = tx1.clone(); - let maxima = maxima_arc.clone(); + todo!("GetFriendsRequest") } - MaximaLibRequest::GetGameImagesRequest(slug) => { - let channel = tx1.clone(); - let maxima = maxima_arc.clone(); + MaximaLibRequest::GetGameImagesRequest(_) => { + todo!("GetGameImagesRequest") } - MaximaLibRequest::GetUserAvatarRequest(id, url) => { - let channel = tx1.clone(); + MaximaLibRequest::GetUserAvatarRequest(_, _) => { + todo!("GetUserAvatarRequest") } - MaximaLibRequest::GetGameDetailsRequest(slug) => { - let channel = tx1.clone(); - let maxima = maxima_arc.clone(); + MaximaLibRequest::GetGameDetailsRequest(_) => { + todo!("GetGameDetailsRequest") } - MaximaLibRequest::StartGameRequest(offer_id, hardcode) => { - //start_game_request(maxima_arc.clone(), offer_id.clone(), hardcode).await; + MaximaLibRequest::StartGameRequest(_, _) => { + todo!("StartGameRequest") } - MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), - } + + MaximaLibRequest::ShutdownRequest => break 'main Ok(()), + }; } } } diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index d294d1b1..7e16d1be 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -7,21 +7,21 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi", "psapi", "wininet", "winerror", "iphlpapi", "tcpmib" ] } -anyhow = "1.0.71" -clap = { version = "4.3.8", features = ["derive"] } -eframe = { version = "0.34.3", features = [ "persistence", "glow" ] } +anyhow = "1.0.102" +clap = { version = "4.6.1", features = ["derive"] } +eframe = { version = "0.34.3", default-features = false, features = [ "persistence", "glow" ] } egui = "0.34.3" egui_extras = {version = "0.34.3", features = [ "svg", "all_loaders" ] } image = { version = "0.25.10", features = [ "png", "jpeg" ] } reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } -serde = "1.0.171" -serde_json = "1.0.100" +serde = "1.0.228" +serde_json = "1.0.150" tokio = { version = "1.52.3", features = ["full"] } -log = "0.4.19" +log = "0.4.32" egui_demo_lib = "0.34.3" puffin_http = "0.17.0" puffin = "0.20.0" -rustls = { version = "0.23", features = ["aws-lc-rs"] } +rustls = { version = "0.23.40", features = ["aws-lc-rs"] } strum = { version = "0.28.0", features = ["derive"] } strum_macros = "0.28.0" sys-locale = "0.3.2" diff --git a/maxima-ui/src/bridge/game_details.rs b/maxima-ui/src/bridge/game_details.rs index a0d54ae1..db27a720 100644 --- a/maxima-ui/src/bridge/game_details.rs +++ b/maxima-ui/src/bridge/game_details.rs @@ -1,5 +1,5 @@ use egui::Context; -use std::sync::mpsc::Sender; +use tokio::sync::mpsc::UnboundedSender; use crate::{ GameDetails, @@ -17,7 +17,7 @@ use maxima::core::{ pub async fn game_details_request( maxima_arc: LockedMaxima, slug: String, - channel: Sender, + channel: UnboundedSender, ctx: &Context, ) -> Result<(), BackendError> { let maxima = maxima_arc.lock().await; @@ -32,8 +32,6 @@ pub async fn game_details_request( ); let rq: ServiceGameSystemRequirements = rq.await?; - //TODO: parse async - let (min, rec) = if rq.system_requirements().len() >= 1 { ( Some(html_to_easymark(rq.system_requirements()[0].minimum())), @@ -54,7 +52,7 @@ pub async fn game_details_request( system_requirements_rec: rec, }, }); - let _ = channel.send(res); + channel.send(res).ok(); egui::Context::request_repaint(&ctx); Ok(()) } diff --git a/maxima-ui/src/bridge/get_friends.rs b/maxima-ui/src/bridge/get_friends.rs index 86b6648b..da66ab08 100644 --- a/maxima-ui/src/bridge/get_friends.rs +++ b/maxima-ui/src/bridge/get_friends.rs @@ -1,7 +1,7 @@ use egui::Context; use log::debug; use maxima::{core::LockedMaxima, rtm::client::BasicPresence}; -use std::sync::mpsc::Sender; +use tokio::sync::mpsc::UnboundedSender; use crate::{ bridge_thread::{BackendError, InteractThreadFriendListResponse, MaximaLibResponse}, @@ -11,8 +11,8 @@ use crate::{ pub async fn get_friends_request( maxima_arc: LockedMaxima, - channel: Sender, - remote_provider_channel: Sender, + channel: UnboundedSender, + remote_provider_channel: UnboundedSender, ctx: &Context, ) -> Result<(), BackendError> { debug!("received request to load friends"); @@ -27,7 +27,7 @@ pub async fn get_friends_request( remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote( crate::ui_image::UIImageType::Avatar(friend.id().to_string()), friend.avatar().as_ref().unwrap().medium().path().to_string(), - ))?; + )).ok(); let friend_info = UIFriend { name: friend.display_name().to_string(), id: friend.id().to_string(), @@ -39,7 +39,7 @@ pub async fn get_friends_request( let res = MaximaLibResponse::FriendInfoResponse(InteractThreadFriendListResponse { friend: friend_info, }); - channel.send(res)?; + channel.send(res).ok(); ctx.request_repaint(); } diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index d0a7305c..1667f2e3 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -16,7 +16,7 @@ use maxima::{ }, util::native::maxima_dir, }; -use std::{fs, sync::mpsc::Sender}; +use tokio::sync::mpsc::UnboundedSender; fn get_preferred_bg_hero(heroes: &Option) -> Option { let heroes = match heroes { @@ -103,7 +103,7 @@ async fn handle_images( has_hero: bool, has_logo: bool, has_background: bool, - channel: Sender, + channel: UnboundedSender, service_layer: ServiceLayerClient, ) -> Result<(), BackendError> { debug!("handling image downloads for {}", &slug); @@ -147,7 +147,7 @@ async fn handle_images( channel.send(UIImageCacheLoaderCommand::ProvideRemote( crate::ui_image::UIImageType::Hero(slug.clone()), hero, - ))? + )).ok(); } } @@ -156,11 +156,11 @@ async fn handle_images( channel.send(UIImageCacheLoaderCommand::ProvideRemote( crate::ui_image::UIImageType::Logo(slug.clone()), logo, - ))? + )).ok(); } else { channel.send(UIImageCacheLoaderCommand::Stub( crate::ui_image::UIImageType::Logo(slug.clone()), - ))? + )).ok(); } } @@ -177,7 +177,7 @@ async fn handle_images( channel.send(UIImageCacheLoaderCommand::ProvideRemote( crate::ui_image::UIImageType::Background(slug), background_image, - ))? + )).ok(); } } @@ -186,20 +186,23 @@ async fn handle_images( pub async fn get_games_request( maxima_arc: LockedMaxima, - channel: Sender, - channel1: Sender, + channel: UnboundedSender, + channel1: UnboundedSender, ctx: &Context, ) -> Result<(), BackendError> { debug!("received request to load games"); - let mut maxima = maxima_arc.lock().await; - 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(); - if !logged_in { - return Err(BackendError::LoggedOut); - } - let owned_games = maxima.mut_library().games().await?; + let (service_layer, locale, owned_games) = { + let mut maxima = maxima_arc.lock().await; + 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(); + if !logged_in { + return Err(BackendError::LoggedOut); + } + let owned_games = maxima.mut_library().games().await?.clone(); + (service_layer, locale, owned_games) + }; for game in owned_games { let slug = game.base_offer().slug().clone(); @@ -212,10 +215,7 @@ pub async fn get_games_request( match downloads.iter().find(|item| item.download_type() == "LIVE") { Some(item) => item, None => { - error!( - "Cannot find LIVE download for game with multiple downloads: {}", - &slug - ); + error!("Cannot find LIVE download for game with multiple downloads: {}", &slug); continue; } } @@ -241,25 +241,28 @@ pub async fn get_games_request( installed: game.base_offer().is_installed().await, has_cloud_saves: game.base_offer().offer().has_cloud_save(), }; + let slug = game_info.slug.clone(); 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(), }; - let res = MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { + + channel.send(MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { game: game_info, settings, - }); - channel.send(res)?; + })).ok(); let bg = maxima_dir()?.join("cache/ui/images/").join(&slug).join("background.jpg"); let game_hero = maxima_dir()?.join("cache/ui/images/").join(&slug).join("hero.jpg"); let game_logo = maxima_dir()?.join("cache/ui/images/").join(&slug).join("logo.png"); - let has_hero = fs::metadata(&game_hero).is_ok(); - let has_logo = fs::metadata(&game_logo).is_ok(); - let has_background = fs::metadata(&bg).is_ok(); + + let (has_hero, has_logo, has_background) = tokio::join!( + async { tokio::fs::metadata(&game_hero).await.is_ok() }, + async { tokio::fs::metadata(&game_logo).await.is_ok() }, + async { tokio::fs::metadata(&bg).await.is_ok() }, + ); if !has_hero || !has_logo || !has_background { //we're like 20 tasks deep i swear but this shit's gonna be real fast, trust @@ -268,7 +271,7 @@ pub async fn get_games_request( let channel_send = channel1.clone(); let service_layer_send = service_layer.clone(); tokio::task::spawn(async move { - handle_images( + let _ = handle_images( slug_send, locale_send, has_hero, @@ -276,10 +279,9 @@ pub async fn get_games_request( has_background, channel_send, service_layer_send, - ) - .await + ).await; }); - tokio::task::yield_now().await; + tokio::task::yield_now().await; } egui::Context::request_repaint(&ctx); diff --git a/maxima-ui/src/bridge/login_oauth.rs b/maxima-ui/src/bridge/login_oauth.rs index a45e1cf5..2882b748 100644 --- a/maxima-ui/src/bridge/login_oauth.rs +++ b/maxima-ui/src/bridge/login_oauth.rs @@ -8,11 +8,11 @@ use maxima::{ }, util::native::take_foreground_focus, }; -use std::sync::mpsc::Sender; +use tokio::sync::mpsc::UnboundedSender; pub async fn login_oauth( maxima_arc: LockedMaxima, - channel: Sender, + channel: UnboundedSender, ctx: &Context, ) -> Result<(), BackendError> { let maxima = maxima_arc.lock().await; @@ -30,7 +30,7 @@ pub async fn login_oauth( you: user.player().as_ref().ok_or(ServiceLayerError::MissingField)?.to_owned(), })); - channel.send(message)?; + channel.send(message).ok(); take_foreground_focus()?; ctx.request_repaint(); diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 1d1da6a2..db92005a 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -1,10 +1,12 @@ use crate::{ - BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, bridge_thread::{self, BackendError}, views::downloads_view::QueuedDownload, + BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, }; -use log::{error, info, warn}; -use std::sync::mpsc::TryRecvError; +use log::{info, warn}; +use tokio::sync::mpsc::error::TryRecvError; + +const MAX_MESSAGES_PER_FRAME: usize = 64; pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { puffin::profile_function!(); @@ -13,135 +15,148 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { return; } - 'outer: loop { + let mut needs_repaint = false; + + for _ in 0..MAX_MESSAGES_PER_FRAME { match app.backend.backend_listener.try_recv() { - Ok(result) => { - use bridge_thread::MaximaLibResponse::*; - match result { - LoginResponse(res) => { - if let Err(error) = &res { - warn!("Login failed. {}", error); - continue; - } - let res = res.unwrap(); + Ok(response) => { + needs_repaint = true; + handle_backend_response(app, response); + if app.critical_error.is_some() { + break; + } + } + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => { + app.critical_error = Some(BackendError::ChannelDisconnected); + break; + } + } + } - info!("Logged in as {}!", &res.you.display_name()); - app.user_name = res.you.display_name().clone(); - app.user_id = res.you.id().clone(); - app.backend_state = BackendStallState::BingChilling; - app.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::GetGamesRequest) - .unwrap(); - app.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::GetFriendsRequest) - .unwrap(); - } - LoginCacheEmpty => app.backend_state = BackendStallState::UserNeedsToLogIn, - ServiceNeedsStarting => { - app.backend_state = BackendStallState::UserNeedsToInstallService - } - ServiceStarted => app.backend_state = BackendStallState::Starting, - GameInfoResponse(res) => { - app.games.insert(res.game.slug.clone(), res.game); - } - GameDetailsResponse(res) => { - let response = res.response; + if needs_repaint { + ctx.request_repaint(); + } +} - for (slug, game) in &mut app.games { - if !slug.eq(&res.slug) { - continue; - } +fn send_request(app: &mut MaximaEguiApp, request: bridge_thread::MaximaLibRequest) { + if app.backend.backend_commander.send(request).is_err() { + warn!("Bridge thread disconnected during send"); + app.critical_error = Some(BackendError::ChannelDisconnected); + } +} - game.details = GameDetailsWrapper::Available(GameDetails { - time: response.time, - achievements_unlocked: response.achievements_unlocked, - achievements_total: response.achievements_total, - path: response.path.clone(), - system_requirements_min: response.system_requirements_min.clone(), - system_requirements_rec: response.system_requirements_rec.clone(), - }); - } - } - FriendInfoResponse(res) => app.friends.push(res.friend), - CriticalError(err) => app.critical_error = Some(*err), - NonFatalError(err) => app.nonfatal_errors.push(*err), - ActiveGameChanged(slug) => app.playing_game = slug, - LocateGameResponse(res) => { - app.installer_state.locate_response = Some(res); - app.installer_state.locating = false; - } - DownloadProgressChanged(offer_id, progress) => { - if let Some(dl_ing) = app.installing_now.as_mut() { - if dl_ing.offer == offer_id { - dl_ing.downloaded_bytes = progress.bytes; - dl_ing.total_bytes = progress.bytes_total; - } - } - } - DownloadFinished(_) => { - // idk - } - DownloadQueueUpdate(current, queue) => { - if let Some(current) = current { - if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { - app.installing_now = Some(QueuedDownload { - slug: { - // This sucks! - let mut rtn: String = String::new(); - for (slug, game) in &app.games { - if game.offer.eq(¤t) { - // "but it's less code in the nest" - // "WHO CARES" - // (it was the same amount overall) - rtn = slug.to_string(); - break; - } - } - rtn - }, - offer: current, - downloaded_bytes: 0, - total_bytes: 0, - }) - } - } else { - app.installing_now = None; - } +fn find_slug_for_offer(games: &std::collections::HashMap, offer: &str) -> String { + games + .iter() + .find(|(_, g)| g.offer == offer) + .map(|(slug, _)| slug.clone()) + .unwrap_or_default() +} - app.install_queue.clear(); - for offer in queue { - let i_fucking_hate_this = QueuedDownload { - slug: { - let mut rtn: String = String::new(); - for (slug, game) in &app.games { - if game.offer.eq(&offer) { - rtn = slug.to_string(); - break; - } - } - rtn - }, - offer: offer.clone(), - downloaded_bytes: 0, - total_bytes: 0, - }; - app.install_queue.insert(offer, i_fucking_hate_this); - } - } - } - ctx.request_repaint(); +fn handle_backend_response(app: &mut MaximaEguiApp, response: bridge_thread::MaximaLibResponse) { + use bridge_thread::MaximaLibResponse::*; + match response { + LoginResponse(res) => handle_login_response(app, res), + LoginCacheEmpty => app.backend_state = BackendStallState::UserNeedsToLogIn, + ServiceNeedsStarting => app.backend_state = BackendStallState::UserNeedsToInstallService, + ServiceStarted => app.backend_state = BackendStallState::Starting, + GameInfoResponse(res) => { + app.games.entry(res.game.slug.clone()).or_insert(res.game); + } + GameDetailsResponse(res) => handle_game_details_response(app, res), + FriendInfoResponse(res) => { + if !app.friends.iter().any(|f| f.id == res.friend.id) { + app.friends.push(res.friend); } - Err(variant) => { - match variant { - TryRecvError::Empty => {} - TryRecvError::Disconnected => { - app.critical_error = Some(BackendError::ChannelDisconnected); - } + } + CriticalError(err) => app.critical_error = Some(*err), + NonFatalError(err) => app.nonfatal_errors.push(*err), + ActiveGameChanged(slug) => app.playing_game = slug, + LocateGameResponse(res) => { + app.installer_state.locate_response = Some(res); + app.installer_state.locating = false; + } + DownloadProgressChanged(offer_id, progress) => { + if let Some(dl) = app.installing_now.as_mut() { + if dl.offer == offer_id { + dl.downloaded_bytes = progress.bytes; + dl.total_bytes = progress.bytes_total; } - break 'outer; } } + DownloadFinished(offer_id) => { + if app.installing_now.as_ref().is_some_and(|n| n.offer == offer_id) { + app.installing_now = None; + } + send_request(app, bridge_thread::MaximaLibRequest::GetGamesRequest); + } + DownloadQueueUpdate(current, queue) => handle_download_queue_update(app, current, queue), + } +} + +fn handle_login_response( + app: &mut MaximaEguiApp, + res: Result, +) { + match res { + Err(error) => { + warn!("Login failed: {}", error); + app.backend_state = BackendStallState::UserNeedsToLogIn; + } + Ok(res) => { + info!("Logged in as {}!", res.you.display_name()); + app.user_name = res.you.display_name().clone(); + app.user_id = res.you.id().clone(); + app.backend_state = BackendStallState::BingChilling; + send_request(app, bridge_thread::MaximaLibRequest::GetGamesRequest); + send_request(app, bridge_thread::MaximaLibRequest::GetFriendsRequest); + } } } + +fn handle_game_details_response( + app: &mut MaximaEguiApp, + res: bridge_thread::InteractThreadGameDetailsResponse, +) { + if let Some(game) = app.games.get_mut(&res.slug) { + let r = res.response; + game.details = GameDetailsWrapper::Available(GameDetails { + time: r.time, + achievements_unlocked: r.achievements_unlocked, + achievements_total: r.achievements_total, + path: r.path.clone(), + system_requirements_min: r.system_requirements_min.clone(), + system_requirements_rec: r.system_requirements_rec.clone(), + }); + } +} + +fn handle_download_queue_update( + app: &mut MaximaEguiApp, + current: Option, + queue: Vec, +) { + if let Some(current_offer) = current { + if !app.installing_now.as_ref().is_some_and(|n| n.offer == current_offer) { + let slug = find_slug_for_offer(&app.games, ¤t_offer); + app.installing_now = Some(QueuedDownload { + slug, + offer: current_offer, + downloaded_bytes: 0, + total_bytes: 0, + }); + } + } else { + app.installing_now = None; + } + + app.install_queue.clear(); + for offer in queue { + let slug = find_slug_for_offer(&app.games, &offer); + app.install_queue.insert( + offer.clone(), + QueuedDownload { slug, offer, downloaded_bytes: 0, total_bytes: 0 }, + ); + } +} \ No newline at end of file diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 56f6fab0..9df4b763 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -16,15 +16,10 @@ use maxima::{ ContentManager, ContentManagerError, QueuedGameBuilder, QueuedGameBuilderError, }, core::{ - LockedMaxima, Maxima, MaximaCreationError, MaximaOptionsBuilder, MaximaOptionsBuilderError, - auth::storage::{AuthError, TokenError}, - launch::LaunchError, - library::LibraryError, - manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, - service_layer::{ + LockedMaxima, Maxima, MaximaCreationError, MaximaEvent, MaximaOptionsBuilder, MaximaOptionsBuilderError, auth::storage::{AuthError, TokenError}, launch::LaunchError, library::LibraryError, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ ServiceGameImagesRequestBuilderError, ServiceHeroBackgroundImageRequestBuilderError, ServiceLayerError, ServicePlayer, - }, + } }, lsx::service::LSXServerError, rtm::RtmError, @@ -33,11 +28,11 @@ use maxima::{ registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; -use std::sync::mpsc::{SendError, TryRecvError}; +use tokio::sync::mpsc::{ + Sender, UnboundedReceiver, UnboundedSender, error::{SendError, TryRecvError}, unbounded_channel, +}; use std::{ - panic, path::PathBuf, - sync::mpsc::{Receiver, Sender}, time::{Duration, SystemTime}, }; @@ -105,11 +100,10 @@ pub enum MaximaLibResponse { DownloadQueueUpdate(Option, Vec), } pub struct BridgeThread { - pub backend_listener: Receiver, - pub backend_commander: Sender, - - pub rtm_listener: Receiver, - pub rtm_commander: Sender, // currently unused except for shutdown + pub backend_listener: UnboundedReceiver, + pub backend_commander: UnboundedSender, + pub rtm_listener: UnboundedReceiver, + pub rtm_commander: UnboundedSender, } #[derive(thiserror::Error, Debug)] @@ -152,11 +146,11 @@ pub enum BackendError { ServiceLayer(#[from] ServiceLayerError), #[error(transparent)] Token(#[from] TokenError), - #[error(transparent)] - TryRecv(#[from] TryRecvError), #[error("backend-frontend communication channel disconnected")] ChannelDisconnected, + #[error("no live build available for the requested offer")] + NoLiveBuildAvailable, #[error("tried to perform an action that requires being logged in, but was logged out")] LoggedOut, } @@ -164,7 +158,7 @@ pub enum BackendError { impl BridgeThread { fn update_queue( content_manager: &ContentManager, - backend_responder: Sender, + backend_responder: UnboundedSender, ) { let current = if let Some(now) = content_manager.queue().current() { Some(now.offer_id().to_owned()) @@ -183,35 +177,38 @@ impl BridgeThread { .unwrap(); } - pub fn new(ctx: &Context, remote_provider_channel: Sender) -> Self { + pub fn new(ctx: &Context, remote_provider_channel: UnboundedSender) -> Self { puffin::profile_function!(); - let (backend_commander, backend_cmd_listener) = std::sync::mpsc::channel(); - let (backend_responder, backend_listener) = std::sync::mpsc::channel(); - - let (rtm_commander, rtm_cmd_listener) = std::sync::mpsc::channel(); - let (rtm_responder, rtm_listener) = std::sync::mpsc::channel(); + let (backend_commander, backend_cmd_listener) = unbounded_channel(); + let (backend_responder, backend_listener) = unbounded_channel(); + let (rtm_commander, rtm_cmd_listener) = unbounded_channel(); + let (rtm_responder, rtm_listener) = unbounded_channel(); let context = ctx.clone(); + let backend_responder_for_task = backend_responder.clone(); + let remote_provider_for_task = remote_provider_channel.clone(); + + // you dare spawn a thread without catching my sneaky sneaky panics mister Potter ?? tokio::task::spawn(async move { - let die_fallback_transmitter = backend_responder.clone(); - //panic::set_hook(Box::new( |_| {})); - let result = BridgeThread::run( + let die_fallback = backend_responder_for_task.clone(); + match BridgeThread::run( backend_cmd_listener, - backend_responder, + backend_responder_for_task, rtm_cmd_listener, rtm_responder, - remote_provider_channel, + remote_provider_for_task, &context, - ) - .await; - if let Err(err) = result { - die_fallback_transmitter - .send(MaximaLibResponse::CriticalError(Box::from(err))) - .unwrap(); - } else { - info!("Interact thread shut down") + ).await { + Ok(()) => (), + Err(err) => { + error!("BridgeThread task: run() returned Err: {}", err); + let _ = die_fallback + .send(MaximaLibResponse::NonFatalError(Box::from(err))); + } } + + info!("Interact thread shut down"); }); Self { @@ -223,49 +220,48 @@ impl BridgeThread { } async fn run( - backend_cmd_listener: Receiver, - backend_responder: Sender, - rtm_cmd_listener: Receiver, - rtm_responder: Sender, - remote_provider_channel: Sender, + mut backend_cmd_listener: UnboundedReceiver, + backend_responder: UnboundedSender, + rtm_cmd_listener: UnboundedReceiver, + rtm_responder: UnboundedSender, + remote_provider_channel: UnboundedSender, ctx: &Context, ) -> Result<(), BackendError> { // first things first check registry // the flow is different for windows/linux but windows needs an extra user prompt, // so we're doing both here, instead of selectively cfg'd functions! #[cfg(not(windows))] - { - if let Err(err) = check_registry_validity() { - warn!("{}, fixing...", err); - set_up_registry()?; - } + if let Err(err) = check_registry_validity() { + warn!("{}, fixing...", err); + set_up_registry()?; } + #[cfg(windows)] { use maxima::{ core::background_service::request_registry_setup, util::{ registry::check_registry_validity, - service::{ - is_service_running, is_service_valid, register_service_user, start_service, - }, + service::{is_service_running, is_service_valid, register_service_user, start_service}, }, }; + if !is_elevated::is_elevated() { if !is_service_valid()? { info!("Installing service..."); - backend_responder.send(MaximaLibResponse::ServiceNeedsStarting)?; - 'wait_for_user_to_authorize: loop { - let request = backend_cmd_listener.try_recv(); - if request.is_err() { - continue; - } + backend_responder.send(MaximaLibResponse::ServiceNeedsStarting).ok(); + + 'wait_for_auth: loop { + let Some(request) = backend_cmd_listener.recv().await else { + info!("Backend command channel closed, shutting down"); + return Ok(()); + }; - match request.unwrap() { + match request { MaximaLibRequest::StartService => { register_service_user()?; - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - break 'wait_for_user_to_authorize; + tokio::time::sleep(Duration::from_secs(1)).await; + break 'wait_for_auth; } MaximaLibRequest::ShutdownRequest => return Ok(()), _ => {} @@ -289,8 +285,8 @@ impl BridgeThread { .dummy_local_user(false) .load_auth_storage(true) .build()?, - ) - .await?; + ).await?; + let logged_in = { let maxima = maxima_arc.lock().await; @@ -302,24 +298,20 @@ impl BridgeThread { }; if !logged_in { - backend_responder.send(MaximaLibResponse::LoginCacheEmpty)?; - 'outer: loop { - let request = backend_cmd_listener.try_recv(); - if request.is_err() { - continue; - } + backend_responder.send(MaximaLibResponse::LoginCacheEmpty).ok(); - match request? { + 'login: loop { + let Ok(request) = backend_cmd_listener.try_recv() else { + continue; + }; + match request { MaximaLibRequest::LoginRequestOauth => { - let channel = backend_responder.clone(); - let maxima = maxima_arc.clone(); - let context = ctx.clone(); - async move { login_oauth(maxima, channel, &context).await } - .await - .expect("// TODO(headassbtw): panic message"); - break 'outer; + login_oauth(maxima_arc.clone(), backend_responder.clone(), ctx).await.expect("// TODO(headassbtw): panic message"); + break 'login; + } + MaximaLibRequest::ShutdownRequest => { + return Ok(()); } - MaximaLibRequest::ShutdownRequest => return Ok(()), _ => {} } } @@ -330,14 +322,15 @@ impl BridgeThread { let user = maxima.local_user().await?; if logged_in { - let message = MaximaLibResponse::LoginResponse(Ok(InteractThreadLoginResponse { - you: user.player().as_ref().unwrap().to_owned(), - })); - backend_responder.send(message)?; + backend_responder.send(MaximaLibResponse::LoginResponse(Ok( + InteractThreadLoginResponse { + you: user.player().as_ref().unwrap().to_owned(), + } + ))).ok(); } - let res = remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote( - crate::ui_image::UIImageType::Avatar(user.id().to_string()), - user.player() + + let avatar_result: Result<(), BackendError> = (|| async { + let avatar = user.player() .as_ref() .ok_or(ServiceLayerError::MissingField)? .avatar() @@ -345,48 +338,50 @@ impl BridgeThread { .ok_or(ServiceLayerError::MissingField)? .medium() .path() - .to_string(), - )); - if let Err(err) = res { - error!("failed to send user pfp to loader: {:?}", err); + .to_string(); + + remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote( + crate::ui_image::UIImageType::Avatar(user.id().to_string()), + avatar, + )).ok(); + Ok(()) + })().await; + + if let Err(e) = avatar_result { + warn!("Failed to load user avatar: {}", e); + // continue anyway } + ctx.request_repaint(); } - let _ = EventThread::new( - &ctx.clone(), - maxima_arc.clone(), - rtm_cmd_listener, - rtm_responder, - ); + let _ = EventThread::new(&ctx.clone(), maxima_arc.clone(), rtm_cmd_listener, rtm_responder); - let mut future = SystemTime::now(); - future = future.checked_add(Duration::from_millis(50)).unwrap(); + let mut next_tick = SystemTime::now() + Duration::from_millis(50); let mut playing_cache: Option = None; - 'outer: loop { - let now = SystemTime::now(); - if now >= future { - // this sucks but it's non-blocking so oh well what are you going to do about it! it's on a non-ui thread anyway, i'm wasteful with it - future = now.checked_add(Duration::from_millis(50)).unwrap(); + 'main: loop { + let now = SystemTime::now(); + + if now >= next_tick { + next_tick = now + Duration::from_millis(50); let mut maxima = maxima_arc.lock().await; maxima.update().await; - let now_playing = maxima.playing(); - if let Some(ctx) = now_playing { - if let Some(offer) = ctx.offer() { - if playing_cache.is_none() { + match maxima.playing() { + Some(ctx) if playing_cache.is_none() => { + if let Some(offer) = ctx.offer() { playing_cache = Some(offer.slug().clone()); - backend_responder.send(MaximaLibResponse::ActiveGameChanged(Some( - offer.slug().clone(), - )))?; + backend_responder.send(MaximaLibResponse::ActiveGameChanged( + Some(offer.slug().clone()) + )).ok(); } } - } else { - if playing_cache.is_some() { + None if playing_cache.is_some() => { playing_cache = None; - backend_responder.send(MaximaLibResponse::ActiveGameChanged(None)).unwrap(); - }; + backend_responder.send(MaximaLibResponse::ActiveGameChanged(None)).ok(); + } + _ => {} } if let Some(dl) = maxima.content_manager().current() { @@ -396,123 +391,113 @@ impl BridgeThread { bytes: dl.bytes_downloaded(), bytes_total: dl.bytes_total(), }, - ))?; + )).ok(); } for ev in maxima.consume_pending_events() { match ev { - maxima::core::MaximaEvent::ReceivedLSXRequest(_, _) => {} - maxima::core::MaximaEvent::InstallFinished(offer_id) => { - backend_responder - .send(MaximaLibResponse::DownloadFinished(offer_id))?; + MaximaEvent::ReceivedLSXRequest(_, _) => {} + MaximaEvent::InstallFinished(offer_id) => { + backend_responder.send(MaximaLibResponse::DownloadFinished(offer_id)).ok(); Self::update_queue(maxima.content_manager(), backend_responder.clone()); } } } } - let request = backend_cmd_listener.try_recv(); - if request.is_err() { - continue; - } - let action = match request? { + let Some(request) = backend_cmd_listener.recv().await else { + continue 'main; + }; + + let result: Result<(), BackendError> = match request { MaximaLibRequest::LoginRequestOauth | MaximaLibRequest::StartService => { error!("bro tried to log in twice"); Ok(()) } + MaximaLibRequest::GetGamesRequest => { - let channel = backend_responder.clone(); - let channel1 = remote_provider_channel.clone(); let maxima = maxima_arc.clone(); + let responder = backend_responder.clone(); + let remote = remote_provider_channel.clone(); let context = ctx.clone(); - async move { get_games_request(maxima, channel, channel1, &context).await } - .await + tokio::task::spawn(async move { + if let Err(e) = get_games_request(maxima, responder.clone(), remote, &context).await { + let _ = responder.send(MaximaLibResponse::NonFatalError(Box::from(e))).ok(); + } + }); + Ok(()) } MaximaLibRequest::GetFriendsRequest => { - let channel = backend_responder.clone(); - let channel1 = remote_provider_channel.clone(); - let maxima = maxima_arc.clone(); - let context = ctx.clone(); - async move { get_friends_request(maxima, channel, channel1, &context).await } - .await + let f = async || get_friends_request(maxima_arc.clone(), backend_responder.clone(), remote_provider_channel.clone(), ctx).await; + f().await } MaximaLibRequest::GetGameDetailsRequest(slug) => { - let channel = backend_responder.clone(); - let maxima = maxima_arc.clone(); - let context = ctx.clone(); - async move { game_details_request(maxima, slug.clone(), channel, &context).await }.await + let f = async || game_details_request(maxima_arc.clone(), slug, backend_responder.clone(), ctx).await; + f().await } - MaximaLibRequest::LocateGameRequest(path) => { + MaximaLibRequest::LocateGameRequest(mut path) => { #[cfg(unix)] maxima::core::launch::mx_linux_setup().await?; - let mut path = path; - if path.ends_with("/") || path.ends_with("\\") { - path.remove(path.len() - 1); + + // strip trailing slash + if path.ends_with('/') || path.ends_with('\\') { + path.pop(); } + 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; - if let Err(err) = guh { - let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse( - InteractThreadLocateGameResponse::Error( - InteractThreadLocateGameFailure { - reason: err, - xml_path: path - .join(MANIFEST_RELATIVE_PATH) - .to_str() - .unwrap() - .to_string(), - }, - ), - )); - } else { - let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse( - InteractThreadLocateGameResponse::Success, - )); - } - } else { - let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse( - InteractThreadLocateGameResponse::Error( + let manifest_path = path.join(MANIFEST_RELATIVE_PATH); + + let response = match manifest::read(manifest_path.clone()).await { + Ok(manifest) => match manifest.run_touchup(&path).await { + Ok(()) => InteractThreadLocateGameResponse::Success, + Err(err) => InteractThreadLocateGameResponse::Error( InteractThreadLocateGameFailure { - reason: manifest.unwrap_err(), - xml_path: path - .join(MANIFEST_RELATIVE_PATH) - .to_str() - .unwrap() - .to_string(), - }, + reason: err, + xml_path: manifest_path.to_string_lossy().into_owned(), + } ), - )); - } + } + Err(err) => InteractThreadLocateGameResponse::Error( + InteractThreadLocateGameFailure { + reason: err, + xml_path: manifest_path.to_string_lossy().into_owned(), + } + ), + }; + + let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse(response)).ok(); info!("finished locating"); ctx.request_repaint(); Ok(()) } + MaximaLibRequest::InstallGameRequest(offer, path) => { let mut maxima = maxima_arc.lock().await; - let builds = - maxima.content_manager().service().available_builds(&offer).await?; - let build = if let Some(build) = builds.live_build() { - build - } else { - continue; - }; + let builds = maxima.content_manager().service().available_builds(&offer).await?; + let build = builds + .live_build() + .ok_or(BackendError::NoLiveBuildAvailable)?; let game = QueuedGameBuilder::default() .offer_id(offer) .build_id(build.build_id().to_owned()) - .path(path.to_owned()) + .path(path) .build()?; Ok(maxima.content_manager().add_install(game).await?) } MaximaLibRequest::StartGameRequest(info, settings) => { Ok(start_game_request(maxima_arc.clone(), info, settings).await?) } - MaximaLibRequest::ShutdownRequest => break 'outer Ok(()), //TODO: kill the bridge thread + MaximaLibRequest::ShutdownRequest => { + info!("BridgeThread: received shutdown request"); + break 'main Ok(()); + } }; - if let Err(err) = action { - let _ = backend_responder.send(MaximaLibResponse::NonFatalError(Box::from(err))); + + if let Err(err) = result { + let _ = backend_responder + .send(MaximaLibResponse::NonFatalError(Box::from(err))) + .ok(); } puffin::GlobalProfiler::lock().new_frame(); diff --git a/maxima-ui/src/event_thread.rs b/maxima-ui/src/event_thread.rs index 27609108..0c415712 100644 --- a/maxima-ui/src/event_thread.rs +++ b/maxima-ui/src/event_thread.rs @@ -1,8 +1,8 @@ use egui::Context; -use std::sync::mpsc::{Receiver, Sender}; +use tokio::{sync::mpsc::{UnboundedSender, UnboundedReceiver}, time::Duration}; use crate::bridge_thread::BackendError; -use log::info; +use log::{error, info}; use maxima::core::{ LockedMaxima, service_layer::{ @@ -31,17 +31,15 @@ impl EventThread { pub fn new( ctx: &Context, maxima: LockedMaxima, - rtm_cmd_listener: Receiver, - rtm_responder: Sender, + rtm_cmd_listener: UnboundedReceiver, + rtm_responder: UnboundedSender, ) -> Self { let context = ctx.clone(); tokio::task::spawn(async move { - let result = EventThread::run(rtm_cmd_listener, rtm_responder, &context, maxima).await; - if result.is_err() { - panic!("Event thread failed! {}", result.err().unwrap()); - } else { - info!("Event thread shut down") + match EventThread::run(rtm_cmd_listener, rtm_responder, &context, maxima).await { + Ok(()) => info!("Event thread shut down cleanly"), + Err(e) => error!("Event thread error: {e}"), } }); @@ -49,8 +47,8 @@ impl EventThread { } async fn run( - rtm_cmd_listener: Receiver, - rtm_responder: Sender, + mut rtm_cmd_listener: UnboundedReceiver, + rtm_responder: UnboundedSender, ctx: &Context, maxima_arc: LockedMaxima, ) -> Result<(), BackendError> { @@ -79,38 +77,47 @@ impl EventThread { rtm.subscribe(&players).await?; drop(maxima); - 'outer: loop { - let mut maxima = maxima_arc.lock().await; - maxima.rtm().heartbeat().await?; - - { - let store = maxima.rtm().presence_store().lock().await; - for entry in store.iter() { - let _ = rtm_responder.send(MaximaEventResponse::FriendStatusResponse( - EventThreadFriendStatusResponse { - id: entry.0.to_string(), - presence: entry.1, - }, - )); - // This can cause excessive repainting if it keeps updating friends we know about - egui::Context::request_repaint(&ctx); - } - } + let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(30)); + heartbeat_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - drop(maxima); + // Presence polling interval — separate from heartbeat + let mut presence_interval = tokio::time::interval(Duration::from_millis(500)); + presence_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - let request = rtm_cmd_listener.try_recv(); - if request.is_err() { - tokio::time::sleep(std::time::Duration::from_millis(500)).await; - continue; - } + loop { + tokio::select! { + _ = heartbeat_interval.tick() => { + let mut maxima = maxima_arc.lock().await; + if let Err(e) = maxima.rtm().heartbeat().await { + error!("RTM heartbeat failed: {e}"); + } + } - match request? { - MaximaEventRequest::SubscribeToFriendPresence => {} - MaximaEventRequest::ShutdownRequest => break 'outer Ok(()), - } + _ = presence_interval.tick() => { + let mut maxima = maxima_arc.lock().await; + let store = maxima.rtm().presence_store().lock().await; + for entry in store.iter() { + let _ = rtm_responder + .send(MaximaEventResponse::FriendStatusResponse( + EventThreadFriendStatusResponse { + id: entry.0.to_string(), + presence: entry.1, + }, + )) + .ok(); + } + if store.entry_count() > 0 { + ctx.request_repaint(); + } + } - tokio::time::sleep(std::time::Duration::from_millis(500)).await; + request = rtm_cmd_listener.recv() => { + match request { + Some(MaximaEventRequest::ShutdownRequest) | None => return Ok(()), + Some(MaximaEventRequest::SubscribeToFriendPresence) => {} + } + } + } } } -} +} \ No newline at end of file diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index e614ac30..04f309f1 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -11,7 +11,7 @@ use log::error; use maxima::{core::library::OwnedOffer, util::log::init_logger}; use std::{collections::HashMap, ops::RangeInclusive, path::PathBuf, sync::Arc}; use strum_macros::EnumIter; -use ui_image::{UIImageCache, UIImageType}; +use crate::ui_image::{UIImageCache, UIImageType}; use views::{ debug_view::debug_view, downloads_view::{QueuedDownload, downloads_view}, @@ -27,11 +27,10 @@ use eframe::egui_glow; use egui_extras::{Size, StripBuilder}; use egui_glow::glow; -use app_bg_renderer::AppBgRenderer; -use bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameResponse}; -use game_view_bg_renderer::GameViewBgRenderer; -use renderers::{app_bg_renderer, game_view_bg_renderer}; -use translation_manager::{TranslationManager, positional_replace}; +use crate::renderers::app_bg_renderer::AppBgRenderer; +use crate::bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameResponse}; +use crate::renderers::game_view_bg_renderer::GameViewBgRenderer; +use crate::translation_manager::{TranslationManager, positional_replace}; pub mod bridge; pub mod util; @@ -311,6 +310,8 @@ pub struct MaximaEguiApp { installer_state: InstallModalState, /// User Settings for the frontend settings: FrontendSettings, + /// Map of offer IDs to slugs for quick lookup + pub offer_to_slug: HashMap, } #[derive(serde::Serialize, serde::Deserialize, PartialEq, EnumIter)] @@ -489,6 +490,7 @@ impl MaximaEguiApp { install_queue: HashMap::new(), installer_state: InstallModalState::new(&settings), settings, + offer_to_slug: HashMap::new(), } } } @@ -906,7 +908,9 @@ 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(); + let _ = self.backend.backend_commander.send( + bridge_thread::MaximaLibRequest::LocateGameRequest(self.installer_state.locate_path.clone()), + ); self.installer_state.locating = true; } }); @@ -931,7 +935,9 @@ 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(); + let _ = self.backend.backend_commander.send( + bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), path.join(slug)), + ); clear = true; } @@ -986,10 +992,7 @@ impl MaximaEguiApp { None }; let _ = self.backend.backend_commander.send( - crate::bridge_thread::MaximaLibRequest::StartGameRequest( - game.clone(), - settings, - ), + crate::bridge_thread::MaximaLibRequest::StartGameRequest(game.clone(), settings), ); clear = true } @@ -1029,10 +1032,9 @@ impl MaximaEguiApp { ) .clicked() { - self.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::LoginRequestOauth) - .unwrap(); + let _ = self.backend.backend_commander.send( + bridge_thread::MaximaLibRequest::LoginRequestOauth, + ); self.backend_state = BackendStallState::LoggingIn; } } @@ -1149,10 +1151,9 @@ impl MaximaEguiApp { ) .clicked() { - self.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::StartService) - .unwrap(); + self.backend.backend_commander.send( + bridge_thread::MaximaLibRequest::StartService, + ).unwrap_or(()); self.backend_state = BackendStallState::Starting; } } @@ -1182,10 +1183,9 @@ impl eframe::App for MaximaEguiApp { } fn on_exit(&mut self, _gl: Option<&glow::Context>) { - self.backend + let _ = self.backend .backend_commander - .send(bridge_thread::MaximaLibRequest::ShutdownRequest) - .unwrap(); + .send(bridge_thread::MaximaLibRequest::ShutdownRequest); } fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { diff --git a/maxima-ui/src/ui_image.rs b/maxima-ui/src/ui_image.rs index d0bbd002..a6b8170c 100644 --- a/maxima-ui/src/ui_image.rs +++ b/maxima-ui/src/ui_image.rs @@ -7,14 +7,14 @@ use std::{ path::PathBuf, sync::{ Arc, Mutex, - mpsc::{Receiver, Sender}, }, }; use tokio::{fs::File, io}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender, error::TryRecvError}; use log::{debug, error, info}; -use image::io::Reader as ImageReader; +use image::ImageReader; use maxima::util::native::{NativeError, SafeStr, maxima_dir}; #[derive(Clone, PartialEq, Eq, Hash, std::fmt::Debug)] @@ -42,7 +42,7 @@ impl Display for UIImageType { pub struct UIImageCache { cache: Arc>>>, // none represents loading, lack of represents untouched - commander: Sender, + commander: UnboundedSender, pub placeholder_avatar: TextureHandle, } @@ -77,9 +77,9 @@ pub enum ImageLoadError { } impl UIImageCache { - pub fn new(ctx: egui::Context) -> (Self, Sender) { + pub fn new(ctx: egui::Context) -> (Self, UnboundedSender) { let cache = Arc::new(Mutex::new(HashMap::new())); - let (tx, rx) = std::sync::mpsc::channel::(); + let (tx, rx) = unbounded_channel::(); let tx1 = tx.clone(); let yeah = load_image_bytes(include_bytes!("../res/usericon_tmp.png")).unwrap(); @@ -201,17 +201,18 @@ impl UIImageCache { async fn run( context: egui::Context, - commander: Receiver, + mut commander: UnboundedReceiver, cache: Arc>>>, ) { let mut remotes: HashMap = HashMap::new(); 'outer: loop { match commander.try_recv() { - Err(error) => { - if error == std::sync::mpsc::TryRecvError::Disconnected { - break 'outer; - } + Err(TryRecvError::Disconnected) => { + break 'outer; + } + Err(TryRecvError::Empty) => { + tokio::task::yield_now().await; } Ok(request) => match request { UIImageCacheLoaderCommand::ProvideRemote(needle, target) => { diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index 3fcc624c..57185eab 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -212,7 +212,7 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { .send(bridge_thread::MaximaLibRequest::GetGameDetailsRequest( game.slug.clone(), )) - .unwrap(); + .unwrap_or_else(|_| ()); app.games.get_mut(&app.game_sel).unwrap().details = GameDetailsWrapper::Loading; None } diff --git a/maxima-ui/src/widgets/enum_dropdown.rs b/maxima-ui/src/widgets/enum_dropdown.rs index c537b994..7f007223 100644 --- a/maxima-ui/src/widgets/enum_dropdown.rs +++ b/maxima-ui/src/widgets/enum_dropdown.rs @@ -1,4 +1,4 @@ -use egui::{Color32, CornerRadius, Rounding, Stroke}; +use egui::{Color32, CornerRadius, Stroke}; use crate::enum_locale_map::EnumToString; use strum::IntoEnumIterator; From feed237a1f61ba6b3b1a52e57277bbbaf553933f Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:53:24 +0200 Subject: [PATCH 11/57] add fr-FR, small changes --- maxima-lib/src/core/manifest/dip.rs | 266 +++++++++++++++++------- maxima-lib/src/core/manifest/mod.rs | 10 +- maxima-lib/src/core/manifest/pre_dip.rs | 199 ++++++++++++------ maxima-service/src/service/mod.rs | 6 +- maxima-tui/src/main.rs | 5 +- maxima-tui/src/service.rs | 16 +- maxima-ui/res/locale/en_us.json | 3 +- maxima-ui/res/locale/fr_fr.json | 141 +++++++++++++ maxima-ui/src/bridge_processor.rs | 229 +++++++++----------- maxima-ui/src/bridge_thread.rs | 4 - maxima-ui/src/enum_locale_map.rs | 1 + maxima-ui/src/main.rs | 1 + maxima-ui/src/translation_manager.rs | 10 +- 13 files changed, 589 insertions(+), 302 deletions(-) create mode 100644 maxima-ui/res/locale/fr_fr.json diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 2761ee27..9122be28 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -6,6 +6,47 @@ use crate::{core::manifest::ManifestError, util::native::platform_path}; use derive_getters::Getters; use serde::Deserialize; +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct DiPContentIDs { + #[serde(rename = "contentID", default)] + pub items: Vec, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct DiPUninstall { + pub path: String, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct DiPInstallManifest { + #[serde(rename = "filePath")] + pub file_path: String, +} + +/// SimCity 3000 Unlimited +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct DiPGameTitle { + #[serde(rename = "@locale")] + pub locale: String, + #[serde(rename = "$text")] + pub title: String, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct DiPGameTitles { + #[serde(rename = "gameTitle", default)] + pub items: Vec, +} + +/// SimCity 3000 Unlimited Launcher +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct DiPName { + #[serde(rename = "@locale")] + pub locale: String, + #[serde(rename = "$text")] + pub value: String, +} + macro_rules! dip_type { ( $(#[$message_attr:meta])* @@ -24,10 +65,8 @@ macro_rules! dip_type { } ) => { pastey::paste! { - // Main struct definition $(#[$message_attr])* #[derive(Default, Debug, Clone, Deserialize, PartialEq, Getters)] - #[serde(rename_all = "camelCase")] pub struct [] { $( $(#[$attr_field_attr])* @@ -43,31 +82,21 @@ macro_rules! dip_type { } } -dip_type!( - Launcher; - attr { - uid: String, - }, - data { - file_path: String, - execute_elevated: Option, - #[serde(default)] - trial: bool, - } -); dip_type!( FeatureFlags; attr { - allowMultipleInstances: bool, - autoUpdateEnabled: bool, - dynamicContentSupportEnabled: bool, - enableDifferentialUpdate: bool, - enableOriginInGameAPI: bool, - forceTouchupInstallerAfterUpdate: bool, - languageChangeSupportEnabled: bool, - treatUpdatesAsMandatory: bool, - useGameVersionFromManifest: bool, + allowMultipleInstances: String, + autoUpdateEnabled: String, + dynamicContentSupportEnabled: String, + enableDifferentialUpdate: String, + enableOriginInGameAPI: String, + #[serde(default)] + enableVersionUpdate: String, + forceTouchupInstallerAfterUpdate: String, + languageChangeSupportEnabled: String, + treatUpdatesAsMandatory: String, + useGameVersionFromManifest: String, }, data {} ); @@ -83,8 +112,10 @@ dip_type!( dip_type!( Requirements; attr { + #[serde(default)] + clientVersion: String, osMinVersion: String, - osReqs64Bit: bool, + osReqs64Bit: String, }, data {} ); @@ -99,10 +130,31 @@ dip_type!( } ); +dip_type!( + Launcher; + attr { + uid: String, + }, + data { + #[serde(default)] + name: Vec, + filePath: String, + #[serde(default)] + parameters: Option, + #[serde(default)] + requires64BitOS: String, + #[serde(default)] + trial: String, + #[serde(default)] + requiresMetal: String, + } +); + dip_type!( Runtime; attr {}, data { + #[serde(default)] launcher: Vec, } ); @@ -111,29 +163,15 @@ dip_type!( Touchup; attr {}, data { - file_path: String, + filePath: String, parameters: String, + #[serde(default)] + updateParameters: String, + #[serde(default)] + repairParameters: String, } ); -fn remove_leading_slash(path: &str) -> &str { - path.strip_prefix('/').unwrap_or(path) -} - -fn remove_trailing_slash(path: &str) -> &str { - path.strip_suffix('/').unwrap_or(path) -} - -fn remove_trailing_backslash(path: &str) -> &str { - path.strip_suffix('\\').unwrap_or(path) -} - -impl DiPTouchup { - pub fn path(&self) -> &str { - remove_leading_slash(&self.file_path) - } -} - dip_type!( Manifest; attr { @@ -141,8 +179,16 @@ dip_type!( }, data { buildMetaData: DiPBuildMetaData, + #[serde(rename = "contentIDs", default)] + contentIds: DiPContentIDs, + #[serde(default)] + gameTitles: DiPGameTitles, + #[serde(default)] + uninstall: DiPUninstall, runtime: DiPRuntime, touchup: DiPTouchup, + #[serde(default)] + installManifest: DiPInstallManifest, } ); @@ -154,6 +200,48 @@ dip_type!( } ); + +fn remove_leading_slash(path: &str) -> &str { + path.strip_prefix('/').unwrap_or(path) +} + +fn remove_trailing_slash(path: &str) -> &str { + path.strip_suffix('/').unwrap_or(path) +} + +fn remove_trailing_backslash(path: &str) -> &str { + path.strip_suffix('\\').unwrap_or(path) +} + +fn split_args(s: &str) -> Result, ManifestError> { + let mut args = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + + for ch in s.chars() { + match ch { + '"' => in_quotes = !in_quotes, + ' ' if !in_quotes => { + if !current.is_empty() { + args.push(current.clone()); + current.clear(); + } + } + _ => current.push(ch), + } + } + + if in_quotes { + return Err(ManifestError::Decode); + } + + if !current.is_empty() { + args.push(current); + } + + Ok(args) +} + /// https://www.reddit.com/r/rust/comments/11co87m/comment/ja4sy88 fn bytes_to_string(bytes: Vec) -> Option { if let Ok(v) = String::from_utf8(bytes.clone()) { @@ -165,28 +253,67 @@ fn bytes_to_string(bytes: Vec) -> Option { .map(|a| u16::from_ne_bytes([a[0], a[1]])) .collect(); - if let Ok(v) = String::from_utf16(&u16_bytes) { - return Some(v); + String::from_utf16(&u16_bytes).ok() +} + +impl DiPTouchup { + pub fn path(&self) -> &str { + remove_leading_slash(&self.filePath) } +} - None +impl DiPLauncher { + pub fn is_trial(&self) -> bool { + self.trial == "1" || self.trial.eq_ignore_ascii_case("true") + } + + pub fn display_name(&self, locale: &str) -> Option<&str> { + self.name + .iter() + .find(|n| n.locale == locale) + .or_else(|| self.name.iter().find(|n| n.locale == "en_US")) + .map(|n| n.value.as_str()) + } } impl DiPManifest { - pub async fn read(path: &PathBuf) -> Result { + pub async fn read(path: &Path) -> Result { let bytes = tokio::fs::read(path).await?; let string = bytes_to_string(bytes).ok_or(ManifestError::Decode)?; - + Ok(quick_xml::de::from_str(&string)?) } - pub fn execute_path(&self, trial: bool) -> Option { - let launcher = self.runtime.launcher.iter().find(|l| l.trial == trial); - launcher.map(|l| l.file_path.clone()) + pub fn version(&self) -> &str { + &self.buildMetaData.gameVersion.attr_version + } + + pub fn execute_path(&self, trial: bool) -> Option<&str> { + self.runtime + .launcher + .iter() + .find(|l| l.is_trial() == trial) + .map(|l| l.filePath.as_str()) } - pub fn version(&self) -> Option { - Some(self.buildMetaData.gameVersion.attr_version().clone()) + fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { + let install_str = platform_path( + remove_trailing_backslash( + install_path.to_str().ok_or(ManifestError::Decode)? + ) + .replace('/', "\\"), + ) + .to_str() + .ok_or(ManifestError::Decode)? + .to_string(); + + let expanded = self + .touchup + .parameters + .replace("{locale}", "en_US") + .replace("{installLocation}", &install_str); + + split_args(&expanded) } #[cfg(unix)] @@ -205,10 +332,9 @@ impl DiPManifest { install_path.to_str().ok_or(ManifestError::Decode)?, )); 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?; + let path = case_insensitive_path(install_path.join(self.touchup.path())); + run_wine_command(path, Some(args), None, true, CommandType::Run).await?; invalidate_mx_wine_registry().await; Ok(()) } @@ -221,10 +347,12 @@ impl DiPManifest { let args = self.collect_touchup_args(install_path)?; let path = install_path.join(self.touchup.path()); - let mut binding = Command::new(path); - let child = binding.args(args); + let status = Command::new(&path) + .args(&args) + .spawn()? + .wait() + .await?; - let status = child.spawn()?.wait().await?; if !status.success() { return Err(ManifestError::Native(NativeError::Command( status.code().unwrap_or(0), @@ -233,22 +361,4 @@ impl DiPManifest { Ok(()) } - - fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { - let mut args = Vec::new(); - for arg in self.touchup.parameters.split(" ") { - let arg = arg.replace("{locale}", "en_US").replace( - "\"{installLocation}\"", - platform_path( - remove_trailing_backslash(install_path.to_str().ok_or(ManifestError::Decode)?) - .replace("/", "\\"), - ) - .to_str() - .ok_or(ManifestError::Decode)?, - ); - - args.push(PathBuf::from(arg)); - } - Ok(args) - } -} +} \ No newline at end of file diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index dbfa2be1..460e4367 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -47,11 +47,11 @@ impl GameManifest for DiPManifest { } fn execute_path(&self, trial: bool) -> Option { - self.execute_path(trial) + self.execute_path(trial).map(std::string::ToString::to_string) } fn version(&self) -> Option { - self.version() + Some(self.version().to_string()) } } @@ -63,11 +63,11 @@ impl GameManifest for PreDiPManifest { } fn execute_path(&self, _: bool) -> Option { - None // pre-dip games don't have an exe field, most if not all just use info in the offer + None // pre-dip games don't have an exe field, usually they put the exe in the "exclude" section for some reason } - fn version(&self) -> Option { - self.version() + fn version(&self) -> std::option::Option { + Some(self.version().to_string()) } } diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 43ac871b..15f093b0 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -5,6 +5,23 @@ use derive_getters::Getters; use serde::Deserialize; use std::path::{Path, PathBuf}; +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct PreDiPContentIDs { + #[serde(rename = "contentID", default)] + pub items: Vec, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct PreDiPUninstall { + pub path: String, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct PreDiPInstallManifest { + #[serde(rename = "filePath")] + pub file_path: String, +} + macro_rules! predip_type { ( $(#[$message_attr:meta])* @@ -23,10 +40,8 @@ macro_rules! predip_type { } ) => { pastey::paste! { - // Main struct definition $(#[$message_attr])* #[derive(Default, Debug, Clone, Deserialize, PartialEq, Getters)] - #[serde(rename_all = "camelCase")] pub struct [] { $( $(#[$attr_field_attr])* @@ -42,14 +57,47 @@ macro_rules! predip_type { } } +predip_type!( + FeatureFlags; + attr { + forceTouchupInstallerAfterUpdate: String, + autoUpdateEnabled: String, + treatUpdatesAsMandatoryEnabled: String, + useGameVersionFromManifestEnabled: String, + UnableDifferentialUpdate: String, + }, + data {} +); + +predip_type!( + Os; + attr { + minVersion: String, + }, + data {} +); + predip_type!( Executable; attr {}, data { - file_path: String, + filePath: String, parameters: String, - update_parameters: String, - repair_parameters: String, + updateParameters: String, + repairParameters: String, + } +); + +predip_type!( + LocaleInfo; + attr { + locale: String, + }, + data { + title: String, + eula: String, + #[serde(default)] + exclude: Vec, } ); @@ -57,26 +105,28 @@ predip_type!( Metadata; attr {}, data { - feature_flags: Vec, - os: String, + featureFlags: PreDiPFeatureFlags, + os: PreDiPOs, + #[serde(default)] ignore: Vec, - locale_info: Vec, - file_path: String, - parameters: String, - update_parameters: String, - repair_parameters: String, + #[serde(default)] + localeInfo: Vec, } ); predip_type!( - LocaleInfo; + Manifest; attr { - locale: String, + gameVersion: String, + manifestVersion: String, }, data { - title: String, - eula: String, - exclude: Vec, + #[serde(rename = "contentIDs")] + contentIds: PreDiPContentIDs, + uninstall: PreDiPUninstall, + metadata: PreDiPMetadata, + executable: PreDiPExecutable, + installManifest: PreDiPInstallManifest, } ); @@ -92,21 +142,6 @@ fn remove_trailing_backslash(path: &str) -> &str { path.strip_suffix('\\').unwrap_or(path) } -predip_type!( - Manifest; - attr { - game_version: String, - manifest_version: String, - }, - data { - content_ids: Vec, - uninstall: String, - metadata: Vec, - executable: PreDiPExecutable, - install_manifest: String, - } -); - /// https://www.reddit.com/r/rust/comments/11co87m/comment/ja4sy88 fn bytes_to_string(bytes: Vec) -> Option { if let Ok(v) = String::from_utf8(bytes.clone()) { @@ -118,23 +153,69 @@ fn bytes_to_string(bytes: Vec) -> Option { .map(|a| u16::from_ne_bytes([a[0], a[1]])) .collect(); - if let Ok(v) = String::from_utf16(&u16_bytes) { - return Some(v); - } - - None + String::from_utf16(&u16_bytes).ok() } + impl PreDiPManifest { - pub async fn read(path: &PathBuf) -> Result { + pub async fn read(path: &Path) -> Result { let bytes = tokio::fs::read(path).await?; let string = bytes_to_string(bytes).ok_or(ManifestError::Decode)?; - Ok(quick_xml::de::from_str(&string)?) } - pub fn version(&self) -> Option { - Some(self.attr_game_version.clone()) + pub fn version(&self) -> &str { + &self.attr_gameVersion + } + + fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { + let install_str = platform_path( + remove_trailing_backslash( + install_path.to_str().ok_or(ManifestError::Decode)? + ) + .replace('/', "\\"), + ) + .to_str() + .ok_or(ManifestError::Decode)? + .to_string(); + + let expanded = self + .executable + .parameters + .replace("{locale}", "en_US") + .replace("{installLocation}", &install_str); + + Self::split_args(&expanded) + } + + fn split_args(s: &str) -> Result, ManifestError> { + let mut args = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + + for ch in s.chars() { + match ch { + '"' => in_quotes = !in_quotes, + ' ' if !in_quotes => { + if !current.is_empty() { + args.push(current.clone()); + current.clear(); + } + } + _ => current.push(ch), + } + } + + if in_quotes { + // Unterminated quote — malformed manifest + return Err(ManifestError::Decode); + } + + if !current.is_empty() { + args.push(current); + } + + Ok(args) } #[cfg(unix)] @@ -153,11 +234,11 @@ impl PreDiPManifest { install_path.to_str().ok_or(ManifestError::Decode)?, )); let args = self.collect_touchup_args(&install_path)?; + let path = case_insensitive_path( + install_path.join(remove_leading_slash(&self.executable.filePath)) + ); - 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?; - invalidate_mx_wine_registry().await; Ok(()) } @@ -168,12 +249,14 @@ impl PreDiPManifest { use tokio::process::Command; let args = self.collect_touchup_args(install_path)?; - let path = install_path.join(&self.executable.file_path); + let path = install_path.join(remove_leading_slash(&self.executable.filePath)); - let mut binding = Command::new(path); - let child = binding.args(args); + let status = Command::new(&path) + .args(&args) + .spawn()? + .wait() + .await?; - let status = child.spawn()?.wait().await?; if !status.success() { return Err(ManifestError::Native(NativeError::Command( status.code().unwrap_or(0), @@ -182,22 +265,4 @@ impl PreDiPManifest { Ok(()) } - - fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { - let mut args = Vec::new(); - for arg in self.executable.parameters.split(" ") { - let arg = arg.replace("{locale}", "en_US").replace( - "\"{installLocation}\"", - platform_path( - remove_trailing_backslash(install_path.to_str().ok_or(ManifestError::Decode)?) - .replace("/", "\\"), - ) - .to_str() - .ok_or(ManifestError::Decode)?, - ); - - args.push(PathBuf::from(arg)); - } - Ok(args) - } -} +} \ No newline at end of file diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index c27d4515..90215d0b 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -6,7 +6,7 @@ use maxima::util::registry::set_up_registry; use maxima::util::service::SERVICE_NAME; use std::ffi::OsString; use std::path::Path; -use std::sync::mpsc::{self, Receiver}; +use tokio::sync::mpsc::{self, Receiver}; use std::thread; use std::time::Duration; use structured_logger::json::new_writer; @@ -36,7 +36,7 @@ fn service_main(arguments: Vec) { } fn bootstrap_service(_arguments: Vec) -> windows_service::Result<()> { - let (shutdown_tx, shutdown_rx) = mpsc::channel(); + let (shutdown_tx, shutdown_rx) = mpsc::channel(1); let event_handler = move |control_event| -> ServiceControlHandlerResult { match control_event { @@ -118,7 +118,7 @@ async fn req_inject_library(body: web::Bytes) -> Result) -> Result<(), self::ServerError> { +fn run_service(mut shutdown_rx: Receiver<()>) -> Result<(), self::ServerError> { let log_path = Path::new("C:/ProgramData/Maxima/Logs/MaximaBackgroundService.log"); std::fs::create_dir_all(log_path.safe_parent()?)?; let log_file = File::create(log_path)?; diff --git a/maxima-tui/src/main.rs b/maxima-tui/src/main.rs index 1a982d53..b596b0eb 100644 --- a/maxima-tui/src/main.rs +++ b/maxima-tui/src/main.rs @@ -112,7 +112,10 @@ impl App { } fn run(&mut self, terminal: &mut Terminal) -> Result<()> { - self.bridge.tx.send(MaximaLibRequest::LoginRequest).unwrap(); + let tx = self.bridge.tx.clone(); + tokio::spawn(async move { + tx.send(MaximaLibRequest::LoginRequest).await.ok(); + }); while self.state == AppState::Running { self.draw(terminal)?; diff --git a/maxima-tui/src/service.rs b/maxima-tui/src/service.rs index 657a8aa9..2220df0d 100644 --- a/maxima-tui/src/service.rs +++ b/maxima-tui/src/service.rs @@ -1,4 +1,4 @@ -use std::sync::mpsc::{self, Receiver, Sender}; +use tokio::sync::mpsc::{self, Receiver, Sender}; use anyhow::{Result, bail}; use log::info; @@ -43,8 +43,8 @@ pub struct BridgeThread { impl BridgeThread { pub fn new() -> Self { - let (tx0, rx1) = mpsc::channel(); - let (tx1, rx0) = mpsc::channel(); + let (tx0, rx1) = mpsc::channel(1); + let (tx1, rx0) = mpsc::channel(1); tokio::task::spawn(async move { let die_fallback = tx1.clone(); @@ -61,7 +61,7 @@ impl BridgeThread { } async fn run( - rx1: Receiver, + mut rx1: Receiver, tx1: Sender, ) -> Result<()> { let maxima_arc: LockedMaxima = Maxima::new_with_options( @@ -91,9 +91,9 @@ impl BridgeThread { success: true, name: user.player().as_ref().unwrap().display_name().to_owned(), }); - tx1.send(message)?; + tx1.send(message).await.ok(); } else { - tx1.send(MaximaLibResponse::LoginCacheEmpty)?; + tx1.send(MaximaLibResponse::LoginCacheEmpty).await.ok(); } } @@ -110,7 +110,7 @@ impl BridgeThread { if !auth_storage.logged_in().await? { let res = login_flow().await?; - auth_storage.add_account(&res); + auth_storage.add_account(&res).await?; } tx1.send(MaximaLibResponse::LoginResponse(InteractThreadLoginResponse { @@ -123,7 +123,7 @@ impl BridgeThread { .unwrap() .display_name() .to_owned(), - }))?; + })).await.ok(); Ok(()) }; diff --git a/maxima-ui/res/locale/en_us.json b/maxima-ui/res/locale/en_us.json index 22d19a29..5992187f 100644 --- a/maxima-ui/res/locale/en_us.json +++ b/maxima-ui/res/locale/en_us.json @@ -104,7 +104,8 @@ }, "locale" : { "default": "System Default", - "en_us": "English" + "en_us": "English", + "fr_fr": "French" }, "modals" : { "close": "Close", diff --git a/maxima-ui/res/locale/fr_fr.json b/maxima-ui/res/locale/fr_fr.json new file mode 100644 index 00000000..36b6010c --- /dev/null +++ b/maxima-ui/res/locale/fr_fr.json @@ -0,0 +1,141 @@ +{ + "errors" : { + "view_not_impl" : "Vue non implémentée", + "view_coming_soon": "Bientôt disponible !", + "critical_thread_crashed" : "Un thread critique en arrière-plan a planté, et l'application ne peut pas continuer à fonctionner." + }, + "startup_flow": { + "starting": "Démarrage...", + "logging_in": "Connexion en cours...", + "login_header": "Vous n'êtes pas connecté.", + "login_button": "Se connecter avec EA", + "service_installer_header": "Configuration du service requise", + "service_installer_description": "Maxima doit installer un service Windows pour effectuer certaines tâches en arrière-plan. Vous serez invité à fournir des droits d'administrateur.", + "service_installer_button": "Installer le service" + }, + "menubar" : { + "games" : "Jeux", + "store" : "Boutique", + "friends" : "Amis", + "settings" : "Paramètres", + "downloads" : "Téléchargements" + }, + "profile_menu" : { + "view_profile" : "Voir le profil", + "view_wishlist" : "Voir la liste de souhaits" + }, + "games_view" : { + "toolbar" : { + "genre_filter" : "Afficher", + "genre_options" : { + "all" : "Tous les genres", + "shooter" : "Tireur", + "simulation" : "Simulation" + }, + "platform_filter" : "Plateforme" , + "platform_options" : { + "all" : "Toutes les plateformes", + "windows" : "Windows", + "mac" : "Mac" + }, + "search_bar_hint" : "Rechercher des jeux", + "running_suffix" : "En cours", + "out_of_date_suffix" : "Mise à jour disponible" + }, + "main" : { + "play" : "▶ Jouer", + "stop" : "⏹ Arrêter", + "pause" : "⏸ Pause", + "resume" : "➡ Reprendre", + "install": "⮋ Installer", + "uninstall" : "⛼ Désinstaller", + "settings" : "⛭ Paramètres", + "playtime" : "Temps de jeu", + "achievements" : "Succès", + "no_loaded_games" : "Vous n'avez aucun jeu, ou ils ne sont pas encore chargés." + }, + "details" : { + "min_system_req" : "Configuration minimale requise", + "rec_system_req" : "Configuration recommandée" + } + }, + "friends_view" : { + "toolbar" : { + "online" : "En ligne", + "all" : "Tous", + "pending" : "En attente", + "blocked" : "Bloqués", + "add_friend" : "Ajouter un ami", + "filter_options" : { + "name" : "Nom", + "game" : "Jeu" + }, + "search_hint" : "Rechercher des amis" + }, + "friend_actions" : { + "profile" : "Profil", + "chat" : "Discussion", + "unfriend" : "Supprimer" + }, + "status" : { + "unknown" : "Inconnu", + "do_not_disturb" : "Ne pas déranger", + "away" : "Absent", + "online" : "En ligne", + "offline" : "Hors ligne", + "presence_basic" : "Sur {game}", + "presence_rich" : "Sur {game} : {rich}" + } + }, + "settings_view": { + "interface" : { + "header": "Interface", + "language": "Langue" + }, + "game_installation" : { + "header": "Installation du jeu", + "default_folder": "Dossier d'installation par défaut", + "ignore_ood_warning": "Ignorer l'avertissement de lancement de jeu obsolète" + }, + "performance" : { + "header": "Performance", + "disable_blur": "Désactiver les effets de flou" + } + }, + "locale" : { + "default": "Par défaut du système", + "en_us": "Anglais", + "fr_fr": "Français" + }, + "modals" : { + "close": "Fermer", + "game_install" : { + "header": "Installer {game}", + "locate_installed": "Localiser une installation existante du jeu", + "locate_action": "Localiser", + "locate_in_progress": "Localisation en cours...", + "locate_failed": "Échec de la localisation.", + "fresh_download": "Installer une nouvelle copie", + "fresh_path_confirmation": "Le jeu sera installé à :", + "fresh_path_invalid": "Chemin invalide", + "fresh_action": "Installer" + }, + "game_settings" : { + "header": "Paramètres du jeu pour {game}", + "not_installed": "Le jeu n'est pas installé", + "cloud_saves": "Sauvegardes dans le cloud", + "launch_arguments": "Arguments de lancement", + "executable_override": "Remplacement de l'exécutable", + "uninstall": "Désinstaller", + "version": "Version {version}" + }, + "game_launch_out_of_date" : { + "header" : "Lancement d'un jeu obsolète", + "warning" : "Vous lancez une version plus ancienne de {gamename} que la version actuelle. Les fonctionnalités en ligne peuvent ne pas être disponibles.", + "really_warning" : "{gamename} a demandé que les mises à jour soient obligatoires. Vous pouvez choisir d'ignorer cela, mais procédez avec prudence. Lancez à vos risques et périls, et sauvegardez vos données.", + "comparison": "Installé : {local}, Dernier : {online}", + "ok_i_get_it" : "Ne plus m'avertir", + "launch" : "Lancer quand même" + } + } +} diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index db92005a..60a293e3 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -3,11 +3,9 @@ use crate::{ views::downloads_view::QueuedDownload, BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, }; -use log::{info, warn}; +use log::{error, info, warn}; use tokio::sync::mpsc::error::TryRecvError; -const MAX_MESSAGES_PER_FRAME: usize = 64; - pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { puffin::profile_function!(); @@ -15,148 +13,115 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { return; } - let mut needs_repaint = false; - - for _ in 0..MAX_MESSAGES_PER_FRAME { + 'outer: loop { match app.backend.backend_listener.try_recv() { - Ok(response) => { - needs_repaint = true; - handle_backend_response(app, response); - if app.critical_error.is_some() { - break; + Ok(result) => { + use bridge_thread::MaximaLibResponse::*; + match result { + LoginResponse(res) => { + if let Err(error) = &res { + warn!("Login failed. {}", error); + continue; + } + let res = res.unwrap(); + + info!("Logged in as {}!", &res.you.display_name()); + app.user_name = res.you.display_name().clone(); + app.user_id = res.you.id().clone(); + app.backend_state = BackendStallState::BingChilling; + let _ = app.backend.backend_commander + .send(bridge_thread::MaximaLibRequest::GetGamesRequest); + let _ = app.backend.backend_commander + .send(bridge_thread::MaximaLibRequest::GetFriendsRequest); + } + LoginCacheEmpty => app.backend_state = BackendStallState::UserNeedsToLogIn, + ServiceNeedsStarting => { + app.backend_state = BackendStallState::UserNeedsToInstallService + } + ServiceStarted => app.backend_state = BackendStallState::Starting, + GameInfoResponse(res) => { + app.games.insert(res.game.slug.clone(), res.game); + } + GameDetailsResponse(res) => { + let response = res.response; + if let Some(game) = app.games.get_mut(&res.slug) { + game.details = GameDetailsWrapper::Available(GameDetails { + time: response.time, + achievements_unlocked: response.achievements_unlocked, + achievements_total: response.achievements_total, + path: response.path.clone(), + system_requirements_min: response.system_requirements_min.clone(), + system_requirements_rec: response.system_requirements_rec.clone(), + }); + } + } + FriendInfoResponse(res) => app.friends.push(res.friend), + CriticalError(err) => app.critical_error = Some(*err), + NonFatalError(err) => app.nonfatal_errors.push(*err), + ActiveGameChanged(slug) => app.playing_game = slug, + LocateGameResponse(res) => { + if matches!(res, bridge_thread::InteractThreadLocateGameResponse::Success) { + let _ = app.backend.backend_commander + .send(bridge_thread::MaximaLibRequest::GetGamesRequest); + } + app.installer_state.locate_response = Some(res); + app.installer_state.locating = false; + } + DownloadProgressChanged(offer_id, progress) => { + if let Some(dl_ing) = app.installing_now.as_mut() { + if dl_ing.offer == offer_id { + dl_ing.downloaded_bytes = progress.bytes; + dl_ing.total_bytes = progress.bytes_total; + } + } + } + DownloadFinished(_) => {} + DownloadQueueUpdate(current, queue) => { + if let Some(current) = current { + if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { + app.installing_now = Some(QueuedDownload { + slug: find_slug_for_offer(&app.games, ¤t), + offer: current, + downloaded_bytes: 0, + total_bytes: 0, + }); + } + } else { + app.installing_now = None; + } + + app.install_queue.clear(); + for offer in queue { + app.install_queue.insert( + offer.clone(), + QueuedDownload { + slug: find_slug_for_offer(&app.games, &offer), + offer, + downloaded_bytes: 0, + total_bytes: 0, + }, + ); + } + } } + ctx.request_repaint(); } - Err(TryRecvError::Empty) => break, + Err(TryRecvError::Empty) => break 'outer, Err(TryRecvError::Disconnected) => { app.critical_error = Some(BackendError::ChannelDisconnected); - break; + break 'outer; } } } - - if needs_repaint { - ctx.request_repaint(); - } -} - -fn send_request(app: &mut MaximaEguiApp, request: bridge_thread::MaximaLibRequest) { - if app.backend.backend_commander.send(request).is_err() { - warn!("Bridge thread disconnected during send"); - app.critical_error = Some(BackendError::ChannelDisconnected); - } } -fn find_slug_for_offer(games: &std::collections::HashMap, offer: &str) -> String { +fn find_slug_for_offer( + games: &std::collections::HashMap, + offer: &str, +) -> String { games .iter() .find(|(_, g)| g.offer == offer) .map(|(slug, _)| slug.clone()) .unwrap_or_default() -} - -fn handle_backend_response(app: &mut MaximaEguiApp, response: bridge_thread::MaximaLibResponse) { - use bridge_thread::MaximaLibResponse::*; - match response { - LoginResponse(res) => handle_login_response(app, res), - LoginCacheEmpty => app.backend_state = BackendStallState::UserNeedsToLogIn, - ServiceNeedsStarting => app.backend_state = BackendStallState::UserNeedsToInstallService, - ServiceStarted => app.backend_state = BackendStallState::Starting, - GameInfoResponse(res) => { - app.games.entry(res.game.slug.clone()).or_insert(res.game); - } - GameDetailsResponse(res) => handle_game_details_response(app, res), - FriendInfoResponse(res) => { - if !app.friends.iter().any(|f| f.id == res.friend.id) { - app.friends.push(res.friend); - } - } - CriticalError(err) => app.critical_error = Some(*err), - NonFatalError(err) => app.nonfatal_errors.push(*err), - ActiveGameChanged(slug) => app.playing_game = slug, - LocateGameResponse(res) => { - app.installer_state.locate_response = Some(res); - app.installer_state.locating = false; - } - DownloadProgressChanged(offer_id, progress) => { - if let Some(dl) = app.installing_now.as_mut() { - if dl.offer == offer_id { - dl.downloaded_bytes = progress.bytes; - dl.total_bytes = progress.bytes_total; - } - } - } - DownloadFinished(offer_id) => { - if app.installing_now.as_ref().is_some_and(|n| n.offer == offer_id) { - app.installing_now = None; - } - send_request(app, bridge_thread::MaximaLibRequest::GetGamesRequest); - } - DownloadQueueUpdate(current, queue) => handle_download_queue_update(app, current, queue), - } -} - -fn handle_login_response( - app: &mut MaximaEguiApp, - res: Result, -) { - match res { - Err(error) => { - warn!("Login failed: {}", error); - app.backend_state = BackendStallState::UserNeedsToLogIn; - } - Ok(res) => { - info!("Logged in as {}!", res.you.display_name()); - app.user_name = res.you.display_name().clone(); - app.user_id = res.you.id().clone(); - app.backend_state = BackendStallState::BingChilling; - send_request(app, bridge_thread::MaximaLibRequest::GetGamesRequest); - send_request(app, bridge_thread::MaximaLibRequest::GetFriendsRequest); - } - } -} - -fn handle_game_details_response( - app: &mut MaximaEguiApp, - res: bridge_thread::InteractThreadGameDetailsResponse, -) { - if let Some(game) = app.games.get_mut(&res.slug) { - let r = res.response; - game.details = GameDetailsWrapper::Available(GameDetails { - time: r.time, - achievements_unlocked: r.achievements_unlocked, - achievements_total: r.achievements_total, - path: r.path.clone(), - system_requirements_min: r.system_requirements_min.clone(), - system_requirements_rec: r.system_requirements_rec.clone(), - }); - } -} - -fn handle_download_queue_update( - app: &mut MaximaEguiApp, - current: Option, - queue: Vec, -) { - if let Some(current_offer) = current { - if !app.installing_now.as_ref().is_some_and(|n| n.offer == current_offer) { - let slug = find_slug_for_offer(&app.games, ¤t_offer); - app.installing_now = Some(QueuedDownload { - slug, - offer: current_offer, - downloaded_bytes: 0, - total_bytes: 0, - }); - } - } else { - app.installing_now = None; - } - - app.install_queue.clear(); - for offer in queue { - let slug = find_slug_for_offer(&app.games, &offer); - app.install_queue.insert( - offer.clone(), - QueuedDownload { slug, offer, downloaded_bytes: 0, total_bytes: 0 }, - ); - } } \ No newline at end of file diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 9df4b763..c8ba4175 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -135,10 +135,6 @@ pub enum BackendError { #[error(transparent)] Rtm(#[from] RtmError), #[error(transparent)] - SendResponse(#[from] SendError), - #[error(transparent)] - SendImageCacheLoaderCommand(#[from] SendError), - #[error(transparent)] ServiceGameImagesRequestBuilder(#[from] ServiceGameImagesRequestBuilderError), #[error(transparent)] ServiceHeroBackgroundImageRequestBuilder(#[from] ServiceHeroBackgroundImageRequestBuilderError), diff --git a/maxima-ui/src/enum_locale_map.rs b/maxima-ui/src/enum_locale_map.rs index 487694e5..3fbca027 100644 --- a/maxima-ui/src/enum_locale_map.rs +++ b/maxima-ui/src/enum_locale_map.rs @@ -72,6 +72,7 @@ impl EnumToString for TranslationManager { match variant { FrontendLanguage::SystemDefault => &self.localization.locale.default, FrontendLanguage::EnUS => &self.localization.locale.en_us, + FrontendLanguage::FrFR => &self.localization.locale.fr_fr, } } fn get_string(&self, variant: &mut FrontendLanguage) -> &str { diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 04f309f1..1df447e3 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -318,6 +318,7 @@ pub struct MaximaEguiApp { pub enum FrontendLanguage { SystemDefault, EnUS, + FrFR, } #[derive(serde::Serialize, serde::Deserialize, Copy, Clone)] diff --git a/maxima-ui/src/translation_manager.rs b/maxima-ui/src/translation_manager.rs index 9e089c1b..4111c2f6 100644 --- a/maxima-ui/src/translation_manager.rs +++ b/maxima-ui/src/translation_manager.rs @@ -97,6 +97,8 @@ pub struct LocalizedLocaleInfo { pub default: String, /// English (US) pub en_us: String, // I will deny all PRs that attempt to add british english. + /// French (France) + pub fr_fr: String, // Tabarnak j'veux pô ajouter l'canadien ça m'caliss, j'vais m'faire pogner par la police de la langue française } #[derive(Deserialize)] @@ -338,10 +340,11 @@ pub(crate) use positional_replace; impl TranslationManager { /// Gets an instance of LocalizedStrings for the specified locale code pub fn get_for_locale(code: &str) -> LocalizedStrings { - let english = include_str!("../res/locale/en_us.json").to_owned(); - let locale_json: &str = language_include_matcher!(code, &english; + let english = include_str!("../res/locale/en_us.json"); + let locale_json: &str = language_include_matcher!( + code, english; "en-US" => "en_us", - //"de-bug" => "de_bug" + "fr-FR" => "fr_fr", ); serde_json::from_str(locale_json).unwrap() @@ -358,6 +361,7 @@ impl TranslationManager { } } FrontendLanguage::EnUS => "en-US".to_owned(), + FrontendLanguage::FrFR => "fr-FR".to_owned(), }; let s = Self::get_for_locale(locale.as_str()); From 8e472d8dbf78e90c5fa65a6a077f421c5d4b0585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Moret?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:02:09 +0200 Subject: [PATCH 12/57] Update connection.rs --- maxima-lib/src/lsx/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index b35c097f..619c01c0 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -77,7 +77,7 @@ const CORE_SENDER: &str = "EALS"; const CHALLENGE_BUILD: &str = "release"; const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; // Need to figure out how to generate this -const CHALLENGE_VERSION: &str = "10,5,30,15625"; +const CHALLENGE_VERSION: &str = "10,5,30,15625"; // we will most certainly need to update that sooner or later... query EA-Protobuffers 'curr_version' ? lazy_static! { static ref LSX_PATTERN: Regex = Regex::new(r".*?").unwrap(); From cdbfe79e2cb84349ec7e029fef612c046453daab Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:19:10 +0200 Subject: [PATCH 13/57] feat: generate lsx challenge key - Added "lsx_crypto" to take care of the necessary stuff - Included a method to prepare a challenge response by encrypting a key and deriving a new seed. - Defined custom error types for better error handling. --- .github/workflows/build-ci.yml | 19 +- .gitignore | 2 - Cargo.lock | 7912 +++++++++++++++++ maxima-lib/src/content/downloader.rs | 8 +- maxima-lib/src/content/zip.rs | 17 + maxima-lib/src/lsx/connection.rs | 62 +- .../src/lsx/request/progressive_install.rs | 4 +- maxima-lib/src/lsx/request/voip.rs | 2 +- maxima-lib/src/lsx/service.rs | 9 +- maxima-lib/src/rtm/connection.rs | 2 +- maxima-lib/src/util/lsx_crypto.rs | 104 + maxima-lib/src/util/mod.rs | 1 + maxima-lib/src/util/simple_crypto.rs | 33 +- maxima-ui/Cargo.toml | 16 +- 14 files changed, 8106 insertions(+), 85 deletions(-) create mode 100644 Cargo.lock create mode 100644 maxima-lib/src/util/lsx_crypto.rs diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 0533aae5..391522b5 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -26,18 +26,10 @@ jobs: - name: Checkout the repo uses: actions/checkout@v4 - - name: Cache Rust ${{ matrix.rust-version }} - uses: actions/cache@v3 - with: - path: ${{ github.workspace }}\rust\.rustup - key: rust-${{ matrix.rust-version }}-${{ hashFiles('**/rust-toolchain') }} - - name: Setup Rust ${{ matrix.rust-version }} - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: ${{ matrix.rust-version }} - override: true components: rustfmt, clippy - name: Install Protoc @@ -56,6 +48,15 @@ jobs: cache-on-failure: true save-if: ${{ github.ref == 'refs/heads/master' }} + - name: Clippy + run: cargo clippy --workspace -- -D warnings + + - name: Format check + run: cargo fmt --all -- --check + + - name: Tests + run: cargo test --workspace + - name: Build Maxima if: runner.os == 'Windows' run: | diff --git a/.gitignore b/.gitignore index 8635fee0..781a6771 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ /target /.vscode /.idea - -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..3ba28420 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7912 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "serde", + "uuid", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags 2.13.0", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash 0.2.0", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.10.1", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash 0.2.0", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.4", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.0", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "git+https://github.com/sjp761/async-compression.git#a226c72f754f942ad187cc65cb1f03df5ae26b7b" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey 0.1.1", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base62" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytebuffer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2618ff1f07e000264e355904bff5c0527091fcbad883ff7f71383c5428915f4b" +dependencies = [ + "byteorder", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "bytestring" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" +dependencies = [ + "bytes", +] + +[[package]] +name = "cacao" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5952f0958672e4aa8fc706d01905c56af57759e078c53a6fddf4a13361943e7a" +dependencies = [ + "block", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "dispatch", + "lazy_static", + "libc", + "objc", + "objc_id", + "os_info", + "url", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.0", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-hex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9" + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "git+https://github.com/sjp761/async-compression.git#a226c72f754f942ad187cc65cb1f03df5ae26b7b" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "git+https://github.com/sjp761/async-compression.git#a226c72f754f942ad187cc65cb1f03df5ae26b7b" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "dataview" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" +dependencies = [ + "derive_pod", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecb" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfbf3db731928d6912bc3beda911d55b834cee3df6131ba79b337a1298a3fa9" +dependencies = [ + "cipher", +] + +[[package]] +name = "ecolor" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6758be723a3f298bbfda4db75748bc2ba0abafe096b6383c7c32da264764fbc3" +dependencies = [ + "bytemuck", + "color-hex", + "emath", + "serde", +] + +[[package]] +name = "eframe" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc8234e2f681b2afd2b2e8c332fa614de2fddbdcb28d0acf5c420449682ea90" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "home", + "image", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "profiling", + "raw-window-handle", + "ron", + "serde", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796c98d50b79631281d516343a6f6e93c0666462ca36e2c93b39f25d7793325" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.13.0", + "emath", + "epaint", + "itertools", + "log", + "nohash-hasher", + "profiling", + "ron", + "serde", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui-winit" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea6bf3608db949588b95b8b341ee358d0c3f95cf4dc3f53d8d76717edee87db" +dependencies = [ + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "serde", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_demo_lib" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579c0df7fe5a6bff816f49b4500390737c33a63fd9a09f5edcbeeb261c2f6ef" +dependencies = [ + "egui", + "egui_extras", + "log", + "unicode_names2", +] + +[[package]] +name = "egui_extras" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bd33be7338367bf21f54d62e069d58a5fb3ae033fa8bc6df7a77b9ef4cf957" +dependencies = [ + "ahash", + "egui", + "ehttp", + "enum-map", + "image", + "itertools", + "log", + "mime_guess2", + "profiling", + "resvg", +] + +[[package]] +name = "egui_glow" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52274b9bfb8d8e252cd0f00c9f6214f360d75a0962baa77a2d62ddb002fe99d4" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", +] + +[[package]] +name = "ehttp" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f1b93eb2e039aaff63ce07cca59bd1dca02f2ce30075a17b619d2c42f56efc" +dependencies = [ + "document-features", + "js-sys", + "ureq", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "emath" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4ec073c9898516584d8c6cfdcee95b530b3d941cd5031ef4050aa36812308b" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "enable-ansi-support" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea7457668b3da8a4b702f3d79e131aa3e81cd7e81cc95fb2d54fce9f182ecc77" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "epaint" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e60a8888b51da911df23918fd7301359b1d43a406a0ff3b8863af093dd7fc6c" +dependencies = [ + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "font-types", + "harfrust", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "serde", + "skrifa", + "smallvec", + "unicode-general-category", + "unicode-segmentation", + "vello_cpu", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ee4e1f553a3584c301f3a56ff1a775f1384781396cea301c8d952e9b93f560" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "git+https://github.com/sjp761/flate2-rs.git#45fc9f6a56a97513b285029fee14213a2d0fe2e5" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide 0.9.1", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-array" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" +dependencies = [ + "rustversion", + "typenum", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "log", + "peniko", + "skrifa", + "smallvec", + "vello_common", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.0", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.2", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + +[[package]] +name = "impl-more" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "inquire" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" +dependencies = [ + "bitflags 2.13.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling 0.23.0", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_elevated" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5299060ff5db63e788015dcb9525ad9b84f4fd9717ed2cbdeba5018cbf42f9b5" +dependencies = [ + "winapi", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.13.0", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maxima-bootstrap" +version = "0.1.0" +dependencies = [ + "base64", + "cacao", + "maxima-lib", + "maxima-resources", + "querystring", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "url", + "urlencoding", + "winapi", + "windows-service", + "winreg", +] + +[[package]] +name = "maxima-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "clap", + "futures", + "inquire", + "is_elevated", + "lazy_static", + "log", + "maxima-lib", + "maxima-resources", + "regex", + "serde", + "serde_json", + "tokio", + "winapi", + "winreg", +] + +[[package]] +name = "maxima-lib" +version = "1.0.0" +dependencies = [ + "aes", + "async-compression", + "async-recursion", + "async-trait", + "base62", + "base64", + "bytebuffer", + "bytes", + "cbc", + "chrono", + "crc32fast", + "derive-getters", + "derive_builder", + "directories", + "ecb", + "enable-ansi-support", + "encoding_rs", + "filetime", + "flate2", + "futures", + "generic-array 1.4.3", + "gethostname", + "glob", + "hex", + "http 1.4.2", + "is_elevated", + "lazy_static", + "log", + "mac_address", + "md5", + "moka", + "open", + "pastey 0.2.3", + "pelite", + "prost", + "prost-build", + "querystring", + "quick-xml 0.40.1", + "rand 0.10.1", + "regex", + "reqwest", + "ring", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "sha2-const", + "smbios-lib", + "strum_macros", + "sysinfo", + "tar", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "toml 1.1.2+spec-1.1.0", + "ureq", + "url", + "uuid", + "webpki-roots", + "widestring", + "winapi", + "windows-service", + "winreg", + "wmi", + "xz2", + "zstd", +] + +[[package]] +name = "maxima-resources" +version = "0.1.0" +dependencies = [ + "winres", +] + +[[package]] +name = "maxima-service" +version = "0.1.0" +dependencies = [ + "actix-web", + "base64", + "derive_more", + "directories", + "hex", + "log", + "maxima-lib", + "maxima-resources", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "structured-logger", + "thiserror 2.0.18", + "tokio", + "winapi", + "windows-service", + "winreg", +] + +[[package]] +name = "maxima-tui" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "clap", + "color-eyre", + "futures", + "inquire", + "is_elevated", + "lazy_static", + "log", + "maxima-lib", + "maxima-resources", + "ratatui", + "regex", + "serde", + "serde_json", + "strum", + "tokio", + "winapi", + "winreg", +] + +[[package]] +name = "maxima-ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "eframe", + "egui", + "egui_demo_lib", + "egui_extras", + "fuzzy-matcher", + "humansize", + "image", + "is_elevated", + "log", + "maxima-lib", + "maxima-resources", + "puffin", + "puffin_http", + "reqwest", + "rustls", + "serde", + "serde_json", + "strum", + "strum_macros", + "sys-locale", + "thiserror 2.0.18", + "tokio", + "winapi", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime", + "phf", + "phf_shared", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-location 0.3.2", + "objc2-core-text", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-user-notifications 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" +dependencies = [ + "android_system_properties", + "log", + "nix 0.31.3", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pelite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +dependencies = [ + "dataview", + "libc", + "no-std-compat", + "pelite-macros", + "winapi", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo 0.13.1", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.6", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", + "unicase", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.118", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "puffin" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b514d95a258be801fde8a1ff1c974f4a4841d9750f5d1d6690fc07a5ad4049" +dependencies = [ + "anyhow", + "bincode", + "byteorder", + "cfg-if", + "itertools", + "lz4_flex", + "parking_lot", + "serde", +] + +[[package]] +name = "puffin_http" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f912991aab1adae69d2be9455e8db0f41e5ad3da87706ab2de64908678c2c76" +dependencies = [ + "anyhow", + "crossbeam-channel", + "log", + "parking_lot", + "puffin", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "querystring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.0", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags 2.13.0", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2-const" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb83f144cbda94be5d2278400eee9e0a03eb68b5504349943655b7b1603c4af" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smbios-lib" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c18320ad3d997a100cb948fc020111936c530eddfde947f467083730e39e72" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "getopts", + "io-kit-sys", + "libc", + "mach2", + "serde", + "serde_json", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.0", + "calloop 0.14.4", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structured-logger" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e5db98a0b69a45a1f8604185fd4c2a5900b640118530b82c4ce74e12c5c9ab" +dependencies = [ + "log", + "parking_lot", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sval" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d" + +[[package]] +name = "sval_buffer" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.39.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.13.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset 0.4.2", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2 0.10.9", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags 2.13.0", + "bytes", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_names2" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c3e18d850bb6ebd57735e5654f0af65e572b05c2397e7b2b1a7c6a792cc29c" +dependencies = [ + "phf", + "unicode_names2_generator", +] + +[[package]] +name = "unicode_names2_generator" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849744a58c479122ff24910d7ca57f312b62613ef0412dc327776ae6b235d16a" +dependencies = [ + "phf_codegen", + "rand 0.8.6", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http 1.4.2", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "imagesize", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "xmlwriter", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "atomic", + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vello_common" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown 0.17.1", + "log", + "peniko", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "vello_cpu" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588691169aed86b5c8fb487266afee01323234e6fd0a3f2aaec0eaa8e4007f23" +dependencies = [ + "bytemuck", + "glifo", + "hashbrown 0.17.1", + "vello_common", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2 0.10.9", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-service" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" +dependencies = [ + "bitflags 2.13.0", + "widestring", + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "android-activity", + "atomic-waker", + "bitflags 2.13.0", + "block2 0.5.1", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows", + "windows-core", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 050c2d6f..b74ff2e9 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -292,8 +292,7 @@ impl<'a> EntryDownloadRequest<'a> { if file_size > entry_size { return Ok(EntryDownloadState::Borked); } - - return Ok(EntryDownloadState::Borked); + return Ok(EntryDownloadState::Resumable); } // We must be calculating the hash incorrectly or something @@ -317,10 +316,7 @@ impl<'a> EntryDownloadRequest<'a> { async fn download(&mut self) -> Result<(), DownloadError> { let mut tries = 0; while tries < 5 { - // State serialization is disabled for now. - //let start = self.decoder.write_in_pos() as i64; - - let start = 0; + let start = self.decoder.write_in_pos() as i64; debug!( "Downloading {} from {} to {} ({})", diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index 7e14fcfd..d8f2a598 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -7,6 +7,7 @@ use reqwest::header::ToStrError; use std::cmp; use std::string::FromUtf8Error; use thiserror::Error; +use std::path::{Component, Path}; /// This module is based on https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html @@ -45,6 +46,8 @@ pub enum EntryError { Decode, #[error("invalid signature `{0:#10x}` (expected `{sig:#10x}`)", sig = ZIP_FILE_HEADER_SIGNATURE)] Signature(u32), + #[error("path traversal detected: {0}")] + PathTraversal(String), } #[derive(Error, Debug)] @@ -95,6 +98,19 @@ fn signature_scan_rev(data: &[u8], signature: u32) -> Option { None } +fn validate_entry_name(name: &str) -> Result<(), EntryError> { + let p = Path::new(name); + if p.is_absolute() { + return Err(EntryError::PathTraversal(name.to_string())); + } + for component in p.components() { + if component == Component::ParentDir { + return Err(EntryError::PathTraversal(name.to_string())); + } + } + Ok(()) +} + #[derive(Default, Clone, Debug, PartialEq)] pub enum CompressionType { #[default] @@ -171,6 +187,7 @@ impl ZipFileEntry { } s.to_string() }; + validate_entry_name(&entry.name)?; entry.extra_field = data.read_bytes(extra_field_len as usize)?; if let Ok(data) = entry.extra_field(0x01) { diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 619c01c0..22cdc985 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -5,14 +5,14 @@ use quick_xml::DeError; use regex::Regex; use std::{ io::{ErrorKind, Read, Write}, - net::TcpStream, path::PathBuf, sync::Arc, - time::Duration, }; use sysinfo::{Pid, System}; use thiserror::Error; -use tokio::sync::{MutexGuard, RwLock}; +use tokio::{io::AsyncWriteExt, sync::{MutexGuard, RwLock}}; +use tokio::net::TcpStream; +use rand::rand_core::{Rng}; use super::{ request::{ @@ -76,9 +76,8 @@ pub enum LSXConnectionError { const CORE_SENDER: &str = "EALS"; const CHALLENGE_BUILD: &str = "release"; -const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; // Need to figure out how to generate this -const CHALLENGE_VERSION: &str = "10,5,30,15625"; // we will most certainly need to update that sooner or later... query EA-Protobuffers 'curr_version' ? - +//const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; // Need to figure out how to generate this +const CHALLENGE_VERSION: &str = "10,5,64,37936"; lazy_static! { static ref LSX_PATTERN: Regex = Regex::new(r".*?").unwrap(); } @@ -205,17 +204,15 @@ pub struct Connection { impl Connection { pub async fn new( maxima_arc: LockedMaxima, - stream: TcpStream, + mut stream: TcpStream, ) -> Result { stream.set_nodelay(true)?; - stream.set_nonblocking(true)?; - stream.set_read_timeout(Some(Duration::from_secs(1)))?; let maxima: MutexGuard<'_, Maxima> = maxima_arc.lock().await; let context: &ActiveGameContext = match maxima.playing() { - Some(context) => context, + Some(ctx) => ctx, None => { - stream.shutdown(std::net::Shutdown::Both)?; + stream.shutdown().await?; return Err(LSXConnectionError::GameContext); } }; @@ -252,19 +249,24 @@ impl Connection { warn!("Failed to find PID through launch ID, things may not work!"); } + // Generate fresh 16-byte challenge per connection + let challenge: String = { + let mut bytes = [0u8; 16]; + rand::rng().fill_bytes(&mut bytes); + hex::encode(bytes) + }; + + drop(maxima); + let state = Arc::new(RwLock::new(ConnectionState { maxima: maxima_arc.clone(), - challenge: CHALLENGE_KEY.to_string(), + challenge, encryption: EncryptionState::Disabled, pid: pid.unwrap_or(0), queued_messages: Vec::new(), })); - Ok(Self { - maxima: maxima_arc.clone(), - stream, - state, - }) + Ok(Self { maxima: maxima_arc, stream, state }) } // State @@ -290,20 +292,13 @@ impl Connection { } pub async fn listen(&mut self) -> Result<(), LSXConnectionError> { + self.stream.readable().await?; let mut buffer = [0; 1024 * 8]; - - let n = match self.stream.read(&mut buffer) { - Ok(0) => { - return Err(LSXConnectionError::Closed); - } - Ok(n) => n, - Err(err) => { - let kind = err.kind(); - if kind == ErrorKind::WouldBlock { - return Ok(()); - } - return Err(LSXConnectionError::Internal(kind)); - } + let n = match self.stream.try_read(&mut buffer) { + Ok(0) => return Err(LSXConnectionError::Closed), + Ok(n) => n, + Err(e) if e.kind() == ErrorKind::WouldBlock => return Ok(()), + Err(e) => return Err(e.into()), }; let state = self.state.write().await; @@ -329,13 +324,13 @@ impl Connection { pub async fn process_queue(&mut self) -> Result<(), LSXConnectionError> { let mut state = self.state.write().await; for message in &state.queued_messages { - if let Err(err) = self.stream.write(message.as_bytes()) { + if let Err(err) = self.stream.write(message.as_bytes()).await { error!("Failed to send LSX message: {}", err); } } if !state.queued_messages.is_empty() { - self.stream.flush()?; + self.stream.flush().await?; } state.queued_messages.clear(); @@ -349,8 +344,7 @@ impl Connection { let mut message = message.to_string(); // replace unstable remove_matches (when most of the devs seem to think that it's stable since 2024 ??) w/ regex - let re = regex::Regex::new(r#"version="" "#).unwrap(); - message = re.replace_all(&message, "").to_string(); + message = message.replace(r#"version="" "#, ""); let lsx_message: LSX = quick_xml::de::from_str(message.as_str())?; let state = self.state.clone(); diff --git a/maxima-lib/src/lsx/request/progressive_install.rs b/maxima-lib/src/lsx/request/progressive_install.rs index 07b74fb3..b25a6374 100644 --- a/maxima-lib/src/lsx/request/progressive_install.rs +++ b/maxima-lib/src/lsx/request/progressive_install.rs @@ -15,7 +15,7 @@ pub async fn handle_pi_availability_request( _: LockedConnectionState, _: LSXIsProgressiveInstallationAvailable, ) -> Result, LSXRequestError> { - make_lsx_handler_response!(Response, IsProgressiveInstallationAvailableResponse, { + return make_lsx_handler_response!(Response, IsProgressiveInstallationAvailableResponse, { attr_Available: false, attr_ItemId: "Origin.OFR.50.0001456".to_string(), }) @@ -25,7 +25,7 @@ pub async fn handle_pi_installed_chunks_request( _: LockedConnectionState, request: LSXAreChunksInstalled, ) -> Result, LSXRequestError> { - make_lsx_handler_response!(Response, AreChunksInstalledResponse, { + return make_lsx_handler_response!(Response, AreChunksInstalledResponse, { attr_ItemId: "Origin.OFR.50.0001456".to_string(), attr_Installed: true, chunk_ids: request.chunk_ids, diff --git a/maxima-lib/src/lsx/request/voip.rs b/maxima-lib/src/lsx/request/voip.rs index 61f846f4..fce0e098 100644 --- a/maxima-lib/src/lsx/request/voip.rs +++ b/maxima-lib/src/lsx/request/voip.rs @@ -11,5 +11,5 @@ pub async fn handle_voip_status_request( _: LockedConnectionState, _: LSXGetVoipStatus, ) -> Result, LSXRequestError> { - make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }) + return make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }) } diff --git a/maxima-lib/src/lsx/service.rs b/maxima-lib/src/lsx/service.rs index 6a25642e..dfe20c9d 100644 --- a/maxima-lib/src/lsx/service.rs +++ b/maxima-lib/src/lsx/service.rs @@ -1,7 +1,7 @@ -use std::{io::ErrorKind, net::TcpListener, time::Duration}; +use std::{io::ErrorKind, time::Duration}; use log::{info, warn}; -use tokio::time::sleep; +use tokio::{time::sleep, net::TcpListener}; use crate::lsx::connection::LSXConnectionError; use crate::{core::LockedMaxima, lsx::connection::Connection}; @@ -18,8 +18,7 @@ pub enum LSXServerError { pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServerError> { let addr = "127.0.0.1:".to_string() + port.to_string().as_str(); - let listener = TcpListener::bind(&addr)?; - listener.set_nonblocking(true)?; + let listener = TcpListener::bind(&addr).await?; info!("Listening on: {}", addr); let mut connections: Vec = Vec::new(); @@ -46,7 +45,7 @@ pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServ idx += 1; } - let (socket, addr) = match listener.accept() { + let (socket, addr) = match listener.accept().await { Ok(s) => s, Err(err) => { let kind = err.kind(); diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index 1b8b2722..9b4db18d 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -139,7 +139,7 @@ impl RtmConnectionManager { if let Some(tx) = pending_responses.remove(id) { - tx.send(msg).unwrap(); + let _ = tx.send(msg); } else if id.is_empty() && let Some(body) = &msg.v1.as_ref().ok_or(RtmError::NoBody)?.body { update_presence_tx.send(body.clone()).await?; } diff --git a/maxima-lib/src/util/lsx_crypto.rs b/maxima-lib/src/util/lsx_crypto.rs new file mode 100644 index 00000000..d505b8f4 --- /dev/null +++ b/maxima-lib/src/util/lsx_crypto.rs @@ -0,0 +1,104 @@ +use aes::Aes128; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyInit, block_padding::Pkcs7}; + +use ecb::{Decryptor, Encryptor}; +use hex; +use thiserror::Error; + +type EcbEnc = Encryptor; +type EcbDec = Decryptor; + +const KEY_SIZE: usize = 16; +const DEFAULT_SEED: u32 = 7; + +#[derive(Debug, Error)] +pub enum CryptoError { + #[error("Input cannot be empty")] + EmptyInput, + #[error("Decryption failed: {0}")] + DecryptionFailed(String), + #[error("UTF-8 conversion error")] + Utf8Error(#[from] std::string::FromUtf8Error), +} + +#[derive(Clone, Debug)] +struct Random { + state: u64, +} + +impl Random { + fn new(seed: u32) -> Self { + Self { state: seed as u64 } + } + fn set_seed(&mut self, seed: u32) { + self.state = seed as u64; + } + /// LCG constants from Numerical Recipes / glibc + fn next(&mut self) -> u32 { + self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1); + (self.state >> 32) as u32 + } +} + +#[derive(Clone, Debug)] +pub struct Crypto { + pub(crate) key: [u8; KEY_SIZE], + rng: Random, +} + +impl Crypto { + pub fn new(seed: u32) -> Self { + let mut s = Self { + key: [0u8; KEY_SIZE], + rng: Random::new(DEFAULT_SEED), + }; + s.set_key(seed); + s + } + + pub fn set_key(&mut self, seed: u32) { + if seed == 0 { + for (i, b) in self.key.iter_mut().enumerate() { + *b = i as u8; + } + } else { + let new_seed = self.rng.next().wrapping_add(seed); + self.rng.set_seed(new_seed); + for b in self.key.iter_mut() { + *b = self.rng.next() as u8; + } + } + } + + pub fn encrypt(&self, plain: &str) -> Result, CryptoError> { + if plain.is_empty() { + return Err(CryptoError::EmptyInput); + } + Ok(EcbEnc::new(&self.key.into()).encrypt_padded_vec::(plain.as_bytes())) + } + + pub fn decrypt(&self, cipher: &[u8]) -> Result { + if cipher.is_empty() { + return Err(CryptoError::EmptyInput); + } + let pt = EcbDec::new(&self.key.into()) + .decrypt_padded_vec::(cipher) + .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?; + String::from_utf8(pt).map_err(CryptoError::from) + } + + /// Encrypt `key` with current AES key, hex-encode, derive new seed from first 2 ASCII bytes, + /// rotate internal key to session key, return hex response. + pub fn prepare_challenge_response(&mut self, key: &str) -> Result { + let ct = self.encrypt(key)?; + let hex_str = hex::encode(ct); + let b = hex_str.as_bytes(); + let seed = ((b[0] as u32) << 8) | (b[1] as u32); + self.set_key(seed); + Ok(hex_str) + } + + pub fn current_key(&self) -> [u8; 16] { + self.key + } +} \ No newline at end of file diff --git a/maxima-lib/src/util/mod.rs b/maxima-lib/src/util/mod.rs index 6e49d964..84b122ca 100644 --- a/maxima-lib/src/util/mod.rs +++ b/maxima-lib/src/util/mod.rs @@ -6,6 +6,7 @@ pub mod registry; pub mod simple_crypto; pub mod system_profiler_utils; pub mod wmi_utils; +pub mod lsx_crypto; #[cfg(windows)] pub mod dll_injector; diff --git a/maxima-lib/src/util/simple_crypto.rs b/maxima-lib/src/util/simple_crypto.rs index 0c82de70..9c6b5b58 100644 --- a/maxima-lib/src/util/simple_crypto.rs +++ b/maxima-lib/src/util/simple_crypto.rs @@ -1,31 +1,30 @@ use std::{io::Write, num::Wrapping, str}; -use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyInit, block_padding::Pkcs7}; use chrono::Datelike; use hex; -const CRYPTO_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; +use crate::util::lsx_crypto::Crypto; -type Aes128EcbEnc = ecb::Encryptor; -type Aes128EcbDec = ecb::Decryptor; +const CRYPTO_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; const PRIME_10K: u32 = 104729; const PRIME_20K: u32 = 224737; const PRIME_30K: u32 = 350377; -pub fn simple_decrypt(data: &[u8], key: &[u8; 16]) -> String { - let data = get_array(str::from_utf8(data).unwrap()); - - let res = Aes128EcbDec::new(&(*key).into()) - .decrypt_padded_vec::(&data) - .unwrap(); - - String::from_utf8(res).unwrap() +/// Encrypt data with AES-128-ECB-PKCS7, return hex string. +pub fn simple_encrypt(data: &[u8], key: &[u8; 16]) -> String { + let mut crypto = Crypto::new(0); // seed=0 gives identity key 00..0f + crypto.key = *key; + let ct = crypto.encrypt(std::str::from_utf8(data).unwrap_or("")).unwrap_or_default(); + hex::encode(ct) } -pub fn simple_encrypt(data: &[u8], key: &[u8; 16]) -> String { - let res = Aes128EcbEnc::new(&(*key).into()).encrypt_padded_vec::(data); - hex::encode(res) +/// Decrypt hex-encoded AES-128-ECB-PKCS7 data. +pub fn simple_decrypt(data: &[u8], key: &[u8; 16]) -> String { + let ciphertext = hex::decode(data).unwrap_or_default(); + let mut crypto = Crypto::new(0); + crypto.key = *key; + crypto.decrypt(&ciphertext).unwrap_or_default() } pub fn check_challenge_response(response: &str, challenge: &str) -> bool { @@ -47,8 +46,8 @@ pub fn make_lsx_key(seed: u16) -> [u8; 16] { crand.seed(seed); let mut result: [u8; 16] = [0; 16]; - for item in &mut result { - *item = crand.rand() as u8; + for i in 0..16 { + result[i] = crand.rand() as u8; } result } diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index 7e16d1be..fab6bdc7 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -7,26 +7,26 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi", "psapi", "wininet", "winerror", "iphlpapi", "tcpmib" ] } -anyhow = "1.0.102" +anyhow = "1.0.103" clap = { version = "4.6.1", features = ["derive"] } -eframe = { version = "0.34.3", default-features = false, features = [ "persistence", "glow" ] } -egui = "0.34.3" -egui_extras = {version = "0.34.3", features = [ "svg", "all_loaders" ] } +eframe = { version = "0.35.0", default-features = false, features = [ "persistence", "glow" ] } +egui = "0.35.0" +egui_extras = {version = "0.35.0", features = [ "svg", "all_loaders" ] } image = { version = "0.25.10", features = [ "png", "jpeg" ] } reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } serde = "1.0.228" serde_json = "1.0.150" tokio = { version = "1.52.3", features = ["full"] } -log = "0.4.32" -egui_demo_lib = "0.34.3" +log = "0.4.33" +egui_demo_lib = "0.35.0" puffin_http = "0.17.0" puffin = "0.20.0" -rustls = { version = "0.23.40", features = ["aws-lc-rs"] } +rustls = { version = "0.23.41", features = ["aws-lc-rs"] } strum = { version = "0.28.0", features = ["derive"] } strum_macros = "0.28.0" sys-locale = "0.3.2" humansize = { version = "2.1.3", features = ["no_alloc"] } -fuzzy-matcher = "*" +fuzzy-matcher = "0.3.7" thiserror = "2.0.18" [target.'cfg(windows)'.dependencies] From e64f1d5afb9e3b33908207a40fd54d6c4f84ed53 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:55:03 +0200 Subject: [PATCH 14/57] add safeguards, bump resolver to 3 to match rust ver, make use of workspace for deps --- Cargo.lock | 38 +++++++---------- Cargo.toml | 30 ++++++++++++- maxima-bootstrap/Cargo.toml | 34 +++++---------- maxima-cli/Cargo.toml | 28 ++++++------ maxima-lib/Cargo.toml | 64 +++++++++++----------------- maxima-lib/src/content/downloader.rs | 12 +++--- maxima-lib/src/lsx/connection.rs | 19 ++++++--- maxima-lib/src/rtm/connection.rs | 7 ++- maxima-service/Cargo.toml | 28 ++++++------ maxima-tui/Cargo.toml | 32 +++++++------- maxima-ui/Cargo.toml | 24 +++++------ 11 files changed, 162 insertions(+), 154 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ba28420..1ff4118a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,9 +539,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -549,14 +549,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -2597,9 +2598,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", "zeroize", @@ -3413,7 +3414,7 @@ dependencies = [ "prost", "prost-build", "querystring", - "quick-xml 0.40.1", + "quick-xml 0.41.0", "rand 0.10.1", "regex", "reqwest", @@ -4302,13 +4303,12 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.5" +version = "5.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" dependencies = [ "is-wsl", "libc", - "pathdiff", ] [[package]] @@ -4429,12 +4429,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "pelite" version = "0.10.0" @@ -4869,9 +4863,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.40.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", "serde", @@ -6301,9 +6295,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "0e48db7b415311b615f910b3dcaa4557bcd4bf1982379c95c223fd8c2a20e210" dependencies = [ "deranged", "libc", @@ -6323,9 +6317,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 792a8d4b..7ef13a47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -resolver = "2" +resolver = "3" members = [ "maxima-bootstrap", "maxima-cli", @@ -10,6 +10,34 @@ members = [ "maxima-resources", ] +[workspace.dependencies] +# Common Workspace Dependencies for all Maxima libs +tokio = { version = "1.52.3", features = ["full", "rt", "rt-multi-thread", "time", "net"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } +base64 = "0.22.1" +thiserror = "2.0.18" +anyhow = "1.0.102" +lazy_static = "1.5.0" +log = "0.4.32" +regex = "1.12.4" +futures = "0.3.32" +clap = { version = "4.6.1", features = ["derive"] } +inquire = "0.9.4" +strum = { version = "0.28.0", features = ["derive"] } +strum_macros = "0.28.0" +url = "2.5.8" +querystring = "1.1.0" +directories = "6.0.0" +hex = "0.4.3" + +# Common Target Specific Dependencies +winapi = { version = "0.3.9", features = ["memoryapi", "handleapi", "synchapi", "wincon", "consoleapi"] } +winreg = "0.56.0" +windows-service = "0.8.1" +is_elevated = "0.1.2" + [profile.release] strip = true diff --git a/maxima-bootstrap/Cargo.toml b/maxima-bootstrap/Cargo.toml index 5a36eecb..a75992ab 100644 --- a/maxima-bootstrap/Cargo.toml +++ b/maxima-bootstrap/Cargo.toml @@ -7,32 +7,20 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -tokio = { version = "1.52.3", features = [ - "full", - "rt", - "rt-multi-thread", - "time", - "net", -] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" -reqwest = { version = "0.13.4", features = ["stream", "rustls", "query"], default-features = false } -base64 = "0.22.1" -url = "2.5.8" -querystring = "1.1.0" +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +reqwest = { workspace = true, features = ["query"] } +base64 = { workspace = true } +url = { workspace = true } +querystring = { workspace = true } urlencoding = "2.1.3" -thiserror = "2.0.18" +thiserror = { workspace = true } [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = [ - "memoryapi", - "handleapi", - "synchapi", - "wincon", - "consoleapi", -] } -winreg = "0.56.0" -windows-service = "0.8.1" +winapi = { workspace = true } +winreg = { workspace = true } +windows-service = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] cacao = "0.3.2" diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index 1e74312f..788e796c 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -6,22 +6,22 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -clap = { version = "4.6.1", features = [ "derive" ] } -tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } -serde = { version = "1.0.228", features = [ "derive" ] } -serde_json = "1.0.150" -anyhow = "1.0.102" -lazy_static = "1.5.0" -base64 = "0.22.1" -log = "0.4.32" -regex = "1.12.4" -inquire = "0.9.4" -futures = "0.3.32" +clap = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +lazy_static = { workspace = true } +base64 = { workspace = true } +log = { workspace = true } +regex = { workspace = true } +inquire = { workspace = true } +futures = { workspace = true } [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi" ] } -winreg = "0.56.0" -is_elevated = "0.1.2" +winapi = { workspace = true } +winreg = { workspace = true } +is_elevated = { workspace = true } [build-dependencies] maxima-resources = { path = "../maxima-resources" } diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 3dcb79fd..81f9a09b 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -13,33 +13,27 @@ documentation = "https://docs.rs/maxima" rust-version = "1.96.0" [dependencies] -tokio = { version = "1.52.3", features = [ - "full", - "rt", - "rt-multi-thread", - "time", - "net", -] } +tokio = { workspace = true } moka = { version = "0.12.15", features = ["sync"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" -quick-xml = { version = "0.40.1", features = ["serialize"] } +serde = { workspace = true } +serde_json = { workspace = true } +quick-xml = { version = "0.41.0", features = ["serialize"] } pastey = "0.2.3" -hex = "0.4.3" -lazy_static = "1.5.0" -reqwest = { version = "0.13.4", features = ["stream", "form", "rustls", "gzip", "json", "query"], default-features = false } -base64 = "0.22.1" +hex = { workspace = true } +lazy_static = { workspace = true } +reqwest = { workspace = true, features = ["form", "gzip", "json"] } +base64 = { workspace = true } base62 = "2.2.4" -log = "0.4.32" +log = { workspace = true } enable-ansi-support = "0.3.1" chrono = "0.4.45" -regex = "1.12.4" -directories = "6.0.0" -open = "5.3.5" -querystring = "1.1.0" +regex = { workspace = true } +directories = { workspace = true } +open = "5.3.6" +querystring = { workspace = true } ureq = { version = "3.3.0", features = ["_tls"] } -sysinfo = "0.39.3" -strum_macros = "0.28.0" +sysinfo = "0.39.5" +strum_macros = { workspace = true } sha2-const = "0.1.3" tar = "0.4.46" flate2 = { version = "=1.1.10", default-features = false, features = ["zlib-default"] } @@ -53,40 +47,35 @@ mac_address = "1.1.8" filetime = "0.2.29" rand = "0.10.1" ring = "0.17.14" -futures = "0.3.32" +futures = { workspace = true } prost = "0.14.4" encoding_rs = "0.8.35" async-compression = { version = "=0.4.42", features = ["tokio", "deflate"] } tokio-util = { version = "0.7.18", features = ["io", "compat"] } -uuid = "1.23.3" +uuid = "1.23.4" tokio-rustls = "0.26.4" -rustls = "0.23.40" -webpki-roots = "1.0.7" +rustls = "0.23.41" +webpki-roots = "1.0.8" aes = "0.9.1" ecb = { version = "0.2.0", features = ["alloc", "block-padding", "default", "zeroize"] } cbc = { version = "0.2.1", features = ["alloc", "block-padding", "default", "zeroize"] } glob = "0.3.3" md5 = "0.8.0" -bytes = "1.11.1" +bytes = "1.12.0" async-trait = "0.1.89" crc32fast = "1.5.0" async-recursion = "1.1.1" pelite = "0.10.0" gethostname = "1.1.0" -thiserror = "2.0.18" -url = "2.5.8" +thiserror = { workspace = true } +url = { workspace = true } http = "1.4.2" generic-array = "1.4.3" rustls-pki-types = "1.14.1" [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = [ - "memoryapi", +winapi = { workspace = true, features = [ "errhandlingapi", - "handleapi", - "synchapi", - "wincon", - "consoleapi", "libloaderapi", "shellapi", "psapi", @@ -97,12 +86,11 @@ winapi = { version = "0.3.9", features = [ "tcpmib", "winsvc", "winbase", - "winreg", "impl-default" ] } -winreg = "0.56.0" -windows-service = "0.8.1" -is_elevated = "0.1.2" +winreg = { workspace = true } +windows-service = { workspace = true } +is_elevated = { workspace = true } widestring = "1.2.1" wmi = "0.18.4" diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index b74ff2e9..d3ded871 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -30,10 +30,10 @@ use tokio::{ }; use tokio_util::compat::FuturesAsyncReadCompatExt; -fn zstate_path(id: &str, path: &str) -> Result { +async fn zstate_path(id: &str, path: &str) -> Result { let mut path = maxima_dir()?.join("temp/downloader").join(id).join(path); path.set_extension("eazstate"); - std::fs::create_dir_all(path.safe_parent()?)?; + tokio::fs::create_dir_all(path.safe_parent()?).await?; Ok(path) } @@ -150,7 +150,7 @@ impl DownloadDecoder for NoopDecoder { struct AsyncWriterWrapper<'a> { id: String, path: String, - zlib_state_file: std::fs::File, + zlib_state_file: tokio::fs::File, decoder: &'a mut Box, inner: Arc>>, } @@ -165,11 +165,11 @@ impl<'a> AsyncWriterWrapper<'a> { Ok(AsyncWriterWrapper { id: id.to_owned(), path: path.to_owned(), - zlib_state_file: std::fs::OpenOptions::new() + zlib_state_file: tokio::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) - .open(zstate_path(&id, &path)?)?, + .open(zstate_path(&id, &path).await?).await?, decoder, inner, }) @@ -536,7 +536,7 @@ impl ZipDownloader { }; if state == EntryDownloadState::Resumable { - let state_file = zstate_path(&self.id, entry.name())?; + let state_file = zstate_path(&self.id, entry.name()).await?; if state_file.exists() { let mut buf = Bytes::from(tokio::fs::read(state_file).await?); decoder.restore_state(&mut buf); diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 22cdc985..242fca98 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -126,7 +126,7 @@ impl ConnectionState { self.maxima.lock().await } - pub fn maxima_arc(&mut self) -> LockedMaxima { + pub fn maxima_arc(&self) -> LockedMaxima { self.maxima.clone() } @@ -355,7 +355,10 @@ impl Connection { LSXMessageType::Request(msg) => { Connection::process_request_message(&state, msg).await } - LSXMessageType::Response(_) => unimplemented!(), + LSXMessageType::Response(_) => { + warn!("Unexpected LSX Response message received, ignoring"); + Ok(None) + } }; let reply: Option = match reply { @@ -397,11 +400,13 @@ impl Connection { message: LSXRequest, ) -> Result, LSXConnectionError> { { - let pid = *state.read().await.pid(); - state - .write() - .await - .maxima() + let (maxima_arc, pid) = { + let s = state.read().await; + (s.maxima_arc(), *s.pid()) + }; + + maxima_arc + .lock() .await .call_event(MaximaEvent::ReceivedLSXRequest(pid, message.value.clone())); } diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index 9b4db18d..a3554ef9 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -128,11 +128,16 @@ impl RtmConnectionManager { expected_size = bytes.get_i32(); } + if expected_size <= 0 || expected_size as usize > 4 * 1024 * 1024 { + warn!("Invalid RTM frame size: {}", expected_size); + break; + } + if bytes.len() < expected_size as usize { break; } - let buf = bytes.clone().freeze().slice(0..expected_size as usize); + let buf = bytes.split_to(expected_size as usize).freeze(); let msg = Communication::decode(buf)?; let id = &msg.v1.as_ref().ok_or(RtmError::NoBody)?.request_id; diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index 72f5aea1..71c0456c 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -6,25 +6,25 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } -serde = { version = "1.0.228", features = [ "derive" ] } -serde_json = "1.0.150" -reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } -base64 = "0.22.1" -log = "0.4.32" -regex = "1.12.4" -directories = "6.0.0" +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +reqwest = { workspace = true } +base64 = { workspace = true } +log = { workspace = true } +regex = { workspace = true } +directories = { workspace = true } structured-logger = "1.0.5" -actix-web = "4.13.0" +actix-web = "4.14.0" sha2 = "0.11.0" derive_more = "2.1.1" -hex = "0.4.3" -thiserror = "2.0.18" +hex = { workspace = true } +thiserror = { workspace = true } [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi" ] } -winreg = "0.56.0" -windows-service = "0.8.1" +winapi = { workspace = true } +winreg = { workspace = true } +windows-service = { workspace = true } [build-dependencies] maxima-resources = { path = "../maxima-resources" } diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index ec60e138..b76551e8 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -6,25 +6,25 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -clap = { version = "4.6.1", features = [ "derive" ] } -tokio = { version = "1.52.3", features = [ "full", "rt", "rt-multi-thread", "time", "net" ] } -serde = { version = "1.0.228", features = [ "derive" ] } -serde_json = "1.0.150" -anyhow = "1.0.102" -lazy_static = "1.5.0" -base64 = "0.22.1" -log = "0.4.32" -regex = "1.12.4" -inquire = "0.9.4" -futures = "0.3.32" -ratatui = "0.30.1" -strum = "0.28.0" +clap = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +lazy_static = { workspace = true } +base64 = { workspace = true } +log = { workspace = true } +regex = { workspace = true } +inquire = { workspace = true } +futures = { workspace = true } +ratatui = "0.30.2" +strum = { workspace = true } color-eyre = "0.6.5" [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi" ] } -winreg = "0.56.0" -is_elevated = "0.1.2" +winapi = { workspace = true } +winreg = { workspace = true } +is_elevated = { workspace = true } [build-dependencies] maxima-resources = { path = "../maxima-resources" } diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index fab6bdc7..3b32f96c 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -6,31 +6,31 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -winapi = { version = "0.3.9", features = [ "memoryapi", "handleapi", "synchapi", "wincon", "consoleapi", "psapi", "wininet", "winerror", "iphlpapi", "tcpmib" ] } -anyhow = "1.0.103" -clap = { version = "4.6.1", features = ["derive"] } +winapi = { workspace = true, features = [ "psapi", "wininet", "winerror", "iphlpapi", "tcpmib" ] } +anyhow = { workspace = true } +clap = { workspace = true } eframe = { version = "0.35.0", default-features = false, features = [ "persistence", "glow" ] } egui = "0.35.0" egui_extras = {version = "0.35.0", features = [ "svg", "all_loaders" ] } image = { version = "0.25.10", features = [ "png", "jpeg" ] } -reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } -serde = "1.0.228" -serde_json = "1.0.150" -tokio = { version = "1.52.3", features = ["full"] } -log = "0.4.33" +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +log = { workspace = true } egui_demo_lib = "0.35.0" puffin_http = "0.17.0" puffin = "0.20.0" rustls = { version = "0.23.41", features = ["aws-lc-rs"] } -strum = { version = "0.28.0", features = ["derive"] } -strum_macros = "0.28.0" +strum = { workspace = true } +strum_macros = { workspace = true } sys-locale = "0.3.2" humansize = { version = "2.1.3", features = ["no_alloc"] } fuzzy-matcher = "0.3.7" -thiserror = "2.0.18" +thiserror = { workspace = true } [target.'cfg(windows)'.dependencies] -is_elevated = "0.1.2" +is_elevated = { workspace = true } [build-dependencies] maxima-resources = { path = "../maxima-resources" } From 67a8ac9078c68945922cbd8010ffeb2721eb7cca Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 3 Jul 2026 01:22:01 +0200 Subject: [PATCH 15/57] add fallback on deps in resolver settings, remove lazy_static for std's lazylock, limit requests to prevent 403s, fix potential lib issue --- .cargo/config.toml | 2 + Cargo.lock | 41 +++--- Cargo.toml | 7 +- maxima-cli/Cargo.toml | 1 - maxima-cli/src/main.rs | 9 +- maxima-lib/Cargo.toml | 7 +- maxima-lib/src/core/auth/login.rs | 9 +- maxima-lib/src/core/background_service_nix.rs | 8 +- maxima-lib/src/core/service_layer.rs | 135 +++++++++++++++--- maxima-lib/src/lsx/connection.rs | 9 +- maxima-lib/src/lsx/request/config.rs | 17 +-- maxima-lib/src/lsx/request/license.rs | 2 +- maxima-lib/src/ooa/mod.rs | 17 ++- maxima-lib/src/rtm/connection.rs | 1 - maxima-lib/src/unix/wine.rs | 15 +- maxima-lib/src/util/simple_crypto.rs | 42 ++---- maxima-tui/Cargo.toml | 1 - maxima-tui/src/main.rs | 9 +- 18 files changed, 199 insertions(+), 133 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..3c1f3636 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[resolver] +incompatible-rust-versions = "fallback" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1ff4118a..ef926e99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.10.1", + "rand 0.10.2", "sha1", "smallvec", "tokio", @@ -427,9 +427,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "as-slice" @@ -3164,14 +3164,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "bitflags 2.13.0", "libc", "plain", - "redox_syscall 0.8.1", + "redox_syscall 0.9.0", ] [[package]] @@ -3361,7 +3361,6 @@ dependencies = [ "futures", "inquire", "is_elevated", - "lazy_static", "log", "maxima-lib", "maxima-resources", @@ -3403,7 +3402,6 @@ dependencies = [ "hex", "http 1.4.2", "is_elevated", - "lazy_static", "log", "mac_address", "md5", @@ -3415,7 +3413,7 @@ dependencies = [ "prost-build", "querystring", "quick-xml 0.41.0", - "rand 0.10.1", + "rand 0.10.2", "regex", "reqwest", "ring", @@ -3489,7 +3487,6 @@ dependencies = [ "futures", "inquire", "is_elevated", - "lazy_static", "log", "maxima-lib", "maxima-resources", @@ -3787,9 +3784,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" dependencies = [ "num-integer", "num-traits", @@ -4971,9 +4968,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -5231,9 +5228,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ "bitflags 2.13.0", ] @@ -5390,9 +5387,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -5459,9 +5456,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -6295,9 +6292,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.52" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e48db7b415311b615f910b3dcaa4557bcd4bf1982379c95c223fd8c2a20e210" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "libc", diff --git a/Cargo.toml b/Cargo.toml index 7ef13a47..fcbacfba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,8 @@ serde_json = "1.0.150" reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } base64 = "0.22.1" thiserror = "2.0.18" -anyhow = "1.0.102" -lazy_static = "1.5.0" -log = "0.4.32" +anyhow = "1.0.103" +log = "0.4.33" regex = "1.12.4" futures = "0.3.32" clap = { version = "4.6.1", features = ["derive"] } @@ -43,4 +42,4 @@ strip = true [patch.crates-io] flate2 = { git = 'https://github.com/sjp761/flate2-rs.git' } -async-compression = { git = 'https://github.com/sjp761/async-compression.git' } +async-compression = { git = 'https://github.com/sjp761/async-compression.git' } \ No newline at end of file diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index 788e796c..b4b9092d 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -11,7 +11,6 @@ tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } -lazy_static = { workspace = true } base64 = { workspace = true } log = { workspace = true } regex = { workspace = true } diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index af823a56..40ecf236 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -2,11 +2,10 @@ use clap::{Parser, Subcommand}; use anyhow::{Result, bail}; use inquire::{Select, Text}; -use lazy_static::lazy_static; use log::{debug, error, info, warn}; use regex::Regex; -use std::{path::PathBuf, sync::Arc, time::Instant}; +use std::{path::PathBuf, sync::{Arc, LazyLock}, time::Instant}; #[cfg(windows)] use is_elevated::is_elevated; @@ -47,9 +46,9 @@ use maxima::{ util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, }; -lazy_static! { - static ref MANUAL_LOGIN_PATTERN: Regex = Regex::new(r"^(.*):(.*)$").unwrap(); -} +static MANUAL_LOGIN_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid") +}); #[derive(Subcommand, Debug)] enum Mode { diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 81f9a09b..61f6c66f 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -20,8 +20,7 @@ serde_json = { workspace = true } quick-xml = { version = "0.41.0", features = ["serialize"] } pastey = "0.2.3" hex = { workspace = true } -lazy_static = { workspace = true } -reqwest = { workspace = true, features = ["form", "gzip", "json"] } +reqwest = { workspace = true, features = ["form", "query", "gzip", "json"] } base64 = { workspace = true } base62 = "2.2.4" log = { workspace = true } @@ -45,7 +44,7 @@ derive-getters = "0.5.0" derive_builder = "0.20.2" mac_address = "1.1.8" filetime = "0.2.29" -rand = "0.10.1" +rand = "0.10.2" ring = "0.17.14" futures = { workspace = true } prost = "0.14.4" @@ -71,7 +70,7 @@ thiserror = { workspace = true } url = { workspace = true } http = "1.4.2" generic-array = "1.4.3" -rustls-pki-types = "1.14.1" +rustls-pki-types = "1.15.0" [target.'cfg(windows)'.dependencies] winapi = { workspace = true, features = [ diff --git a/maxima-lib/src/core/auth/login.rs b/maxima-lib/src/core/auth/login.rs index 39354d18..f28b88b6 100644 --- a/maxima-lib/src/core/auth/login.rs +++ b/maxima-lib/src/core/auth/login.rs @@ -1,16 +1,15 @@ -use lazy_static::lazy_static; use regex::Regex; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::net::TcpListener; +use std::sync::LazyLock; use crate::core::{auth::storage::AuthError, clients::JUNO_PC_CLIENT_ID}; use super::context::AuthContext; -lazy_static! { - static ref HTTP_PATTERN: Regex = - Regex::new(r"^([A-Za-z]+) +(.*) +(HTTP/[0-9][.][0-9])").unwrap(); -} +static HTTP_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r"^([A-Za-z]+) +(.*) +(HTTP/[0-9][.][0-9])").expect("HTTP pattern regex should be valid") +}); pub async fn begin_oauth_login_flow<'a>(context: &mut AuthContext<'a>) -> Result<(), AuthError> { open::that(context.nucleus_auth_url(JUNO_PC_CLIENT_ID, "code")?)?; diff --git a/maxima-lib/src/core/background_service_nix.rs b/maxima-lib/src/core/background_service_nix.rs index d8fe2e1e..0af28dcb 100644 --- a/maxima-lib/src/core/background_service_nix.rs +++ b/maxima-lib/src/core/background_service_nix.rs @@ -1,17 +1,17 @@ use base64::{Engine, engine::general_purpose}; -use lazy_static::lazy_static; use log::debug; use regex::Regex; use serde::Serialize; +use std::sync::LazyLock; use crate::{ unix::wine::{CommandType, run_wine_command}, util::native::{NativeError, SafeParent, SafeStr, module_path}, }; -lazy_static! { - static ref PID_PATTERN: Regex = Regex::new(r"wine-helper: PID (.*)").unwrap(); -} +static PID_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r"wine-helper: PID (.*)").expect("PID pattern regex should be valid") +}); #[derive(Default, Serialize)] pub struct WineGetPidArgs { diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index 1754277a..ad08e596 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -1,12 +1,16 @@ #![allow(non_snake_case)] -use log::debug; +use log::{debug, error, warn}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; use sha2_const; use std::fmt; +use std::sync::Arc; +use std::time::Duration; use thiserror::Error; +use tokio::sync::{Mutex, Semaphore}; +use tokio::time::{sleep, Instant}; use derive_builder::Builder; use derive_getters::Getters; @@ -50,6 +54,12 @@ pub enum ServiceLayerError { status_code: StatusCode, message: String, }, + #[error("Rate-limited or forbidden on `{operation}` after {retries} retries: `{message}`")] + RateLimit { + operation: String, + retries: u32, + message: String, + }, #[error("GraphQL error in operation `{operation}`: `{error:?}`")] GraphQL { operation: String, @@ -139,6 +149,9 @@ define_graphql_request!(ContentfulProxy, GetHeroBackgroundImage, gameHubCollecti pub struct ServiceLayerClient { auth: LockedAuthStorage, client: Client, + semaphore: Arc, + last_request: Arc>>, + min_request_interval: Duration, } impl ServiceLayerClient { @@ -146,6 +159,9 @@ impl ServiceLayerClient { Self { auth, client: Client::new(), + semaphore: Arc::new(Semaphore::new(3)), + last_request: Arc::new(Mutex::new(None)), + min_request_interval: Duration::from_millis(100), } } @@ -174,6 +190,84 @@ impl ServiceLayerClient { variables: T, full_query: bool, ) -> Result + where + T: Serialize, + R: for<'a> Deserialize<'a>, + { + const MAX_RETRIES: u32 = 3; + const BASE_DELAY_MS: u64 = 500; + + let _permit = self.semaphore.acquire().await.map_err(|_| { + ServiceLayerError::Http { + status_code: StatusCode::INTERNAL_SERVER_ERROR, + message: "semaphore closed".to_string(), + } + })?; + + // Enforce minimum interval between requests without holding a lock across await + let wait = { + let mut last = self.last_request.lock().await; + let now = Instant::now(); + let wait = last.map(|instant| { + let elapsed = instant.elapsed(); + if elapsed < self.min_request_interval { + self.min_request_interval - elapsed + } else { + Duration::ZERO + } + }); + *last = Some(now + wait.unwrap_or(Duration::ZERO)); + wait + }; + if let Some(duration) = wait { + if duration > Duration::ZERO { + sleep(duration).await; + } + } + + let mut last_error = None; + + for attempt in 0..=MAX_RETRIES { + match self.do_request(operation, &variables, full_query).await { + Ok(res) => return Ok(res), + Err(err) => { + let should_retry = matches!( + &err, + ServiceLayerError::Http { status_code, .. } + if *status_code == StatusCode::TOO_MANY_REQUESTS + ); + + if should_retry && attempt < MAX_RETRIES { + let delay = Duration::from_millis(BASE_DELAY_MS * 2u64.pow(attempt)); + warn!( + "Request `{}` returned {}; waiting {:?} before retry {}/{}", + operation.operation, + match &err { + ServiceLayerError::Http { status_code, .. } => status_code.to_string(), + _ => "error".to_string(), + }, + delay, + attempt + 1, + MAX_RETRIES + ); + sleep(delay).await; + last_error = Some(err); + } else { + return Err(err); + } + } + } + } + + Err(last_error.unwrap_or_else(|| ServiceLayerError::NoData)) + } + + async fn do_request( + &self, + operation: &ServiceLayerGraphQLRequest, + variables: T, + full_query: bool, + ) -> Result where T: Serialize, R: for<'a> Deserialize<'a>, @@ -237,34 +331,37 @@ impl ServiceLayerClient { ); let result = serde_json::from_str::(text.as_str())?; - let errors = result.get("errors"); - if let Some(errors) = errors { - let errors = errors.as_array().unwrap(); - let error = if let Value::Object(o) = &errors[0] { - o - } else { - return Err(ServiceLayerError::GraphQL { - operation: operation.operation.to_string(), - error: None, - }); - }; + if let Some(errors) = result.get("errors") { + let message = errors + .as_array() + .and_then(|arr| arr.first()) + .and_then(|first| first.get("message")) + .and_then(|msg| msg.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| errors.to_string()); return Err(ServiceLayerError::GraphQL { operation: operation.operation.to_string(), - error: error.get("message").map(|error| error.to_string()) + error: Some(message), }); } let data = result .get("data") .ok_or(ServiceLayerError::NoData)? - .as_object() - .ok_or(ServiceLayerError::NoData)? .get(operation.key) - .ok_or(ServiceLayerError::NoData)? - .to_owned(); - - Ok(serde_json::from_value::(data)?) + .ok_or_else(|| ServiceLayerError::MissingField)? + .clone(); + Ok(serde_json::from_value::(data).map_err(|err| { + error!( + "Failed to deserialize response for operation `{}` (key `{}`): {}. Response data: {}", + operation.operation, + operation.key, + err, + result.get("data").unwrap_or(&Value::Null) + ); + err + })?) } } diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 242fca98..a2a92df0 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -1,12 +1,11 @@ use derive_getters::Getters; -use lazy_static::lazy_static; use log::{debug, error, warn}; use quick_xml::DeError; use regex::Regex; use std::{ io::{ErrorKind, Read, Write}, path::PathBuf, - sync::Arc, + sync::{Arc, LazyLock}, }; use sysinfo::{Pid, System}; use thiserror::Error; @@ -78,9 +77,9 @@ const CORE_SENDER: &str = "EALS"; const CHALLENGE_BUILD: &str = "release"; //const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; // Need to figure out how to generate this const CHALLENGE_VERSION: &str = "10,5,64,37936"; -lazy_static! { - static ref LSX_PATTERN: Regex = Regex::new(r".*?").unwrap(); -} +static LSX_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r".*?").expect("LSX pattern regex should be valid") +}); macro_rules! lsx_message_matcher { ( diff --git a/maxima-lib/src/lsx/request/config.rs b/maxima-lib/src/lsx/request/config.rs index dc893de0..1948b783 100644 --- a/maxima-lib/src/lsx/request/config.rs +++ b/maxima-lib/src/lsx/request/config.rs @@ -1,4 +1,4 @@ -use lazy_static::lazy_static; +use std::sync::LazyLock; use crate::{ lsx::{ @@ -9,8 +9,8 @@ use crate::{ make_lsx_handler_response, }; -lazy_static! { - static ref SERVICES: Vec = vec![ +static SERVICES: LazyLock> = LazyLock::new(|| { + vec![ LSXService::new("EbisuSDK", "SDK"), LSXService::new("EbisuSDK", "PROFILE"), LSXService::new("XMPP", "PRESENCE"), @@ -45,17 +45,14 @@ lazy_static! { LSXService::new("PI", "PROGRESSIVE_INSTALLATION"), LSXService::new("PI", "PROGRESSIVE_INSTALLATION_EVENT"), LSXService::new("EbisuSDK", "CONTENT"), - ]; -} + ] +}); pub async fn handle_config_request( _: LockedConnectionState, _: LSXGetConfig, ) -> Result, LSXRequestError> { - let mut services: Vec = Vec::new(); - for service in SERVICES.iter() { - services.push(service.clone()); - } + let services = &SERVICES; - return make_lsx_handler_response!(Response, GetConfigResponse, { service: services }) + return make_lsx_handler_response!(Response, GetConfigResponse, { service: services.to_vec() }) } diff --git a/maxima-lib/src/lsx/request/license.rs b/maxima-lib/src/lsx/request/license.rs index 5a662d5c..3ed109cc 100644 --- a/maxima-lib/src/lsx/request/license.rs +++ b/maxima-lib/src/lsx/request/license.rs @@ -60,5 +60,5 @@ pub async fn handle_license_request( debug!("Got Denuvo Token: {}", token); - make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }) + return make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }) } diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index 5e31c6ef..bd8b0ccb 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -1,8 +1,12 @@ use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7}; use chrono::{DateTime, Duration, Utc}; use log::{debug, warn}; -use std::string::FromUtf8Error; -use std::{fs::create_dir_all, path::PathBuf}; +use std::{ + fs::create_dir_all, + path::PathBuf, + string::FromUtf8Error, + sync::LazyLock, +}; use tokio::fs; use base64::{DecodeError, Engine, engine::general_purpose}; @@ -11,7 +15,6 @@ use crate::core::{auth::hardware::HardwareInfo, endpoints::API_PROXY_NOVAFUSION_ #[cfg(unix)] use crate::unix::fs::case_insensitive_path; use crate::util::native::{NativeError, SafeParent}; -use lazy_static::lazy_static; use quick_xml::DeError; use regex::Regex; use reqwest::header::ToStrError; @@ -26,12 +29,12 @@ pub const OOA_CRYPTO_KEY: [u8; 16] = [ type Aes128CbcEnc = cbc::Encryptor; type Aes128CbcDec = cbc::Decryptor; -lazy_static! { - static ref EMAIL_PATTERN: Regex = Regex::new( +static EMAIL_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new( r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})" ) - .unwrap(); -} + .expect("email regex should be valid") +}); const LICENSE_PATH: &str = "ProgramData/Electronic Arts/EA Services/License"; diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index a3554ef9..1f0c0bf9 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -149,7 +149,6 @@ impl RtmConnectionManager { update_presence_tx.send(body.clone()).await?; } - bytes.advance(expected_size as usize); expected_size = -1; } }, diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 21c7e19d..a8a7b660 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -9,7 +9,6 @@ use std::{ }; use flate2::read::GzDecoder; -use lazy_static::lazy_static; use log::{info, warn}; use regex::Regex; use reqwest::StatusCode; @@ -28,9 +27,9 @@ use crate::util::{ registry::RegistryError, }; -lazy_static! { - static ref PROTON_PATTERN: Regex = Regex::new(r"GE-Proton\d+-\d+\.tar\.gz").unwrap(); -} +static PROTON_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r"GE-Proton\d+-\d+\.tar\.gz").expect("proton pattern regex should be valid") +}); // A Proton verb to use pub enum CommandType { @@ -309,7 +308,7 @@ pub(crate) async fn install_wine() -> Result<(), NativeError> { let asset = match release .assets .iter() - .find(|x| PROTON_PATTERN.captures(&x.name).is_some()) + .find(|x| PROTON_PATTERN.is_match(&x.name)) { Some(asset) => asset, None => return Err(NativeError::Wine(WineError::Fetch)), @@ -437,9 +436,9 @@ pub async fn setup_wine_registry() -> Result<(), NativeError> { pub type WineRegistry = HashMap; -lazy_static! { - static ref MX_WINE_REGISTRY: Mutex = Mutex::new(WineRegistry::new()); -} +static MX_WINE_REGISTRY: LazyLock> = LazyLock::new(|| { + Mutex::new(WineRegistry::new()) +}); async fn parse_wine_registry(file_path: &str) -> WineRegistry { let mut registry_map = MX_WINE_REGISTRY.lock().await; diff --git a/maxima-lib/src/util/simple_crypto.rs b/maxima-lib/src/util/simple_crypto.rs index 9c6b5b58..a4ac3170 100644 --- a/maxima-lib/src/util/simple_crypto.rs +++ b/maxima-lib/src/util/simple_crypto.rs @@ -1,9 +1,11 @@ -use std::{io::Write, num::Wrapping, str}; +use std::{num::Wrapping, str}; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyInit, block_padding::Pkcs7}; use chrono::Datelike; use hex; -use crate::util::lsx_crypto::Crypto; +type Aes128EcbEnc = ecb::Encryptor; +type Aes128EcbDec = ecb::Decryptor; const CRYPTO_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; @@ -13,18 +15,18 @@ const PRIME_30K: u32 = 350377; /// Encrypt data with AES-128-ECB-PKCS7, return hex string. pub fn simple_encrypt(data: &[u8], key: &[u8; 16]) -> String { - let mut crypto = Crypto::new(0); // seed=0 gives identity key 00..0f - crypto.key = *key; - let ct = crypto.encrypt(std::str::from_utf8(data).unwrap_or("")).unwrap_or_default(); + let ct = Aes128EcbEnc::new(key.into()).encrypt_padded_vec::(data); hex::encode(ct) } /// Decrypt hex-encoded AES-128-ECB-PKCS7 data. pub fn simple_decrypt(data: &[u8], key: &[u8; 16]) -> String { - let ciphertext = hex::decode(data).unwrap_or_default(); - let mut crypto = Crypto::new(0); - crypto.key = *key; - crypto.decrypt(&ciphertext).unwrap_or_default() + let Ok(ciphertext) = hex::decode(data) else { return String::new() }; + Aes128EcbDec::new(key.into()) + .decrypt_padded_vec::(&ciphertext) + .ok() + .and_then(|v| String::from_utf8(v).ok()) + .unwrap_or_default() } pub fn check_challenge_response(response: &str, challenge: &str) -> bool { @@ -68,28 +70,6 @@ impl CRandom { } } -fn get_array(s: &str) -> Vec { - let mut m = std::io::Cursor::new(Vec::new()); - let mut sb = String::new(); - let filter = "0123456789abcdef"; - let s = s.to_lowercase(); - for c in s.chars() { - if filter.contains(c) { - sb.push(c); - } - } - if sb.len() % 2 != 0 { - return Vec::new(); - } - let s = sb.as_str(); - for i in 0..(sb.len() / 2) { - let byte_str = &s[(i * 2)..((i * 2) + 2)]; - let byte = u8::from_str_radix(byte_str, 16).expect("Failed to parse byte from string"); - m.write_all(&[byte]).expect("Failed to write byte to stream"); - } - m.into_inner() -} - /// This code is required to launch games and changes daily pub fn rtp_handshake() -> u32 { let current_date = chrono::Utc::now(); diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index b76551e8..b48aa163 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -11,7 +11,6 @@ tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } -lazy_static = { workspace = true } base64 = { workspace = true } log = { workspace = true } regex = { workspace = true } diff --git a/maxima-tui/src/main.rs b/maxima-tui/src/main.rs index b596b0eb..a1e64658 100644 --- a/maxima-tui/src/main.rs +++ b/maxima-tui/src/main.rs @@ -2,14 +2,13 @@ use clap::{Parser, Subcommand}; use futures::StreamExt; use inquire::Select; -use lazy_static::lazy_static; use log::{debug, error, info, warn}; use regex::Regex; use service::{BridgeThread, MaximaLibRequest, MaximaLibResponse}; use std::{ io::stdout, - sync::Arc, + sync::{Arc, LazyLock}, time::{Duration, Instant}, }; @@ -54,9 +53,9 @@ use maxima::{ rtm::client::BasicPresence, }; -lazy_static! { - static ref MANUAL_LOGIN_PATTERN: Regex = Regex::new(r"^(.*):(.*)$").unwrap(); -} +static MANUAL_LOGIN_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid") +}); use anyhow::{Result, bail}; use color_eyre::config::HookBuilder; From 296ec4e7f4a2c76a990c3b6b3d8e0786646b937e Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:23:42 +0200 Subject: [PATCH 16/57] WIP DO NOT USE --- .github/workflows/build-ci.yml | 8 +- Cargo.lock | 97 +---- Cargo.toml | 20 +- maxima-bootstrap/Cargo.toml | 2 +- maxima-cli/Cargo.toml | 2 +- maxima-cli/src/main.rs | 11 +- maxima-lib/Cargo.toml | 36 +- maxima-lib/src/content/downloader.rs | 3 +- maxima-lib/src/content/manager.rs | 115 +++++- maxima-lib/src/content/zip.rs | 2 +- maxima-lib/src/core/auth/hardware.rs | 5 +- maxima-lib/src/core/auth/login.rs | 5 +- maxima-lib/src/core/cache.rs | 4 +- maxima-lib/src/core/cloudsync.rs | 5 +- maxima-lib/src/core/library.rs | 6 +- maxima-lib/src/core/manifest/dip.rs | 40 +- maxima-lib/src/core/manifest/mod.rs | 3 +- maxima-lib/src/core/manifest/pre_dip.rs | 44 ++- maxima-lib/src/core/mod.rs | 15 +- maxima-lib/src/core/service_layer.rs | 28 +- maxima-lib/src/lsx/connection.rs | 24 +- maxima-lib/src/lsx/request/account.rs | 2 +- maxima-lib/src/lsx/request/auth.rs | 2 +- maxima-lib/src/lsx/request/challenge.rs | 2 +- maxima-lib/src/lsx/request/config.rs | 2 +- maxima-lib/src/lsx/request/core.rs | 4 +- maxima-lib/src/lsx/request/game.rs | 4 +- maxima-lib/src/lsx/request/license.rs | 2 +- maxima-lib/src/lsx/request/offer.rs | 2 +- maxima-lib/src/lsx/request/profile.rs | 53 +-- .../src/lsx/request/progressive_install.rs | 4 +- maxima-lib/src/lsx/request/voip.rs | 2 +- maxima-lib/src/lsx/service.rs | 7 +- maxima-lib/src/ooa/mod.rs | 9 +- maxima-lib/src/rtm/connection.rs | 4 +- maxima-lib/src/unix/wine.rs | 5 +- maxima-lib/src/util/dll_injector.rs | 33 +- maxima-lib/src/util/lsx_crypto.rs | 2 +- maxima-lib/src/util/mod.rs | 2 +- maxima-lib/src/util/native.rs | 30 +- maxima-lib/src/util/registry.rs | 26 +- maxima-lib/src/util/service_win.rs | 29 +- maxima-lib/src/util/simple_crypto.rs | 4 +- maxima-resources/Cargo.toml | 2 +- maxima-service/Cargo.toml | 6 +- maxima-service/src/service/error.rs | 2 +- maxima-service/src/service/hash.rs | 11 +- maxima-service/src/service/mod.rs | 2 +- maxima-tui/Cargo.toml | 4 +- maxima-tui/src/main.rs | 7 +- maxima-tui/src/service.rs | 37 +- maxima-ui/Cargo.toml | 3 +- maxima-ui/src/bridge/get_friends.rs | 10 +- maxima-ui/src/bridge/get_games.rs | 60 +-- maxima-ui/src/bridge_processor.rs | 25 +- maxima-ui/src/bridge_thread.rs | 201 +++++++--- maxima-ui/src/event_thread.rs | 7 +- maxima-ui/src/main.rs | 23 +- maxima-ui/src/ui_image.rs | 8 +- .../src/util/easy_mark/easy_mark_editor.rs | 276 ++++++++++++++ .../util/easy_mark/easy_mark_highlighter.rs | 192 ++++++++++ .../src/util/easy_mark/easy_mark_parser.rs | 359 ++++++++++++++++++ .../src/util/easy_mark/easy_mark_viewer.rs | 178 +++++++++ maxima-ui/src/util/easy_mark/mod.rs | 14 + maxima-ui/src/util/mod.rs | 1 + maxima-ui/src/views/downloads_view.rs | 25 +- maxima-ui/src/views/game_view.rs | 6 +- 67 files changed, 1701 insertions(+), 463 deletions(-) create mode 100644 maxima-ui/src/util/easy_mark/easy_mark_editor.rs create mode 100644 maxima-ui/src/util/easy_mark/easy_mark_highlighter.rs create mode 100644 maxima-ui/src/util/easy_mark/easy_mark_parser.rs create mode 100644 maxima-ui/src/util/easy_mark/easy_mark_viewer.rs create mode 100644 maxima-ui/src/util/easy_mark/mod.rs diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 391522b5..998d596a 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -1,9 +1,9 @@ name: Maxima Build -on: [push] +on: [push, pull_request] concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request && github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -15,9 +15,9 @@ jobs: strategy: fail-fast: false matrix: - rust-version: [nightly] + rust-version: ["1.96", "stable", "nightly"] + experimental: [false, false, true] os: [ubuntu-latest, windows-latest] - experimental: [false] env: CARGO_TERM_COLOR: always diff --git a/Cargo.lock b/Cargo.lock index ef926e99..06dca3e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,17 +451,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -1015,12 +1004,6 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "color-hex" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9" - [[package]] name = "color-spantrace" version = "0.3.0" @@ -1617,7 +1600,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6758be723a3f298bbfda4db75748bc2ba0abafe096b6383c7c32da264764fbc3" dependencies = [ "bytemuck", - "color-hex", "emath", "serde", ] @@ -1702,18 +1684,6 @@ dependencies = [ "winit", ] -[[package]] -name = "egui_demo_lib" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9579c0df7fe5a6bff816f49b4500390737c33a63fd9a09f5edcbeeb261c2f6ef" -dependencies = [ - "egui", - "egui_extras", - "log", - "unicode_names2", -] - [[package]] name = "egui_extras" version = "0.35.0" @@ -3346,8 +3316,8 @@ dependencies = [ "tokio", "url", "urlencoding", - "winapi", "windows-service", + "windows-sys 0.61.2", "winreg", ] @@ -3368,7 +3338,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "winapi", + "windows-sys 0.61.2", "winreg", ] @@ -3378,7 +3348,6 @@ version = "1.0.0" dependencies = [ "aes", "async-compression", - "async-recursion", "async-trait", "base62", "base64", @@ -3429,6 +3398,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-rustls", + "tokio-stream", "tokio-util", "toml 1.1.2+spec-1.1.0", "ureq", @@ -3436,8 +3406,8 @@ dependencies = [ "uuid", "webpki-roots", "widestring", - "winapi", "windows-service", + "windows-sys 0.61.2", "winreg", "wmi", "xz2", @@ -3471,8 +3441,8 @@ dependencies = [ "structured-logger", "thiserror 2.0.18", "tokio", - "winapi", "windows-service", + "windows-sys 0.61.2", "winreg", ] @@ -3484,7 +3454,6 @@ dependencies = [ "base64", "clap", "color-eyre", - "futures", "inquire", "is_elevated", "log", @@ -3496,7 +3465,8 @@ dependencies = [ "serde_json", "strum", "tokio", - "winapi", + "tokio-stream", + "windows-sys 0.61.2", "winreg", ] @@ -3508,7 +3478,6 @@ dependencies = [ "clap", "eframe", "egui", - "egui_demo_lib", "egui_extras", "fuzzy-matcher", "humansize", @@ -3528,7 +3497,7 @@ dependencies = [ "sys-locale", "thiserror 2.0.18", "tokio", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4951,8 +4920,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ - "libc", - "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -4962,7 +4929,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -4977,16 +4944,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -5002,9 +4959,6 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] [[package]] name = "rand_core" @@ -5150,7 +5104,7 @@ dependencies = [ "paste", "profiling", "rand 0.9.4", - "rand_chacha 0.9.0", + "rand_chacha", "simd_helpers", "thiserror 2.0.18", "v_frame", @@ -6411,6 +6365,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -6660,26 +6625,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_names2" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c3e18d850bb6ebd57735e5654f0af65e572b05c2397e7b2b1a7c6a792cc29c" -dependencies = [ - "phf", - "unicode_names2_generator", -] - -[[package]] -name = "unicode_names2_generator" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849744a58c479122ff24910d7ca57f312b62613ef0412dc327776ae6b235d16a" -dependencies = [ - "phf_codegen", - "rand 0.8.6", -] - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index fcbacfba..dfc93120 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ [workspace.dependencies] # Common Workspace Dependencies for all Maxima libs -tokio = { version = "1.52.3", features = ["full", "rt", "rt-multi-thread", "time", "net"] } +tokio = { version = "1.52.3", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } @@ -32,13 +32,29 @@ directories = "6.0.0" hex = "0.4.3" # Common Target Specific Dependencies -winapi = { version = "0.3.9", features = ["memoryapi", "handleapi", "synchapi", "wincon", "consoleapi"] } +windows-sys = { version = "0.61.2", features = ["Win32_System", "Win32_System_Threading", "Win32_System_Console", "Win32_System_Console"] } winreg = "0.56.0" windows-service = "0.8.1" is_elevated = "0.1.2" [profile.release] strip = true +lto = "thin" +codegen-units = 1 +panic = "abort" + +[profile.release-debug] +inherits = "release" +strip = false +debug = 1 + +[profile.dev] +opt-level = 0 +debug = true +split-debuginfo = "unpacked" + +[profile.dev.package."*"] +opt-level = 1 [patch.crates-io] flate2 = { git = 'https://github.com/sjp761/flate2-rs.git' } diff --git a/maxima-bootstrap/Cargo.toml b/maxima-bootstrap/Cargo.toml index a75992ab..9f558d98 100644 --- a/maxima-bootstrap/Cargo.toml +++ b/maxima-bootstrap/Cargo.toml @@ -18,7 +18,7 @@ urlencoding = "2.1.3" thiserror = { workspace = true } [target.'cfg(windows)'.dependencies] -winapi = { workspace = true } +windows-sys = { workspace = true } winreg = { workspace = true } windows-service = { workspace = true } diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index b4b9092d..747a544f 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -18,7 +18,7 @@ inquire = { workspace = true } futures = { workspace = true } [target.'cfg(windows)'.dependencies] -winapi = { workspace = true } +windows-sys = { workspace = true } winreg = { workspace = true } is_elevated = { workspace = true } diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 40ecf236..c6b90797 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -5,7 +5,11 @@ use inquire::{Select, Text}; use log::{debug, error, info, warn}; use regex::Regex; -use std::{path::PathBuf, sync::{Arc, LazyLock}, time::Instant}; +use std::{ + path::PathBuf, + sync::{Arc, LazyLock}, + time::Instant, +}; #[cfg(windows)] use is_elevated::is_elevated; @@ -46,9 +50,8 @@ use maxima::{ util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, }; -static MANUAL_LOGIN_PATTERN: LazyLock = LazyLock::new(|| { - Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid") -}); +static MANUAL_LOGIN_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid")); #[derive(Subcommand, Debug)] enum Mode { diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 61f6c66f..80dfd1c0 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -56,14 +56,13 @@ tokio-rustls = "0.26.4" rustls = "0.23.41" webpki-roots = "1.0.8" aes = "0.9.1" -ecb = { version = "0.2.0", features = ["alloc", "block-padding", "default", "zeroize"] } -cbc = { version = "0.2.1", features = ["alloc", "block-padding", "default", "zeroize"] } +ecb = { version = "0.2.0", features = ["alloc", "block-padding", "zeroize"] } +cbc = { version = "0.2.1", features = ["alloc", "block-padding", "zeroize"] } glob = "0.3.3" md5 = "0.8.0" bytes = "1.12.0" async-trait = "0.1.89" crc32fast = "1.5.0" -async-recursion = "1.1.1" pelite = "0.10.0" gethostname = "1.1.0" thiserror = { workspace = true } @@ -71,21 +70,26 @@ url = { workspace = true } http = "1.4.2" generic-array = "1.4.3" rustls-pki-types = "1.15.0" +tokio-stream = "0.1.18" [target.'cfg(windows)'.dependencies] -winapi = { workspace = true, features = [ - "errhandlingapi", - "libloaderapi", - "shellapi", - "psapi", - "sddl", - "wininet", - "winerror", - "iphlpapi", - "tcpmib", - "winsvc", - "winbase", - "impl-default" +windows-sys = { workspace = true, features = [ + "Win32_System_LibraryLoader", + "Win32_UI_Shell", + "Win32_System_ProcessStatus", + "Win32_Security_Authorization", + "Win32_Security", + "Win32_Networking_WinInet", + "Win32_Foundation", + "Win32_System_WindowsProgramming", + "Win32_System_Threading", + "Win32_System_Console", + "Win32_System_Registry", + "Win32_System_Memory", + "Win32_System_Diagnostics_Debug", + "Win32_UI_WindowsAndMessaging", + "Win32_Storage_FileSystem", + "Win32_System_Services", ] } winreg = { workspace = true } windows-service = { workspace = true } diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index d3ded871..7e8ddcb4 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -169,7 +169,8 @@ impl<'a> AsyncWriterWrapper<'a> { .write(true) .create(true) .truncate(true) - .open(zstate_path(&id, &path).await?).await?, + .open(zstate_path(&id, &path).await?) + .await?, decoder, inner, }) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 329f8ba7..be8170b9 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -62,6 +62,8 @@ pub enum ContentManagerError { #[error("download in progress, you must cancel it before starting a new one")] DownloadInProgress, + #[error("offer `{0}` is not in the download queue")] + NotInQueue(String), } #[derive(Error, Debug)] @@ -133,6 +135,7 @@ pub struct GameDownloader { total_count: usize, total_bytes: usize, notify: Arc, + error: Arc>>, } impl GameDownloader { @@ -171,11 +174,12 @@ impl GameDownloader { total_count, total_bytes, notify: Arc::new(Notify::new()), + error: Arc::new(std::sync::Mutex::new(None)), }) } pub fn download(&self) { - let (downloader_arc, entries, cancel_token, completed_bytes, notify) = + let (downloader_arc, entries, cancel_token, completed_bytes, notify, error) = self.prepare_download_vars(); let total_count = self.total_count; tokio::spawn(async move { @@ -189,7 +193,8 @@ impl GameDownloader { ) .await; if let Err(err) = dl { - error!("Error when downloading!: `{:?}", err) + error!("Error when downloading!: `{:?}", err); + *error.lock().unwrap() = Some(err); } }); } @@ -202,6 +207,7 @@ impl GameDownloader { CancellationToken, Arc, Arc, + Arc>>, ) { ( self.downloader.clone(), @@ -209,6 +215,7 @@ impl GameDownloader { self.cancel_token.clone(), self.completed_bytes.clone(), self.notify.clone(), + self.error.clone(), ) } @@ -297,6 +304,10 @@ impl GameDownloader { pub fn offer_id(&self) -> &String { &self.offer_id } + + pub fn take_error(&self) -> Option { + self.error.lock().unwrap().take() + } } #[derive(Getters)] @@ -359,19 +370,103 @@ impl ContentManager { Ok(()) } - pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { - let mut event = None; + pub async fn cancel_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { + if let Some(current) = &self.current { + if current.offer_id() == offer_id { + current.cancel(); + self.current = None; + self.queue.current = None; - if let Some(current) = &self.current && current.is_done() { - event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - self.current = None; - self.queue.current = None; + if let Some(game) = self.queue.queued.pop() { + self.install_now(game).await?; + } + + self.queue.save().await?; + return Ok(()); + } + } + + let pos = self + .queue + .queued + .iter() + .position(|g| g.offer_id == offer_id); + if let Some(pos) = pos { + self.queue.queued.remove(pos); + self.queue.save().await?; + return Ok(()); + } + + Err(ContentManagerError::NotInQueue(offer_id.to_owned())) + } + + pub async fn pause_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { + if let Some(current) = &self.current { + if current.offer_id() == offer_id { + current.cancel(); + self.current = None; + let game = self + .queue + .current + .take() + .ok_or_else(|| ContentManagerError::NotInQueue(offer_id.to_owned()))?; + + self.queue.queued.insert(0, game); + + if let Some(next) = self.queue.queued.pop() { + self.install_now(next).await?; + } - if let Some(game) = self.queue.queued.pop() { - self.install_now(game).await?; + self.queue.save().await?; + return Ok(()); } + } + Err(ContentManagerError::NotInQueue(offer_id.to_owned())) + } + + pub async fn move_install_to_top(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { + let pos = self + .queue + .queued + .iter() + .position(|g| g.offer_id == offer_id); + if let Some(pos) = pos { + let game = self.queue.queued.remove(pos); + self.queue.queued.insert(0, game); self.queue.save().await?; + return Ok(()); + } + + Err(ContentManagerError::NotInQueue(offer_id.to_owned())) + } + + pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { + let mut event = None; + + if let Some(current) = &self.current { + if current.is_done() { + event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); + self.current = None; + self.queue.current = None; + + if let Some(game) = self.queue.queued.pop() { + self.install_now(game).await?; + } + + self.queue.save().await?; + } else if let Some(err) = current.take_error() { + let offer_id = current.offer_id().to_owned(); + self.current = None; + self.queue.current = None; + + if let Some(game) = self.queue.queued.pop() { + self.install_now(game).await?; + } + + self.queue.save().await?; + event = Some(MaximaEvent::InstallFailed(offer_id, err.to_string())); + } } Ok(event) diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index d8f2a598..605bbc57 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -5,9 +5,9 @@ use log::{debug, warn}; use reqwest::Client; use reqwest::header::ToStrError; use std::cmp; +use std::path::{Component, Path}; use std::string::FromUtf8Error; use thiserror::Error; -use std::path::{Component, Path}; /// This module is based on https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index 21007ee7..c5138fe5 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -552,10 +552,9 @@ fn extract_diskutil_volume_uuid(output: &str) -> Option<&str> { #[cfg(target_os = "windows")] fn get_c_drive_volume_serial() -> u32 { use std::ptr; - use winapi::shared::minwindef::DWORD; - use winapi::um::fileapi::GetVolumeInformationW; + use windows_sys::Win32::Storage::FileSystem::GetVolumeInformationW; - let mut serial: DWORD = 0; + let mut serial: u32 = 0; let res = unsafe { GetVolumeInformationW( "C:\\" diff --git a/maxima-lib/src/core/auth/login.rs b/maxima-lib/src/core/auth/login.rs index f28b88b6..73a2426f 100644 --- a/maxima-lib/src/core/auth/login.rs +++ b/maxima-lib/src/core/auth/login.rs @@ -1,14 +1,15 @@ use regex::Regex; +use std::sync::LazyLock; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::net::TcpListener; -use std::sync::LazyLock; use crate::core::{auth::storage::AuthError, clients::JUNO_PC_CLIENT_ID}; use super::context::AuthContext; static HTTP_PATTERN: LazyLock = LazyLock::new(|| { - Regex::new(r"^([A-Za-z]+) +(.*) +(HTTP/[0-9][.][0-9])").expect("HTTP pattern regex should be valid") + Regex::new(r"^([A-Za-z]+) +(.*) +(HTTP/[0-9][.][0-9])") + .expect("HTTP pattern regex should be valid") }); pub async fn begin_oauth_login_flow<'a>(context: &mut AuthContext<'a>) -> Result<(), AuthError> { diff --git a/maxima-lib/src/core/cache.rs b/maxima-lib/src/core/cache.rs index 8cd79bfb..7a13d0ca 100644 --- a/maxima-lib/src/core/cache.rs +++ b/maxima-lib/src/core/cache.rs @@ -31,6 +31,8 @@ impl DynamicCache { Q: Hash + Eq + ?Sized, T: Sync + Send + Clone + 'static, { - self.cache.get(key).map(|cached| (*cached.downcast::().unwrap()).clone()) + self.cache + .get(key) + .map(|cached| (*cached.downcast::().unwrap()).clone()) } } diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 01f235fd..49ae1fa8 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -19,7 +19,6 @@ use super::{ }; use crate::util::native::{NativeError, SafeParent, SafeStr}; use derive_getters::Getters; -use futures::StreamExt; use log::{debug, error}; use reqwest::{Client, ClientBuilder}; use serde::{Deserialize, Serialize}; @@ -33,6 +32,7 @@ use tokio::{ fs::{File, OpenOptions}, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, }; +use tokio_stream::StreamExt; const AUTH_HEADER: &str = "X-Origin-AuthToken"; const LOCK_HEADER: &str = "X-Origin-Sync-Lock"; @@ -614,7 +614,8 @@ impl CloudSyncClient { lock, mode, allowed_files, - ).await + ) + .await } } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index f6777d4a..e31054ed 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -75,8 +75,7 @@ impl OwnedOffer { pub async fn install_check_path(&self) -> Result { Ok(parse_registry_path( - self - .offer + self.offer .install_check_override() .as_ref() .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, @@ -137,8 +136,7 @@ impl OwnedOffer { } else { let path = PathBuf::from( parse_partial_registry_path( - self - .offer + self.offer .install_check_override() .as_ref() .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 9122be28..90c99043 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -82,7 +82,6 @@ macro_rules! dip_type { } } - dip_type!( FeatureFlags; attr { @@ -200,7 +199,6 @@ dip_type!( } ); - fn remove_leading_slash(path: &str) -> &str { path.strip_prefix('/').unwrap_or(path) } @@ -280,7 +278,7 @@ impl DiPManifest { pub async fn read(path: &Path) -> Result { let bytes = tokio::fs::read(path).await?; let string = bytes_to_string(bytes).ok_or(ManifestError::Decode)?; - + Ok(quick_xml::de::from_str(&string)?) } @@ -288,6 +286,16 @@ impl DiPManifest { &self.buildMetaData.gameVersion.attr_version } + pub fn locale(&self) -> &str { + self.gameTitles + .items + .iter() + .find(|t| t.locale == "en_US") + .map(|t| t.locale.as_str()) + .or_else(|| self.gameTitles.items.first().map(|t| t.locale.as_str())) + .unwrap_or("en_US") + } + pub fn execute_path(&self, trial: bool) -> Option<&str> { self.runtime .launcher @@ -296,12 +304,14 @@ impl DiPManifest { .map(|l| l.filePath.as_str()) } - fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { + fn collect_touchup_args( + &self, + install_path: &Path, + locale: &str, + ) -> Result, ManifestError> { let install_str = platform_path( - remove_trailing_backslash( - install_path.to_str().ok_or(ManifestError::Decode)? - ) - .replace('/', "\\"), + remove_trailing_backslash(install_path.to_str().ok_or(ManifestError::Decode)?) + .replace('/', "\\"), ) .to_str() .ok_or(ManifestError::Decode)? @@ -310,7 +320,7 @@ impl DiPManifest { let expanded = self .touchup .parameters - .replace("{locale}", "en_US") + .replace("{locale}", locale) .replace("{installLocation}", &install_str); split_args(&expanded) @@ -331,7 +341,7 @@ impl DiPManifest { let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, )); - let args = self.collect_touchup_args(&install_path)?; + let args = self.collect_touchup_args(&install_path, self.locale())?; let path = case_insensitive_path(install_path.join(self.touchup.path())); run_wine_command(path, Some(args), None, true, CommandType::Run).await?; @@ -344,14 +354,10 @@ impl DiPManifest { use crate::util::native::NativeError; use tokio::process::Command; - let args = self.collect_touchup_args(install_path)?; + let args = self.collect_touchup_args(install_path, self.locale())?; let path = install_path.join(self.touchup.path()); - let status = Command::new(&path) - .args(&args) - .spawn()? - .wait() - .await?; + let status = Command::new(&path).args(&args).spawn()?.wait().await?; if !status.success() { return Err(ManifestError::Native(NativeError::Command( @@ -361,4 +367,4 @@ impl DiPManifest { Ok(()) } -} \ No newline at end of file +} diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index 460e4367..bef5084a 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -47,7 +47,8 @@ impl GameManifest for DiPManifest { } fn execute_path(&self, trial: bool) -> Option { - self.execute_path(trial).map(std::string::ToString::to_string) + self.execute_path(trial) + .map(std::string::ToString::to_string) } fn version(&self) -> Option { diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 15f093b0..077ca833 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -156,7 +156,6 @@ fn bytes_to_string(bytes: Vec) -> Option { String::from_utf16(&u16_bytes).ok() } - impl PreDiPManifest { pub async fn read(path: &Path) -> Result { let bytes = tokio::fs::read(path).await?; @@ -168,12 +167,29 @@ impl PreDiPManifest { &self.attr_gameVersion } - fn collect_touchup_args(&self, install_path: &Path) -> Result, ManifestError> { + pub fn locale(&self) -> &str { + self.metadata + .localeInfo + .iter() + .find(|l| l.attr_locale == "en_US") + .map(|l| l.attr_locale.as_str()) + .or_else(|| { + self.metadata + .localeInfo + .first() + .map(|l| l.attr_locale.as_str()) + }) + .unwrap_or("en_US") + } + + fn collect_touchup_args( + &self, + install_path: &Path, + locale: &str, + ) -> Result, ManifestError> { let install_str = platform_path( - remove_trailing_backslash( - install_path.to_str().ok_or(ManifestError::Decode)? - ) - .replace('/', "\\"), + remove_trailing_backslash(install_path.to_str().ok_or(ManifestError::Decode)?) + .replace('/', "\\"), ) .to_str() .ok_or(ManifestError::Decode)? @@ -182,7 +198,7 @@ impl PreDiPManifest { let expanded = self .executable .parameters - .replace("{locale}", "en_US") + .replace("{locale}", locale) .replace("{installLocation}", &install_str); Self::split_args(&expanded) @@ -233,9 +249,9 @@ impl PreDiPManifest { let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, )); - let args = self.collect_touchup_args(&install_path)?; + let args = self.collect_touchup_args(&install_path, self.locale())?; let path = case_insensitive_path( - install_path.join(remove_leading_slash(&self.executable.filePath)) + install_path.join(remove_leading_slash(&self.executable.filePath)), ); run_wine_command(path, Some(args), None, true, CommandType::Run).await?; @@ -248,14 +264,10 @@ impl PreDiPManifest { use crate::util::native::NativeError; use tokio::process::Command; - let args = self.collect_touchup_args(install_path)?; + let args = self.collect_touchup_args(install_path, self.locale())?; let path = install_path.join(remove_leading_slash(&self.executable.filePath)); - let status = Command::new(&path) - .args(&args) - .spawn()? - .wait() - .await?; + let status = Command::new(&path).args(&args).spawn()?.wait().await?; if !status.success() { return Err(ManifestError::Native(NativeError::Command( @@ -265,4 +277,4 @@ impl PreDiPManifest { Ok(()) } -} \ No newline at end of file +} diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index fe5f1a1a..2920f195 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -74,6 +74,8 @@ pub enum MaximaEvent { ReceivedLSXRequest(u32, LSXRequestType), /// Offer ID. Use `maxima.mut_library().title_by_base_offer(id)` for details InstallFinished(String), + /// Offer ID, error message + InstallFailed(String, String), } pub type MaximaLSXEventCallback = extern "C" fn(*const c_char); @@ -431,7 +433,9 @@ impl Maxima { } pub fn set_player_started(&mut self) { - if let Some(playing) = &mut self.playing { playing.set_started() } + if let Some(playing) = &mut self.playing { + playing.set_started() + } } /// Call this as often as possible from the loop you consume events from @@ -455,11 +459,16 @@ impl Maxima { } let playing = self.playing.as_mut().unwrap(); - if let Ok(None) = playing.process_mut().try_wait() { return } + if let Ok(None) = playing.process_mut().try_wait() { + return; + } info!("Game stopped"); - if let Some(offer) = playing.offer() && *playing.cloud_saves() && offer.offer().has_cloud_save() { + if let Some(offer) = playing.offer() + && *playing.cloud_saves() + && offer.offer().has_cloud_save() + { let result = self .cloud_sync .obtain_lock(offer, CloudSyncLockMode::Write) diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index ad08e596..df329992 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use std::time::Duration; use thiserror::Error; use tokio::sync::{Mutex, Semaphore}; -use tokio::time::{sleep, Instant}; +use tokio::time::{Instant, sleep}; use derive_builder::Builder; use derive_getters::Getters; @@ -110,8 +110,8 @@ macro_rules! load_graphql_request { ($type:ident, $operation:expr, $key:expr) => {{ let content = include_str!(concat!("graphql/", $operation, ".gql")); let hash = sha2_const::Sha256::new() - .update(content.as_bytes()) - .finalize(); + .update(content.as_bytes()) + .finalize(); ServiceLayerGraphQLRequest { query: content, operation: $operation, @@ -197,12 +197,14 @@ impl ServiceLayerClient { const MAX_RETRIES: u32 = 3; const BASE_DELAY_MS: u64 = 500; - let _permit = self.semaphore.acquire().await.map_err(|_| { - ServiceLayerError::Http { + let _permit = self + .semaphore + .acquire() + .await + .map_err(|_| ServiceLayerError::Http { status_code: StatusCode::INTERNAL_SERVER_ERROR, message: "semaphore closed".to_string(), - } - })?; + })?; // Enforce minimum interval between requests without holding a lock across await let wait = { @@ -243,7 +245,8 @@ impl ServiceLayerClient { "Request `{}` returned {}; waiting {:?} before retry {}/{}", operation.operation, match &err { - ServiceLayerError::Http { status_code, .. } => status_code.to_string(), + ServiceLayerError::Http { status_code, .. } => + status_code.to_string(), _ => "error".to_string(), }, delay, @@ -619,16 +622,11 @@ service_layer_type!(AvailableBuild, { build_live_date: Option, }); -impl ServiceAvailableBuild { -} +impl ServiceAvailableBuild {} impl fmt::Display for ServiceAvailableBuild { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - self.game_version.as_deref().unwrap_or("UnkVer") - )?; + write!(f, "{}", self.game_version.as_deref().unwrap_or("UnkVer"))?; if let Some(download_type) = &self.download_type { write!(f, "/{download_type}")?; diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index a2a92df0..522a9404 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -1,6 +1,7 @@ use derive_getters::Getters; use log::{debug, error, warn}; use quick_xml::DeError; +use rand::rand_core::Rng; use regex::Regex; use std::{ io::{ErrorKind, Read, Write}, @@ -9,9 +10,11 @@ use std::{ }; use sysinfo::{Pid, System}; use thiserror::Error; -use tokio::{io::AsyncWriteExt, sync::{MutexGuard, RwLock}}; use tokio::net::TcpStream; -use rand::rand_core::{Rng}; +use tokio::{ + io::AsyncWriteExt, + sync::{MutexGuard, RwLock}, +}; use super::{ request::{ @@ -77,9 +80,8 @@ const CORE_SENDER: &str = "EALS"; const CHALLENGE_BUILD: &str = "release"; //const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; // Need to figure out how to generate this const CHALLENGE_VERSION: &str = "10,5,64,37936"; -static LSX_PATTERN: LazyLock = LazyLock::new(|| { - Regex::new(r".*?").expect("LSX pattern regex should be valid") -}); +static LSX_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r".*?").expect("LSX pattern regex should be valid")); macro_rules! lsx_message_matcher { ( @@ -218,7 +220,9 @@ impl Connection { // The PID system is mainly for Kyber injection let mut pid = get_os_pid(context); - if cfg!(unix) && let Ok(os_pid) = pid { + if cfg!(unix) + && let Ok(os_pid) = pid + { let sys = System::new_all(); if let Some(process) = sys.process(Pid::from_u32(os_pid)) { let filename = PathBuf::from( @@ -265,7 +269,11 @@ impl Connection { queued_messages: Vec::new(), })); - Ok(Self { maxima: maxima_arc, stream, state }) + Ok(Self { + maxima: maxima_arc, + stream, + state, + }) } // State @@ -295,7 +303,7 @@ impl Connection { let mut buffer = [0; 1024 * 8]; let n = match self.stream.try_read(&mut buffer) { Ok(0) => return Err(LSXConnectionError::Closed), - Ok(n) => n, + Ok(n) => n, Err(e) if e.kind() == ErrorKind::WouldBlock => return Ok(()), Err(e) => return Err(e.into()), }; diff --git a/maxima-lib/src/lsx/request/account.rs b/maxima-lib/src/lsx/request/account.rs index 886f47f1..7e4ed645 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -62,5 +62,5 @@ pub async fn handle_query_entitlements_request( }); } - return make_lsx_handler_response!(Response, QueryEntitlementsResponse, { entitlement: lsx_entitlements }) + return make_lsx_handler_response!(Response, QueryEntitlementsResponse, { entitlement: lsx_entitlements }); } diff --git a/maxima-lib/src/lsx/request/auth.rs b/maxima-lib/src/lsx/request/auth.rs index 029ec832..0e279024 100644 --- a/maxima-lib/src/lsx/request/auth.rs +++ b/maxima-lib/src/lsx/request/auth.rs @@ -35,5 +35,5 @@ pub async fn handle_auth_code_request( } }; - return make_lsx_handler_response!(Response, AuthCode, { attr_value: auth_code }) + return make_lsx_handler_response!(Response, AuthCode, { attr_value: auth_code }); } diff --git a/maxima-lib/src/lsx/request/challenge.rs b/maxima-lib/src/lsx/request/challenge.rs index 21c3f739..a6208d63 100644 --- a/maxima-lib/src/lsx/request/challenge.rs +++ b/maxima-lib/src/lsx/request/challenge.rs @@ -40,5 +40,5 @@ pub async fn handle_challenge_response( hex::encode(encryption_key), message.attr_version ); - return make_lsx_handler_response!(Response, ChallengeAccepted, { attr_response: accept_key }) + return make_lsx_handler_response!(Response, ChallengeAccepted, { attr_response: accept_key }); } diff --git a/maxima-lib/src/lsx/request/config.rs b/maxima-lib/src/lsx/request/config.rs index 1948b783..9da8579b 100644 --- a/maxima-lib/src/lsx/request/config.rs +++ b/maxima-lib/src/lsx/request/config.rs @@ -54,5 +54,5 @@ pub async fn handle_config_request( ) -> Result, LSXRequestError> { let services = &SERVICES; - return make_lsx_handler_response!(Response, GetConfigResponse, { service: services.to_vec() }) + return make_lsx_handler_response!(Response, GetConfigResponse, { service: services.to_vec() }); } diff --git a/maxima-lib/src/lsx/request/core.rs b/maxima-lib/src/lsx/request/core.rs index 818f382c..b8c3cddf 100644 --- a/maxima-lib/src/lsx/request/core.rs +++ b/maxima-lib/src/lsx/request/core.rs @@ -21,7 +21,7 @@ pub async fn handle_settings_request( MaximaSetting::Environment => "production".to_string(), }; - return make_lsx_handler_response!(Response, GetSettingResponse, { attr_Setting: setting }) + return make_lsx_handler_response!(Response, GetSettingResponse, { attr_Setting: setting }); } pub async fn handle_connectivity_request( @@ -29,7 +29,7 @@ pub async fn handle_connectivity_request( _: LSXGetInternetConnectedState, ) -> Result, LSXRequestError> { // TODO Actually check this - return make_lsx_handler_response!(Response, InternetConnectedState, { attr_connected: 1 }) + return make_lsx_handler_response!(Response, InternetConnectedState, { attr_connected: 1 }); } pub async fn handle_set_downloader_util_request( diff --git a/maxima-lib/src/lsx/request/game.rs b/maxima-lib/src/lsx/request/game.rs index 1c98310e..584b0528 100644 --- a/maxima-lib/src/lsx/request/game.rs +++ b/maxima-lib/src/lsx/request/game.rs @@ -25,7 +25,7 @@ pub async fn handle_game_info_request( LSXGameInfoId::InstalledLanguage => "en_US".to_string(), }; - return make_lsx_handler_response!(Response, GetGameInfoResponse, { attr_GameInfo: game_info }) + return make_lsx_handler_response!(Response, GetGameInfoResponse, { attr_GameInfo: game_info }); } // @@ -97,5 +97,5 @@ pub async fn handle_all_game_info_request( attr_InstalledLanguage: "en_US".to_string(), attr_FullGameReleaseDate: full_game_release_date, attr_SystemTime: "2023-06-22T04:00:00".to_string() - }) + }); } diff --git a/maxima-lib/src/lsx/request/license.rs b/maxima-lib/src/lsx/request/license.rs index 3ed109cc..f577cc7f 100644 --- a/maxima-lib/src/lsx/request/license.rs +++ b/maxima-lib/src/lsx/request/license.rs @@ -60,5 +60,5 @@ pub async fn handle_license_request( debug!("Got Denuvo Token: {}", token); - return make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }) + return make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }); } diff --git a/maxima-lib/src/lsx/request/offer.rs b/maxima-lib/src/lsx/request/offer.rs index aa3c4327..ac6b7b77 100644 --- a/maxima-lib/src/lsx/request/offer.rs +++ b/maxima-lib/src/lsx/request/offer.rs @@ -64,5 +64,5 @@ pub async fn handle_query_offers_request( }) } - return make_lsx_handler_response!(Response, QueryOffersResponse, { offer: rtn }) + return make_lsx_handler_response!(Response, QueryOffersResponse, { offer: rtn }); } diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index a1c5aa19..80ee07ea 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -39,20 +39,20 @@ pub async fn handle_profile_request( debug!("Got profile for {} {:?}", &name, path); return make_lsx_handler_response!(Response, GetProfileResponse, { - attr_Persona: name.to_owned(), - attr_SubscriberLevel: 0, - attr_CommerceCurrency: "USD".to_string(), - attr_IsTrialSubscriber: false, - attr_Country: "US".to_string(), - attr_UserId: user.id().parse::()?, - attr_GeoCountry: "US".to_string(), - attr_AvatarId: path.safe_str()?.to_string(), - attr_IsSubscriber: false, - attr_IsSteamSubscriber: false, - attr_PersonaId: player.psd().parse::()?, - attr_IsUnderAge: false, - attr_UserIndex: 0, - }); + attr_Persona: name.to_owned(), + attr_SubscriberLevel: 0, + attr_CommerceCurrency: "USD".to_string(), + attr_IsTrialSubscriber: false, + attr_Country: "US".to_string(), + attr_UserId: user.id().parse::()?, + attr_GeoCountry: "US".to_string(), + attr_AvatarId: path.safe_str()?.to_string(), + attr_IsSubscriber: false, + attr_IsSteamSubscriber: false, + attr_PersonaId: player.psd().parse::()?, + attr_IsUnderAge: false, + attr_UserIndex: 0, + }); } pub async fn handle_presence_request( @@ -60,17 +60,17 @@ pub async fn handle_presence_request( _: LSXGetPresence, ) -> Result, LSXRequestError> { return make_lsx_handler_response!(Response, GetPresenceResponse, { - attr_UserId: 1005663144213, - attr_Presence: LSXPresence::Ingame, - attr_Title: None, - attr_TitleId: None, - attr_MultiplayerId: None, - attr_RichPresence: None, - attr_GamePresence: None, - attr_SessionId: None, - attr_Group: None, - attr_GroupId: None, - }); + attr_UserId: 1005663144213, + attr_Presence: LSXPresence::Ingame, + attr_Title: None, + attr_TitleId: None, + attr_MultiplayerId: None, + attr_RichPresence: None, + attr_GamePresence: None, + attr_SessionId: None, + attr_Group: None, + attr_GroupId: None, + }); } pub async fn handle_set_presence_request( @@ -80,7 +80,8 @@ pub async fn handle_set_presence_request( info!( "Setting Presence to {:?}: {}", request.attr_Presence, - request.attr_RichPresence + request + .attr_RichPresence .to_owned() .unwrap_or(String::new()) ); diff --git a/maxima-lib/src/lsx/request/progressive_install.rs b/maxima-lib/src/lsx/request/progressive_install.rs index b25a6374..2cf72a5d 100644 --- a/maxima-lib/src/lsx/request/progressive_install.rs +++ b/maxima-lib/src/lsx/request/progressive_install.rs @@ -18,7 +18,7 @@ pub async fn handle_pi_availability_request( return make_lsx_handler_response!(Response, IsProgressiveInstallationAvailableResponse, { attr_Available: false, attr_ItemId: "Origin.OFR.50.0001456".to_string(), - }) + }); } pub async fn handle_pi_installed_chunks_request( @@ -29,5 +29,5 @@ pub async fn handle_pi_installed_chunks_request( attr_ItemId: "Origin.OFR.50.0001456".to_string(), attr_Installed: true, chunk_ids: request.chunk_ids, - }) + }); } diff --git a/maxima-lib/src/lsx/request/voip.rs b/maxima-lib/src/lsx/request/voip.rs index fce0e098..24ca32be 100644 --- a/maxima-lib/src/lsx/request/voip.rs +++ b/maxima-lib/src/lsx/request/voip.rs @@ -11,5 +11,5 @@ pub async fn handle_voip_status_request( _: LockedConnectionState, _: LSXGetVoipStatus, ) -> Result, LSXRequestError> { - return make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }) + return make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }); } diff --git a/maxima-lib/src/lsx/service.rs b/maxima-lib/src/lsx/service.rs index dfe20c9d..afcd0813 100644 --- a/maxima-lib/src/lsx/service.rs +++ b/maxima-lib/src/lsx/service.rs @@ -1,7 +1,7 @@ use std::{io::ErrorKind, time::Duration}; use log::{info, warn}; -use tokio::{time::sleep, net::TcpListener}; +use tokio::{net::TcpListener, time::sleep}; use crate::lsx::connection::LSXConnectionError; use crate::{core::LockedMaxima, lsx::connection::Connection}; @@ -61,7 +61,10 @@ pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServ let conn = Connection::new(maxima.clone(), socket).await; if conn.is_err() { - warn!("Failed to establish LSX connection: {}", conn.err().unwrap()); + warn!( + "Failed to establish LSX connection: {}", + conn.err().unwrap() + ); continue; } diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index bd8b0ccb..e15e8808 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -1,12 +1,7 @@ use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7}; use chrono::{DateTime, Duration, Utc}; use log::{debug, warn}; -use std::{ - fs::create_dir_all, - path::PathBuf, - string::FromUtf8Error, - sync::LazyLock, -}; +use std::{fs::create_dir_all, path::PathBuf, string::FromUtf8Error, sync::LazyLock}; use tokio::fs; use base64::{DecodeError, Engine, engine::general_purpose}; @@ -31,7 +26,7 @@ type Aes128CbcDec = cbc::Decryptor; static EMAIL_PATTERN: LazyLock = LazyLock::new(|| { Regex::new( - r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})" + r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})", ) .expect("email regex should be valid") }); diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index 1f0c0bf9..b7e6de02 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -206,9 +206,7 @@ impl RtmConnectionManager { .ok_or(RtmError::NoBody)? .body .ok_or(RtmError::NoBody)?), - Err(_) => Err(RtmError::Io(io::Error::other( - "Failed to receive response", - ))), + Err(_) => Err(RtmError::Io(io::Error::other("Failed to receive response"))), } } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index a8a7b660..71b557ef 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -436,9 +436,8 @@ pub async fn setup_wine_registry() -> Result<(), NativeError> { pub type WineRegistry = HashMap; -static MX_WINE_REGISTRY: LazyLock> = LazyLock::new(|| { - Mutex::new(WineRegistry::new()) -}); +static MX_WINE_REGISTRY: LazyLock> = + LazyLock::new(|| Mutex::new(WineRegistry::new())); async fn parse_wine_registry(file_path: &str) -> WineRegistry { let mut registry_map = MX_WINE_REGISTRY.lock().await; diff --git a/maxima-lib/src/util/dll_injector.rs b/maxima-lib/src/util/dll_injector.rs index 7d785325..6a38b1bf 100644 --- a/maxima-lib/src/util/dll_injector.rs +++ b/maxima-lib/src/util/dll_injector.rs @@ -1,17 +1,15 @@ -use std::ffi::CString; +use std::ffi::{CString, c_void}; use std::mem; use std::ptr; use thiserror::Error; -use winapi::shared::minwindef::LPVOID; -use winapi::um::errhandlingapi::GetLastError; -use winapi::um::handleapi::CloseHandle; -use winapi::um::libloaderapi::{GetModuleHandleA, GetProcAddress}; -use winapi::um::memoryapi::{VirtualAllocEx, VirtualFreeEx, WriteProcessMemory}; -use winapi::um::processthreadsapi::{CreateRemoteThread, OpenProcess}; -use winapi::um::synchapi::WaitForSingleObject; -use winapi::um::winbase::INFINITE; -use winapi::um::winnt::{ - HANDLE, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, PROCESS_ALL_ACCESS, +use windows_sys::Win32::{ + Foundation::{CloseHandle, GetLastError, HANDLE}, + System::Diagnostics::Debug::WriteProcessMemory, + System::LibraryLoader::{GetModuleHandleA, GetProcAddress}, + System::Memory::{ + MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx, + }, + System::Threading::{CreateRemoteThread, OpenProcess, PROCESS_ALL_ACCESS, WaitForSingleObject}, }; #[derive(Debug, Error)] @@ -77,7 +75,7 @@ impl DllInjector { let result = WriteProcessMemory( process_handle, remote_memory, - dll_path_bytes.as_ptr() as LPVOID, + dll_path_bytes.as_ptr() as *const c_void, dll_path_size, &mut bytes_written as *mut usize, ); @@ -87,15 +85,16 @@ impl DllInjector { } let kernel32_cstring = CString::new("kernel32.dll").unwrap(); - let kernel32_handle = GetModuleHandleA(kernel32_cstring.as_ptr()); + let kernel32_handle = GetModuleHandleA(kernel32_cstring.as_ptr() as *const u8); if kernel32_handle.is_null() { return Err(InjectionError::GetModuleHandleFailed(GetLastError())); } let load_library_cstring = CString::new("LoadLibraryA").unwrap(); - let load_library_addr = GetProcAddress(kernel32_handle, load_library_cstring.as_ptr()); + let load_library_addr = + GetProcAddress(kernel32_handle, load_library_cstring.as_ptr() as *const u8); - if load_library_addr.is_null() { + if load_library_addr.is_none() { return Err(InjectionError::GetProcAddressFailed(GetLastError())); } @@ -113,7 +112,7 @@ impl DllInjector { return Err(InjectionError::CreateRemoteThreadFailed(GetLastError())); } - WaitForSingleObject(thread_handle, INFINITE); + WaitForSingleObject(thread_handle, u32::MAX); CloseHandle(thread_handle); Ok(()) @@ -135,7 +134,7 @@ impl Drop for ProcessHandleGuard { struct RemoteMemoryGuard { process_handle: HANDLE, - address: LPVOID, + address: *mut c_void, } impl Drop for RemoteMemoryGuard { diff --git a/maxima-lib/src/util/lsx_crypto.rs b/maxima-lib/src/util/lsx_crypto.rs index d505b8f4..21ccabeb 100644 --- a/maxima-lib/src/util/lsx_crypto.rs +++ b/maxima-lib/src/util/lsx_crypto.rs @@ -101,4 +101,4 @@ impl Crypto { pub fn current_key(&self) -> [u8; 16] { self.key } -} \ No newline at end of file +} diff --git a/maxima-lib/src/util/mod.rs b/maxima-lib/src/util/mod.rs index 84b122ca..e1bfbbc7 100644 --- a/maxima-lib/src/util/mod.rs +++ b/maxima-lib/src/util/mod.rs @@ -1,12 +1,12 @@ pub mod github; pub mod hash; pub mod log; +pub mod lsx_crypto; pub mod native; pub mod registry; pub mod simple_crypto; pub mod system_profiler_utils; pub mod wmi_utils; -pub mod lsx_crypto; #[cfg(windows)] pub mod dll_injector; diff --git a/maxima-lib/src/util/native.rs b/maxima-lib/src/util/native.rs index 4a2a89ca..60795772 100644 --- a/maxima-lib/src/util/native.rs +++ b/maxima-lib/src/util/native.rs @@ -13,15 +13,12 @@ use std::{ }; #[cfg(windows)] -use winapi::{ - shared::windef::HWND, - um::{ - libloaderapi::{GetModuleFileNameW, GetModuleHandleW}, - wincon::GetConsoleWindow, - winuser::{ - EnumWindows, FindWindowA, GetWindowThreadProcessId, IsWindowVisible, - SetForegroundWindow, - }, +use windows_sys::Win32::{ + Foundation::{HWND, LPARAM}, + System::Console::GetConsoleWindow, + System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}, + UI::WindowsAndMessaging::{ + EnumWindows, FindWindowA, GetWindowThreadProcessId, IsWindowVisible, SetForegroundWindow, }, }; @@ -137,23 +134,20 @@ pub enum NativeError { } #[cfg(windows)] -unsafe extern "system" fn enum_windows_proc( - hwnd: HWND, - _l_param: winapi::shared::minwindef::LPARAM, -) -> winapi::shared::minwindef::BOOL { +unsafe extern "system" fn enum_windows_proc(hwnd: HWND, _l_param: LPARAM) -> i32 { let mut window_process_id: u32 = 0; unsafe { GetWindowThreadProcessId(hwnd, &mut window_process_id) }; if window_process_id != std::process::id() || unsafe { IsWindowVisible(hwnd) } == 0 { - return winapi::shared::minwindef::TRUE; + return 1; } if unsafe { IsWindowVisible(hwnd) } != 0 { unsafe { SetForegroundWindow(hwnd) }; } - winapi::shared::minwindef::TRUE + 1 } #[cfg(windows)] pub fn get_hwnd() -> Result { @@ -161,7 +155,7 @@ pub fn get_hwnd() -> Result { EnumWindows(Some(enum_windows_proc), 0); let window_name = CString::new("Maxima").expect("Failed to create native string"); - let mut hwnd = FindWindowA(std::ptr::null(), window_name.as_ptr()); + let mut hwnd = FindWindowA(std::ptr::null(), window_name.as_ptr() as *const u8); if !hwnd.is_null() { return Ok(hwnd); } @@ -198,9 +192,9 @@ pub fn module_path() -> Result { .collect::>(); maxima_mod_name.push(0); - let mut hmodule = unsafe { GetModuleHandleW(maxima_mod_name.as_mut_ptr()) }; + let mut hmodule = unsafe { GetModuleHandleW(maxima_mod_name.as_mut_ptr() as *const u16) }; if hmodule.is_null() { - hmodule = unsafe { GetModuleHandleW(std::ptr::null_mut()) }; + hmodule = unsafe { GetModuleHandleW(std::ptr::null()) }; } if hmodule.is_null() { diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 80467456..84f5e3eb 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -1,9 +1,3 @@ -#[cfg(windows)] -extern crate winapi; - -use std::path::PathBuf; -use thiserror::Error; - #[cfg(windows)] use std::ptr; @@ -11,16 +5,10 @@ use std::ptr; use widestring::U16CString; #[cfg(windows)] -use winapi::{ - shared::{minwindef::HKEY, winerror::ERROR_CANCELLED}, - um::{ - errhandlingapi::GetLastError, - shellapi::{SEE_MASK_NO_CONSOLE, SEE_MASK_NOCLOSEPROCESS, ShellExecuteExW}, - }, - um::{ - winnt::KEY_QUERY_VALUE, - winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW}, - }, +use windows_sys::Win32::{ + Foundation::{ERROR_CANCELLED, GetLastError}, + System::Registry::{HKEY, KEY_QUERY_VALUE, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}, + UI::Shell::{SEE_MASK_NO_CONSOLE, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, ShellExecuteExW}, }; #[cfg(windows)] @@ -29,8 +17,10 @@ use winreg::{ enums::{HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE}, }; +use std::path::PathBuf; #[cfg(unix)] use std::{collections::HashMap, env, fs}; +use thiserror::Error; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; @@ -264,8 +254,8 @@ pub fn launch_bootstrap() -> Result<(), NativeError> { let file = file.encode_utf16().chain(Some(0)).collect::>(); let parameters = parameters.encode_utf16().chain(Some(0)).collect::>(); - let mut shell_execute_info = winapi::um::shellapi::SHELLEXECUTEINFOW { - cbSize: std::mem::size_of::() as u32, + let mut shell_execute_info = SHELLEXECUTEINFOW { + cbSize: std::mem::size_of::() as u32, lpVerb: verb.as_ptr(), lpFile: file.as_ptr(), lpParameters: parameters.as_ptr(), diff --git a/maxima-lib/src/util/service_win.rs b/maxima-lib/src/util/service_win.rs index 8b344842..ace45002 100644 --- a/maxima-lib/src/util/service_win.rs +++ b/maxima-lib/src/util/service_win.rs @@ -3,14 +3,19 @@ use std::ffi::{CString, OsStr, OsString}; use std::path::PathBuf; use std::time::Duration; use widestring::U16CString; -use winapi::shared::sddl::{ - ConvertSecurityDescriptorToStringSecurityDescriptorW, - ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, -}; -use winapi::um::winnt::{DACL_SECURITY_INFORMATION, LPWSTR, PSECURITY_DESCRIPTOR}; -use winapi::um::winsvc::{ - OpenSCManagerA, OpenServiceA, QueryServiceObjectSecurity, SetServiceObjectSecurity, - SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, +use windows_sys::Win32::{ + Foundation::ERROR_INSUFFICIENT_BUFFER, + Security::{ + Authorization::{ + ConvertSecurityDescriptorToStringSecurityDescriptorW, + ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, + }, + DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, + }, + System::Services::{ + OpenSCManagerA, OpenServiceA, QueryServiceObjectSecurity, SetServiceObjectSecurity, + SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, + }, }; use windows_service::service::{ ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, ServiceType, @@ -79,13 +84,13 @@ pub fn register_service() -> Result<(), BackgroundServiceControlError> { pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlError> { let hscm = unsafe { OpenSCManagerA( std::ptr::null(), - CString::new("ServicesActive")?.as_ptr(), + CString::new("ServicesActive")?.as_ptr() as *const u8, SC_MANAGER_ALL_ACCESS, ) }; let hservice = unsafe { OpenServiceA( hscm, - CString::new(SERVICE_NAME)?.as_ptr(), + CString::new(SERVICE_NAME)?.as_ptr() as *const u8, SERVICE_ALL_ACCESS, ) }; @@ -107,7 +112,7 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro // The initial call failed; check if the error was related to buffer size let last_error = std::io::Error::last_os_error(); let raw_error = (&last_error).raw_os_error().unwrap() as u32; - if raw_error != winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER { + if raw_error != ERROR_INSUFFICIENT_BUFFER { return Err(BackgroundServiceControlError::ServiceObjectSecurity(last_error)); } } @@ -132,7 +137,7 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro } // Convert the security descriptor to a string - let mut sddl_string: LPWSTR = std::ptr::null_mut(); + let mut sddl_string: *mut u16 = std::ptr::null_mut(); let mut sddl_string_len: u32 = 0; let result = unsafe { ConvertSecurityDescriptorToStringSecurityDescriptorW( security_descriptor, diff --git a/maxima-lib/src/util/simple_crypto.rs b/maxima-lib/src/util/simple_crypto.rs index a4ac3170..27d5c69b 100644 --- a/maxima-lib/src/util/simple_crypto.rs +++ b/maxima-lib/src/util/simple_crypto.rs @@ -21,7 +21,9 @@ pub fn simple_encrypt(data: &[u8], key: &[u8; 16]) -> String { /// Decrypt hex-encoded AES-128-ECB-PKCS7 data. pub fn simple_decrypt(data: &[u8], key: &[u8; 16]) -> String { - let Ok(ciphertext) = hex::decode(data) else { return String::new() }; + let Ok(ciphertext) = hex::decode(data) else { + return String::new(); + }; Aes128EcbDec::new(key.into()) .decrypt_padded_vec::(&ciphertext) .ok() diff --git a/maxima-resources/Cargo.toml b/maxima-resources/Cargo.toml index 23191a8f..a4212c31 100644 --- a/maxima-resources/Cargo.toml +++ b/maxima-resources/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Sean Kahler "] edition = "2024" -[dependencies] +[target.'cfg(windows)'.dependencies] winres = "0.1.12" [lib] diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index 71c0456c..bd7b4769 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -22,7 +22,11 @@ hex = { workspace = true } thiserror = { workspace = true } [target.'cfg(windows)'.dependencies] -winapi = { workspace = true } +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_System_Threading", + "Win32_System_ProcessStatus", +] } winreg = { workspace = true } windows-service = { workspace = true } diff --git a/maxima-service/src/service/error.rs b/maxima-service/src/service/error.rs index a5b251f8..8c64fdd7 100644 --- a/maxima-service/src/service/error.rs +++ b/maxima-service/src/service/error.rs @@ -1,4 +1,4 @@ -use actix_web::{HttpResponse, error, http::header::ContentType, http::StatusCode}; +use actix_web::{HttpResponse, error, http::StatusCode, http::header::ContentType}; use thiserror::Error; #[derive(Error, Debug)] diff --git a/maxima-service/src/service/hash.rs b/maxima-service/src/service/hash.rs index 64e9f58e..f3911b04 100644 --- a/maxima-service/src/service/hash.rs +++ b/maxima-service/src/service/hash.rs @@ -4,14 +4,15 @@ use std::ffi::OsString; use std::fs; use std::os::windows::ffi::OsStringExt; use std::ptr::null_mut; -use winapi::um::handleapi::CloseHandle; -use winapi::um::processthreadsapi::OpenProcess; -use winapi::um::psapi::GetModuleFileNameExW; -use winapi::um::winnt::PROCESS_QUERY_INFORMATION; +use windows_sys::Win32::{ + Foundation::{CloseHandle, HANDLE}, + System::ProcessStatus::GetModuleFileNameExW, + System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION}, +}; pub fn get_sha256_hash_of_pid(pid: u32) -> Result<[u8; 32], NativeError> { unsafe { - let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); + let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); if process_handle.is_null() { return Err(NativeError::CantFindProcess); } diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 90215d0b..68a6dcd3 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -6,10 +6,10 @@ use maxima::util::registry::set_up_registry; use maxima::util::service::SERVICE_NAME; use std::ffi::OsString; use std::path::Path; -use tokio::sync::mpsc::{self, Receiver}; use std::thread; use std::time::Duration; use structured_logger::json::new_writer; +use tokio::sync::mpsc::{self, Receiver}; use windows_service::service::{ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, }; diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index b48aa163..91ff3366 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -15,13 +15,13 @@ base64 = { workspace = true } log = { workspace = true } regex = { workspace = true } inquire = { workspace = true } -futures = { workspace = true } ratatui = "0.30.2" strum = { workspace = true } color-eyre = "0.6.5" +tokio-stream = "0.1.18" [target.'cfg(windows)'.dependencies] -winapi = { workspace = true } +windows-sys = { workspace = true } winreg = { workspace = true } is_elevated = { workspace = true } diff --git a/maxima-tui/src/main.rs b/maxima-tui/src/main.rs index a1e64658..1b119d9c 100644 --- a/maxima-tui/src/main.rs +++ b/maxima-tui/src/main.rs @@ -1,10 +1,10 @@ use clap::{Parser, Subcommand}; -use futures::StreamExt; use inquire::Select; use log::{debug, error, info, warn}; use regex::Regex; use service::{BridgeThread, MaximaLibRequest, MaximaLibResponse}; +use tokio_stream::StreamExt; use std::{ io::stdout, @@ -53,9 +53,8 @@ use maxima::{ rtm::client::BasicPresence, }; -static MANUAL_LOGIN_PATTERN: LazyLock = LazyLock::new(|| { - Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid") -}); +static MANUAL_LOGIN_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid")); use anyhow::{Result, bail}; use color_eyre::config::HookBuilder; diff --git a/maxima-tui/src/service.rs b/maxima-tui/src/service.rs index 2220df0d..0a0318d8 100644 --- a/maxima-tui/src/service.rs +++ b/maxima-tui/src/service.rs @@ -48,12 +48,12 @@ impl BridgeThread { tokio::task::spawn(async move { let die_fallback = tx1.clone(); - + if let Err(err) = BridgeThread::run(rx1, tx1).await { let _ = die_fallback.send(MaximaLibResponse::InteractionThreadDiedResponse); panic!("Interact thread failed! {err}"); } - + info!("Interact thread shut down"); }); @@ -69,11 +69,12 @@ impl BridgeThread { .dummy_local_user(false) .load_auth_storage(true) .build()?, - ).await?; + ) + .await?; { let maxima = maxima_arc.lock().await; - + let Ok(()) = maxima.start_lsx(maxima_arc.clone()).await else { info!("LSX failed to start!"); return Ok(()); @@ -107,23 +108,27 @@ impl BridgeThread { let login = async || { let maxima = maxima_arc.lock().await; let mut auth_storage = maxima.auth_storage().lock().await; - + if !auth_storage.logged_in().await? { let res = login_flow().await?; auth_storage.add_account(&res).await?; } - tx1.send(MaximaLibResponse::LoginResponse(InteractThreadLoginResponse { - success: true, - name: maxima - .local_user() - .await? - .player() - .as_ref() - .unwrap() - .display_name() - .to_owned(), - })).await.ok(); + tx1.send(MaximaLibResponse::LoginResponse( + InteractThreadLoginResponse { + success: true, + name: maxima + .local_user() + .await? + .player() + .as_ref() + .unwrap() + .display_name() + .to_owned(), + }, + )) + .await + .ok(); Ok(()) }; diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index 3b32f96c..a65d6bcd 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -winapi = { workspace = true, features = [ "psapi", "wininet", "winerror", "iphlpapi", "tcpmib" ] } +windows-sys = { workspace = true, features = [ "Win32_System_ProcessStatus", "Win32_Networking_WinInet" ] } anyhow = { workspace = true } clap = { workspace = true } eframe = { version = "0.35.0", default-features = false, features = [ "persistence", "glow" ] } @@ -18,7 +18,6 @@ serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } log = { workspace = true } -egui_demo_lib = "0.35.0" puffin_http = "0.17.0" puffin = "0.20.0" rustls = { version = "0.23.41", features = ["aws-lc-rs"] } diff --git a/maxima-ui/src/bridge/get_friends.rs b/maxima-ui/src/bridge/get_friends.rs index da66ab08..9d38eda6 100644 --- a/maxima-ui/src/bridge/get_friends.rs +++ b/maxima-ui/src/bridge/get_friends.rs @@ -24,10 +24,12 @@ pub async fn get_friends_request( let friends = maxima.friends(0).await?; for friend in friends { - remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote( - crate::ui_image::UIImageType::Avatar(friend.id().to_string()), - friend.avatar().as_ref().unwrap().medium().path().to_string(), - )).ok(); + remote_provider_channel + .send(UIImageCacheLoaderCommand::ProvideRemote( + crate::ui_image::UIImageType::Avatar(friend.id().to_string()), + friend.avatar().as_ref().unwrap().medium().path().to_string(), + )) + .ok(); let friend_info = UIFriend { name: friend.display_name().to_string(), id: friend.id().to_string(), diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 1667f2e3..16c8e3bf 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -144,23 +144,29 @@ async fn handle_images( if !has_hero { if let Some(hero) = get_preferred_hero_image(&images_0).await { - channel.send(UIImageCacheLoaderCommand::ProvideRemote( - crate::ui_image::UIImageType::Hero(slug.clone()), - hero, - )).ok(); + channel + .send(UIImageCacheLoaderCommand::ProvideRemote( + crate::ui_image::UIImageType::Hero(slug.clone()), + hero, + )) + .ok(); } } if !has_logo { if let Some(logo) = get_logo_image(&images_0) { - channel.send(UIImageCacheLoaderCommand::ProvideRemote( - crate::ui_image::UIImageType::Logo(slug.clone()), - logo, - )).ok(); + channel + .send(UIImageCacheLoaderCommand::ProvideRemote( + crate::ui_image::UIImageType::Logo(slug.clone()), + logo, + )) + .ok(); } else { - channel.send(UIImageCacheLoaderCommand::Stub( - crate::ui_image::UIImageType::Logo(slug.clone()), - )).ok(); + channel + .send(UIImageCacheLoaderCommand::Stub( + crate::ui_image::UIImageType::Logo(slug.clone()), + )) + .ok(); } } @@ -174,10 +180,12 @@ async fn handle_images( if !has_background { if let Some(background_image) = get_preferred_bg_hero(&images_1) { - channel.send(UIImageCacheLoaderCommand::ProvideRemote( - crate::ui_image::UIImageType::Background(slug), - background_image, - )).ok(); + channel + .send(UIImageCacheLoaderCommand::ProvideRemote( + crate::ui_image::UIImageType::Background(slug), + background_image, + )) + .ok(); } } @@ -215,7 +223,10 @@ pub async fn get_games_request( match downloads.iter().find(|item| item.download_type() == "LIVE") { Some(item) => item, None => { - error!("Cannot find LIVE download for game with multiple downloads: {}", &slug); + error!( + "Cannot find LIVE download for game with multiple downloads: {}", + &slug + ); continue; } } @@ -249,10 +260,14 @@ pub async fn get_games_request( exe_override: String::new(), }; - channel.send(MaximaLibResponse::GameInfoResponse(InteractThreadGameListResponse { - game: game_info, - settings, - })).ok(); + channel + .send(MaximaLibResponse::GameInfoResponse( + InteractThreadGameListResponse { + game: game_info, + settings, + }, + )) + .ok(); let bg = maxima_dir()?.join("cache/ui/images/").join(&slug).join("background.jpg"); let game_hero = maxima_dir()?.join("cache/ui/images/").join(&slug).join("hero.jpg"); @@ -279,9 +294,10 @@ pub async fn get_games_request( has_background, channel_send, service_layer_send, - ).await; + ) + .await; }); - tokio::task::yield_now().await; + tokio::task::yield_now().await; } egui::Context::request_repaint(&ctx); diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 60a293e3..4e9cb3dc 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -1,7 +1,7 @@ use crate::{ + BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, bridge_thread::{self, BackendError}, views::downloads_view::QueuedDownload, - BackendStallState, GameDetails, GameDetailsWrapper, MaximaEguiApp, }; use log::{error, info, warn}; use tokio::sync::mpsc::error::TryRecvError; @@ -29,9 +29,13 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { app.user_name = res.you.display_name().clone(); app.user_id = res.you.id().clone(); app.backend_state = BackendStallState::BingChilling; - let _ = app.backend.backend_commander + let _ = app + .backend + .backend_commander .send(bridge_thread::MaximaLibRequest::GetGamesRequest); - let _ = app.backend.backend_commander + let _ = app + .backend + .backend_commander .send(bridge_thread::MaximaLibRequest::GetFriendsRequest); } LoginCacheEmpty => app.backend_state = BackendStallState::UserNeedsToLogIn, @@ -60,8 +64,13 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { NonFatalError(err) => app.nonfatal_errors.push(*err), ActiveGameChanged(slug) => app.playing_game = slug, LocateGameResponse(res) => { - if matches!(res, bridge_thread::InteractThreadLocateGameResponse::Success) { - let _ = app.backend.backend_commander + if matches!( + res, + bridge_thread::InteractThreadLocateGameResponse::Success + ) { + let _ = app + .backend + .backend_commander .send(bridge_thread::MaximaLibRequest::GetGamesRequest); } app.installer_state.locate_response = Some(res); @@ -103,6 +112,10 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { ); } } + DownloadFailed(offer, reason) => { + app.installing_now = None; + app.nonfatal_errors.push(BackendError::DownloadFailed(offer, reason)); + } } ctx.request_repaint(); } @@ -124,4 +137,4 @@ fn find_slug_for_offer( .find(|(_, g)| g.offer == offer) .map(|(slug, _)| slug.clone()) .unwrap_or_default() -} \ No newline at end of file +} diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index c8ba4175..c9e2ec11 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -16,10 +16,16 @@ use maxima::{ ContentManager, ContentManagerError, QueuedGameBuilder, QueuedGameBuilderError, }, core::{ - LockedMaxima, Maxima, MaximaCreationError, MaximaEvent, MaximaOptionsBuilder, MaximaOptionsBuilderError, auth::storage::{AuthError, TokenError}, launch::LaunchError, library::LibraryError, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ + LockedMaxima, Maxima, MaximaCreationError, MaximaEvent, MaximaOptionsBuilder, + MaximaOptionsBuilderError, + auth::storage::{AuthError, TokenError}, + launch::LaunchError, + library::LibraryError, + manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, + service_layer::{ ServiceGameImagesRequestBuilderError, ServiceHeroBackgroundImageRequestBuilderError, ServiceLayerError, ServicePlayer, - } + }, }, lsx::service::LSXServerError, rtm::RtmError, @@ -28,13 +34,15 @@ use maxima::{ registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; -use tokio::sync::mpsc::{ - Sender, UnboundedReceiver, UnboundedSender, error::{SendError, TryRecvError}, unbounded_channel, -}; use std::{ path::PathBuf, time::{Duration, SystemTime}, }; +use tokio::sync::mpsc::{ + Sender, UnboundedReceiver, UnboundedSender, + error::{SendError, TryRecvError}, + unbounded_channel, +}; // TODO(headassbtw): integrate these all into the enums pub struct InteractThreadLoginResponse { @@ -78,6 +86,9 @@ pub enum MaximaLibRequest { GetGameDetailsRequest(String), StartGameRequest(GameInfo, Option), InstallGameRequest(String, PathBuf), + CancelInstallRequest(String), + PauseInstallRequest(String), + MoveInstallToTopRequest(String), LocateGameRequest(String), ShutdownRequest, } @@ -97,6 +108,7 @@ pub enum MaximaLibResponse { ActiveGameChanged(Option), DownloadProgressChanged(String, InteractThreadDownloadProgressResponse), DownloadFinished(String), + DownloadFailed(String, String), DownloadQueueUpdate(Option, Vec), } pub struct BridgeThread { @@ -149,6 +161,8 @@ pub enum BackendError { NoLiveBuildAvailable, #[error("tried to perform an action that requires being logged in, but was logged out")] LoggedOut, + #[error("download failed for `{0}`: {1}")] + DownloadFailed(String, String), } impl BridgeThread { @@ -173,7 +187,10 @@ impl BridgeThread { .unwrap(); } - pub fn new(ctx: &Context, remote_provider_channel: UnboundedSender) -> Self { + pub fn new( + ctx: &Context, + remote_provider_channel: UnboundedSender, + ) -> Self { puffin::profile_function!(); let (backend_commander, backend_cmd_listener) = unbounded_channel(); let (backend_responder, backend_listener) = unbounded_channel(); @@ -184,7 +201,6 @@ impl BridgeThread { let backend_responder_for_task = backend_responder.clone(); let remote_provider_for_task = remote_provider_channel.clone(); - // you dare spawn a thread without catching my sneaky sneaky panics mister Potter ?? tokio::task::spawn(async move { let die_fallback = backend_responder_for_task.clone(); @@ -195,12 +211,13 @@ impl BridgeThread { rtm_responder, remote_provider_for_task, &context, - ).await { + ) + .await + { Ok(()) => (), Err(err) => { error!("BridgeThread task: run() returned Err: {}", err); - let _ = die_fallback - .send(MaximaLibResponse::NonFatalError(Box::from(err))); + let _ = die_fallback.send(MaximaLibResponse::NonFatalError(Box::from(err))); } } @@ -238,7 +255,9 @@ impl BridgeThread { core::background_service::request_registry_setup, util::{ registry::check_registry_validity, - service::{is_service_running, is_service_valid, register_service_user, start_service}, + service::{ + is_service_running, is_service_valid, register_service_user, start_service, + }, }, }; @@ -281,8 +300,8 @@ impl BridgeThread { .dummy_local_user(false) .load_auth_storage(true) .build()?, - ).await?; - + ) + .await?; let logged_in = { let maxima = maxima_arc.lock().await; @@ -302,7 +321,9 @@ impl BridgeThread { }; match request { MaximaLibRequest::LoginRequestOauth => { - login_oauth(maxima_arc.clone(), backend_responder.clone(), ctx).await.expect("// TODO(headassbtw): panic message"); + login_oauth(maxima_arc.clone(), backend_responder.clone(), ctx) + .await + .expect("// TODO(headassbtw): panic message"); break 'login; } MaximaLibRequest::ShutdownRequest => { @@ -318,15 +339,18 @@ impl BridgeThread { let user = maxima.local_user().await?; if logged_in { - backend_responder.send(MaximaLibResponse::LoginResponse(Ok( - InteractThreadLoginResponse { - you: user.player().as_ref().unwrap().to_owned(), - } - ))).ok(); + backend_responder + .send(MaximaLibResponse::LoginResponse(Ok( + InteractThreadLoginResponse { + you: user.player().as_ref().unwrap().to_owned(), + }, + ))) + .ok(); } let avatar_result: Result<(), BackendError> = (|| async { - let avatar = user.player() + let avatar = user + .player() .as_ref() .ok_or(ServiceLayerError::MissingField)? .avatar() @@ -336,29 +360,37 @@ impl BridgeThread { .path() .to_string(); - remote_provider_channel.send(UIImageCacheLoaderCommand::ProvideRemote( - crate::ui_image::UIImageType::Avatar(user.id().to_string()), - avatar, - )).ok(); + remote_provider_channel + .send(UIImageCacheLoaderCommand::ProvideRemote( + crate::ui_image::UIImageType::Avatar(user.id().to_string()), + avatar, + )) + .ok(); Ok(()) - })().await; - + })() + .await; + if let Err(e) = avatar_result { warn!("Failed to load user avatar: {}", e); // continue anyway } - + ctx.request_repaint(); } - let _ = EventThread::new(&ctx.clone(), maxima_arc.clone(), rtm_cmd_listener, rtm_responder); + let _ = EventThread::new( + &ctx.clone(), + maxima_arc.clone(), + rtm_cmd_listener, + rtm_responder, + ); let mut next_tick = SystemTime::now() + Duration::from_millis(50); let mut playing_cache: Option = None; 'main: loop { let now = SystemTime::now(); - + if now >= next_tick { next_tick = now + Duration::from_millis(50); let mut maxima = maxima_arc.lock().await; @@ -368,9 +400,11 @@ impl BridgeThread { Some(ctx) if playing_cache.is_none() => { if let Some(offer) = ctx.offer() { playing_cache = Some(offer.slug().clone()); - backend_responder.send(MaximaLibResponse::ActiveGameChanged( - Some(offer.slug().clone()) - )).ok(); + backend_responder + .send(MaximaLibResponse::ActiveGameChanged(Some( + offer.slug().clone(), + ))) + .ok(); } } None if playing_cache.is_some() => { @@ -381,20 +415,30 @@ impl BridgeThread { } if let Some(dl) = maxima.content_manager().current() { - backend_responder.send(MaximaLibResponse::DownloadProgressChanged( - dl.offer_id().to_string(), - InteractThreadDownloadProgressResponse { - bytes: dl.bytes_downloaded(), - bytes_total: dl.bytes_total(), - }, - )).ok(); + backend_responder + .send(MaximaLibResponse::DownloadProgressChanged( + dl.offer_id().to_string(), + InteractThreadDownloadProgressResponse { + bytes: dl.bytes_downloaded(), + bytes_total: dl.bytes_total(), + }, + )) + .ok(); } for ev in maxima.consume_pending_events() { match ev { MaximaEvent::ReceivedLSXRequest(_, _) => {} MaximaEvent::InstallFinished(offer_id) => { - backend_responder.send(MaximaLibResponse::DownloadFinished(offer_id)).ok(); + backend_responder + .send(MaximaLibResponse::DownloadFinished(offer_id)) + .ok(); + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + } + MaximaEvent::InstallFailed(offer_id, reason) => { + backend_responder + .send(MaximaLibResponse::DownloadFailed(offer_id, reason)) + .ok(); Self::update_queue(maxima.content_manager(), backend_responder.clone()); } } @@ -417,18 +461,37 @@ impl BridgeThread { let remote = remote_provider_channel.clone(); let context = ctx.clone(); tokio::task::spawn(async move { - if let Err(e) = get_games_request(maxima, responder.clone(), remote, &context).await { - let _ = responder.send(MaximaLibResponse::NonFatalError(Box::from(e))).ok(); + if let Err(e) = + get_games_request(maxima, responder.clone(), remote, &context).await + { + let _ = + responder.send(MaximaLibResponse::NonFatalError(Box::from(e))).ok(); } }); Ok(()) } MaximaLibRequest::GetFriendsRequest => { - let f = async || get_friends_request(maxima_arc.clone(), backend_responder.clone(), remote_provider_channel.clone(), ctx).await; + let f = async || { + get_friends_request( + maxima_arc.clone(), + backend_responder.clone(), + remote_provider_channel.clone(), + ctx, + ) + .await + }; f().await } MaximaLibRequest::GetGameDetailsRequest(slug) => { - let f = async || game_details_request(maxima_arc.clone(), slug, backend_responder.clone(), ctx).await; + let f = async || { + game_details_request( + maxima_arc.clone(), + slug, + backend_responder.clone(), + ctx, + ) + .await + }; f().await } MaximaLibRequest::LocateGameRequest(mut path) => { @@ -442,26 +505,26 @@ impl BridgeThread { let path = PathBuf::from(path); let manifest_path = path.join(MANIFEST_RELATIVE_PATH); - + let response = match manifest::read(manifest_path.clone()).await { Ok(manifest) => match manifest.run_touchup(&path).await { Ok(()) => InteractThreadLocateGameResponse::Success, - Err(err) => InteractThreadLocateGameResponse::Error( - InteractThreadLocateGameFailure { - reason: err, - xml_path: manifest_path.to_string_lossy().into_owned(), - } - ), - } + Err(err) => { + warn!("Touchup failed during locate, treating as success: {}", err); + InteractThreadLocateGameResponse::Success + } + }, Err(err) => InteractThreadLocateGameResponse::Error( InteractThreadLocateGameFailure { reason: err, xml_path: manifest_path.to_string_lossy().into_owned(), - } + }, ), }; - let _ = backend_responder.send(MaximaLibResponse::LocateGameResponse(response)).ok(); + let _ = backend_responder + .send(MaximaLibResponse::LocateGameResponse(response)) + .ok(); info!("finished locating"); ctx.request_repaint(); Ok(()) @@ -469,10 +532,9 @@ impl BridgeThread { MaximaLibRequest::InstallGameRequest(offer, path) => { let mut maxima = maxima_arc.lock().await; - let builds = maxima.content_manager().service().available_builds(&offer).await?; - let build = builds - .live_build() - .ok_or(BackendError::NoLiveBuildAvailable)?; + let builds = + maxima.content_manager().service().available_builds(&offer).await?; + let build = builds.live_build().ok_or(BackendError::NoLiveBuildAvailable)?; let game = QueuedGameBuilder::default() .offer_id(offer) @@ -481,6 +543,24 @@ impl BridgeThread { .build()?; Ok(maxima.content_manager().add_install(game).await?) } + MaximaLibRequest::CancelInstallRequest(offer) => { + let mut maxima = maxima_arc.lock().await; + maxima.content_manager().cancel_install(&offer).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + Ok(()) + } + MaximaLibRequest::PauseInstallRequest(offer) => { + let mut maxima = maxima_arc.lock().await; + maxima.content_manager().pause_install(&offer).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + Ok(()) + } + MaximaLibRequest::MoveInstallToTopRequest(offer) => { + let mut maxima = maxima_arc.lock().await; + maxima.content_manager().move_install_to_top(&offer).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + Ok(()) + } MaximaLibRequest::StartGameRequest(info, settings) => { Ok(start_game_request(maxima_arc.clone(), info, settings).await?) } @@ -491,9 +571,8 @@ impl BridgeThread { }; if let Err(err) = result { - let _ = backend_responder - .send(MaximaLibResponse::NonFatalError(Box::from(err))) - .ok(); + let _ = + backend_responder.send(MaximaLibResponse::NonFatalError(Box::from(err))).ok(); } puffin::GlobalProfiler::lock().new_frame(); diff --git a/maxima-ui/src/event_thread.rs b/maxima-ui/src/event_thread.rs index 0c415712..b82eb068 100644 --- a/maxima-ui/src/event_thread.rs +++ b/maxima-ui/src/event_thread.rs @@ -1,5 +1,8 @@ use egui::Context; -use tokio::{sync::mpsc::{UnboundedSender, UnboundedReceiver}, time::Duration}; +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + time::Duration, +}; use crate::bridge_thread::BackendError; use log::{error, info}; @@ -120,4 +123,4 @@ impl EventThread { } } } -} \ No newline at end of file +} diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 1df447e3..e797aaad 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -1,3 +1,4 @@ +use crate::ui_image::{UIImageCache, UIImageType}; use clap::Parser; use desktop::check_desktop_icon; use egui::{ @@ -11,7 +12,6 @@ use log::error; use maxima::{core::library::OwnedOffer, util::log::init_logger}; use std::{collections::HashMap, ops::RangeInclusive, path::PathBuf, sync::Arc}; use strum_macros::EnumIter; -use crate::ui_image::{UIImageCache, UIImageType}; use views::{ debug_view::debug_view, downloads_view::{QueuedDownload, downloads_view}, @@ -27,8 +27,8 @@ use eframe::egui_glow; use egui_extras::{Size, StripBuilder}; use egui_glow::glow; -use crate::renderers::app_bg_renderer::AppBgRenderer; use crate::bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameResponse}; +use crate::renderers::app_bg_renderer::AppBgRenderer; use crate::renderers::game_view_bg_renderer::GameViewBgRenderer; use crate::translation_manager::{TranslationManager, positional_replace}; @@ -1033,9 +1033,10 @@ impl MaximaEguiApp { ) .clicked() { - let _ = self.backend.backend_commander.send( - bridge_thread::MaximaLibRequest::LoginRequestOauth, - ); + let _ = self + .backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::LoginRequestOauth); self.backend_state = BackendStallState::LoggingIn; } } @@ -1152,9 +1153,10 @@ impl MaximaEguiApp { ) .clicked() { - self.backend.backend_commander.send( - bridge_thread::MaximaLibRequest::StartService, - ).unwrap_or(()); + self.backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::StartService) + .unwrap_or(()); self.backend_state = BackendStallState::Starting; } } @@ -1184,9 +1186,8 @@ impl eframe::App for MaximaEguiApp { } fn on_exit(&mut self, _gl: Option<&glow::Context>) { - let _ = self.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::ShutdownRequest); + let _ = + self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::ShutdownRequest); } fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { diff --git a/maxima-ui/src/ui_image.rs b/maxima-ui/src/ui_image.rs index a6b8170c..7ebaa2e1 100644 --- a/maxima-ui/src/ui_image.rs +++ b/maxima-ui/src/ui_image.rs @@ -5,12 +5,12 @@ use std::{ fmt::Display, fs, path::PathBuf, - sync::{ - Arc, Mutex, - }, + sync::{Arc, Mutex}, +}; +use tokio::sync::mpsc::{ + UnboundedReceiver, UnboundedSender, error::TryRecvError, unbounded_channel, }; use tokio::{fs::File, io}; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender, error::TryRecvError}; use log::{debug, error, info}; diff --git a/maxima-ui/src/util/easy_mark/easy_mark_editor.rs b/maxima-ui/src/util/easy_mark/easy_mark_editor.rs new file mode 100644 index 00000000..9c0ea256 --- /dev/null +++ b/maxima-ui/src/util/easy_mark/easy_mark_editor.rs @@ -0,0 +1,276 @@ +// Copied from https://github.com/emilk/egui/tree/main/crates/egui_demo_lib/src/easy_mark +// Prevent using a whole demo lib for easy_mark. + +use egui::{ + Key, KeyboardShortcut, Modifiers, ScrollArea, TextBuffer, TextEdit, Ui, text::CCursorRange, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +#[serde(default)] +pub struct EasyMarkEditor { + code: String, + highlight_editor: bool, + show_rendered: bool, + + #[serde(skip)] + highlighter: crate::util::easy_mark::MemoizedEasymarkHighlighter, +} + +impl PartialEq for EasyMarkEditor { + fn eq(&self, other: &Self) -> bool { + (&self.code, self.highlight_editor, self.show_rendered) + == (&other.code, other.highlight_editor, other.show_rendered) + } +} + +impl Default for EasyMarkEditor { + fn default() -> Self { + Self { + code: DEFAULT_CODE.trim().to_owned(), + highlight_editor: true, + show_rendered: true, + highlighter: Default::default(), + } + } +} + +impl EasyMarkEditor { + pub fn panels(&mut self, ui: &mut egui::Ui) { + egui::CentralPanel::default().show(ui, |ui| { + self.ui(ui); + }); + } + + pub fn ui(&mut self, ui: &mut egui::Ui) { + egui::Grid::new("controls").show(ui, |ui| { + let _response = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui); + ui.checkbox(&mut self.show_rendered, "Show rendered"); + ui.checkbox(&mut self.highlight_editor, "Highlight editor"); + egui::reset_button(ui, self, "Reset"); + ui.end_row(); + }); + ui.separator(); + + if self.show_rendered { + ui.columns(2, |columns| { + ScrollArea::vertical() + .id_salt("source") + .show(&mut columns[0], |ui| self.editor_ui(ui)); + ScrollArea::vertical().id_salt("rendered").show(&mut columns[1], |ui| { + // TODO(emilk): we can save some more CPU by caching the rendered output. + crate::util::easy_mark::easy_mark(ui, &self.code); + }); + }); + } else { + ScrollArea::vertical().id_salt("source").show(ui, |ui| self.editor_ui(ui)); + } + } + + fn editor_ui(&mut self, ui: &mut egui::Ui) { + let Self { + code, highlighter, .. + } = self; + + let response = if self.highlight_editor { + let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| { + let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str()); + layout_job.wrap.max_width = wrap_width; + ui.fonts_mut(|f| f.layout_job(layout_job)) + }; + + ui.add( + egui::TextEdit::multiline(code) + .desired_width(f32::INFINITY) + .font(egui::TextStyle::Monospace) // for cursor height + .layouter(&mut layouter), + ) + } else { + ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY)) + }; + + if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) + && let Some(mut ccursor_range) = state.cursor.char_range() + { + let any_change = shortcuts(ui, code, &mut ccursor_range); + if any_change { + state.cursor.set_char_range(Some(ccursor_range)); + state.store(ui.ctx(), response.id); + } + } + } +} + +pub const SHORTCUT_BOLD: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::B); +pub const SHORTCUT_CODE: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::N); +pub const SHORTCUT_ITALICS: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::I); +pub const SHORTCUT_SUBSCRIPT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::L); +pub const SHORTCUT_SUPERSCRIPT: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::COMMAND, Key::Y); +pub const SHORTCUT_STRIKETHROUGH: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::Q); +pub const SHORTCUT_UNDERLINE: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::W); +pub const SHORTCUT_INDENT: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::E); + +fn nested_hotkeys_ui(ui: &mut egui::Ui) { + egui::Grid::new("shortcuts").striped(true).show(ui, |ui| { + let mut label = |shortcut, what| { + ui.label(what); + ui.weak(ui.ctx().format_shortcut(&shortcut)); + ui.end_row(); + }; + + label(SHORTCUT_BOLD, "*bold*"); + label(SHORTCUT_CODE, "`code`"); + label(SHORTCUT_ITALICS, "/italics/"); + label(SHORTCUT_SUBSCRIPT, "$subscript$"); + label(SHORTCUT_SUPERSCRIPT, "^superscript^"); + label(SHORTCUT_STRIKETHROUGH, "~strikethrough~"); + label(SHORTCUT_UNDERLINE, "_underline_"); + label(SHORTCUT_INDENT, "two spaces"); // Placeholder for tab indent + }); +} + +fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool { + let mut any_change = false; + + if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) { + // This is a placeholder till we can indent the active line + any_change = true; + let [primary, _secondary] = ccursor_range.sorted_cursors(); + + let advance = code.insert_text(" ", primary.index); + ccursor_range.primary.index += advance; + ccursor_range.secondary.index += advance; + } + + for (shortcut, surrounding) in [ + (SHORTCUT_BOLD, "*"), + (SHORTCUT_CODE, "`"), + (SHORTCUT_ITALICS, "/"), + (SHORTCUT_SUBSCRIPT, "$"), + (SHORTCUT_SUPERSCRIPT, "^"), + (SHORTCUT_STRIKETHROUGH, "~"), + (SHORTCUT_UNDERLINE, "_"), + ] { + if ui.input_mut(|i| i.consume_shortcut(&shortcut)) { + any_change = true; + toggle_surrounding(code, ccursor_range, surrounding); + } + } + + any_change +} + +/// E.g. toggle *strong* with `toggle_surrounding(&mut text, &mut cursor, "*")` +fn toggle_surrounding( + code: &mut dyn TextBuffer, + ccursor_range: &mut CCursorRange, + surrounding: &str, +) { + let [primary, secondary] = ccursor_range.sorted_cursors(); + + let surrounding_ccount = surrounding.chars().count(); + + let prefix_crange = primary.index.saturating_sub(surrounding_ccount)..primary.index; + let suffix_crange = secondary.index..secondary.index.saturating_add(surrounding_ccount); + let already_surrounded = code.char_range(prefix_crange.clone()) == surrounding + && code.char_range(suffix_crange.clone()) == surrounding; + + if already_surrounded { + code.delete_char_range(suffix_crange); + code.delete_char_range(prefix_crange); + ccursor_range.primary.index -= surrounding_ccount; + ccursor_range.secondary.index -= surrounding_ccount; + } else { + code.insert_text(surrounding, secondary.index); + let advance = code.insert_text(surrounding, primary.index); + + ccursor_range.primary.index += advance; + ccursor_range.secondary.index += advance; + } +} + +// ---------------------------------------------------------------------------- + +const DEFAULT_CODE: &str = r#" +# EasyMark +EasyMark is a markup language, designed for extreme simplicity. + +``` +WARNING: EasyMark is still an evolving specification, +and is also missing some features. +``` + +---------------- + +# At a glance +- inline text: + - normal, `code`, *strong*, ~strikethrough~, _underline_, /italics/, ^raised^, $small$ + - `\` escapes the next character + - [hyperlink](https://github.com/emilk/egui) + - Embedded URL: +- `# ` header +- `---` separator (horizontal line) +- `> ` quote +- `- ` bullet list +- `1. ` numbered list +- \`\`\` code fence +- a^2^ + b^2^ = c^2^ +- $Remember to read the small print$ + +# Design +> /"Why do what everyone else is doing, when everyone else is already doing it?" +> \- Emil + +Goals: +1. easy to parse +2. easy to learn +3. similar to markdown + +[The reference parser](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion. + +There is never more than one way to accomplish the same thing, and each special character is only used for one thing. For instance `*` is used for *strong* and `-` is used for bullet lists. There is no alternative way to specify the *strong* style or getting a bullet list. + +Similarity to markdown is kept when possible, but with much less ambiguity and some improvements (like _underlining_). + +# Details +All style changes are single characters, so it is `*strong*`, NOT `**strong**`. Style is reset by a matching character, or at the end of the line. + +Style change characters and escapes (`\`) work everywhere except for in inline code, code blocks and in URLs. + +You can mix styles. For instance: /italics _underline_/ and *strong `code`*. + +You can use styles on URLs: ~my webpage is at ~. + +Newlines are preserved. If you want to continue text on the same line, just do so. Alternatively, escape the newline by ending the line with a backslash (`\`). \ +Escaping the newline effectively ignores it. + +The style characters are chosen to be similar to what they are representing: + `_` = _underline_ + `~` = ~strikethrough~ (`-` is used for bullet points) + `/` = /italics/ + `*` = *strong* + `$` = $small$ + `^` = ^raised^ + +# To do +- Sub-headers (`## h2`, `### h3` etc) +- Hotkey Editor +- International keyboard algorithm for non-letter keys +- ALT+SHIFT+Num1 is not a functioning hotkey +- Tab Indent Increment/Decrement CTRL+], CTRL+[ + +- Images + - we want to be able to optionally specify size (width and\/or height) + - centering of images is very desirable + - captioning (image with a text underneath it) + - `![caption=My image][width=200][center](url)` ? +- Nicer URL:s + - `` and `[url](url)` do the same thing yet look completely different. + - let's keep similarity with images +- Tables +- Inspiration: +"#; diff --git a/maxima-ui/src/util/easy_mark/easy_mark_highlighter.rs b/maxima-ui/src/util/easy_mark/easy_mark_highlighter.rs new file mode 100644 index 00000000..cf960274 --- /dev/null +++ b/maxima-ui/src/util/easy_mark/easy_mark_highlighter.rs @@ -0,0 +1,192 @@ +// Copied from https://github.com/emilk/egui/tree/main/crates/egui_demo_lib/src/easy_mark +// Prevent using a whole demo lib for easy_mark. + +use crate::util::easy_mark::easy_mark_parser; + +/// Highlight easymark, memoizing previous output to save CPU. +/// +/// In practice, the highlighter is fast enough not to need any caching. +#[derive(Default)] +pub struct MemoizedEasymarkHighlighter { + style: egui::Style, + code: String, + output: egui::text::LayoutJob, +} + +impl MemoizedEasymarkHighlighter { + pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob { + if (&self.style, self.code.as_str()) != (egui_style, code) { + self.style = egui_style.clone(); + code.clone_into(&mut self.code); + self.output = highlight_easymark(egui_style, code); + } + self.output.clone() + } +} + +pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::text::LayoutJob { + let mut job = egui::text::LayoutJob::default(); + let mut style = easy_mark_parser::Style::default(); + let mut start_of_line = true; + + while !text.is_empty() { + if start_of_line && text.starts_with("```") { + let end = text.find("\n```").map_or_else(|| text.len(), |i| i + 4); + job.append( + &text[..end], + 0.0, + format_from_style( + egui_style, + &easy_mark_parser::Style { + code: true, + ..Default::default() + }, + ), + ); + text = &text[end..]; + style = Default::default(); + continue; + } + + if text.starts_with('`') { + style.code = true; + let end = text[1..].find(&['`', '\n'][..]).map_or_else(|| text.len(), |i| i + 2); + job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); + text = &text[end..]; + style.code = false; + continue; + } + + let mut skip; + + if text.starts_with('\\') && text.len() >= 2 { + skip = 2; + } else if start_of_line && text.starts_with(' ') { + // we don't preview indentation, because it is confusing + skip = 1; + } else if start_of_line && text.starts_with("# ") { + style.heading = true; + skip = 2; + } else if start_of_line && text.starts_with("> ") { + style.quoted = true; + skip = 2; + // we don't preview indentation, because it is confusing + } else if start_of_line && text.starts_with("- ") { + skip = 2; + // we don't preview indentation, because it is confusing + } else if text.starts_with('*') { + skip = 1; + if style.strong { + // Include the character that is ending this style: + job.append(&text[..skip], 0.0, format_from_style(egui_style, &style)); + text = &text[skip..]; + skip = 0; + } + style.strong ^= true; + } else if text.starts_with('$') { + skip = 1; + if style.small { + // Include the character that is ending this style: + job.append(&text[..skip], 0.0, format_from_style(egui_style, &style)); + text = &text[skip..]; + skip = 0; + } + style.small ^= true; + } else if text.starts_with('^') { + skip = 1; + if style.raised { + // Include the character that is ending this style: + job.append(&text[..skip], 0.0, format_from_style(egui_style, &style)); + text = &text[skip..]; + skip = 0; + } + style.raised ^= true; + } else { + skip = 0; + } + // Note: we don't preview underline, strikethrough and italics because it confuses things. + + // Swallow everything up to the next special character: + let line_end = text[skip..].find('\n').map_or_else(|| text.len(), |i| skip + i + 1); + let end = text[skip..] + .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..]) + .map_or_else(|| text.len(), |i| (skip + i).max(1)); + + if line_end <= end { + job.append( + &text[..line_end], + 0.0, + format_from_style(egui_style, &style), + ); + text = &text[line_end..]; + start_of_line = true; + style = Default::default(); + } else { + job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); + text = &text[end..]; + start_of_line = false; + } + } + + job +} + +fn format_from_style( + egui_style: &egui::Style, + emark_style: &easy_mark_parser::Style, +) -> egui::text::TextFormat { + use egui::{Align, Color32, Stroke, TextStyle}; + + let color = if emark_style.strong || emark_style.heading { + egui_style.visuals.strong_text_color() + } else if emark_style.quoted { + egui_style.visuals.weak_text_color() + } else { + egui_style.visuals.text_color() + }; + + let text_style = if emark_style.heading { + TextStyle::Heading + } else if emark_style.code { + TextStyle::Monospace + } else if emark_style.small | emark_style.raised { + TextStyle::Small + } else { + TextStyle::Body + }; + + let background = if emark_style.code { + egui_style.visuals.code_bg_color + } else { + Color32::TRANSPARENT + }; + + let underline = if emark_style.underline { + Stroke::new(1.0, color) + } else { + Stroke::NONE + }; + + let strikethrough = if emark_style.strikethrough { + Stroke::new(1.0, color) + } else { + Stroke::NONE + }; + + let valign = if emark_style.raised { + Align::TOP + } else { + Align::BOTTOM + }; + + egui::text::TextFormat { + font_id: text_style.resolve(egui_style), + color, + background, + italics: emark_style.italics, + underline, + strikethrough, + valign, + ..Default::default() + } +} diff --git a/maxima-ui/src/util/easy_mark/easy_mark_parser.rs b/maxima-ui/src/util/easy_mark/easy_mark_parser.rs new file mode 100644 index 00000000..29326ff8 --- /dev/null +++ b/maxima-ui/src/util/easy_mark/easy_mark_parser.rs @@ -0,0 +1,359 @@ +// Copied from https://github.com/emilk/egui/tree/main/crates/egui_demo_lib/src/easy_mark +// Prevent using a whole demo lib for easy_mark. + +//! A parser for `EasyMark`: a very simple markup language. +//! +//! WARNING: `EasyMark` is subject to change. +// +//! # `EasyMark` design goals: +//! 1. easy to parse +//! 2. easy to learn +//! 3. similar to markdown + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Item<'a> { + /// `\n` + // TODO(emilk): add Style here so empty heading still uses up the right amount of space. + Newline, + + /// Text + Text(Style, &'a str), + + /// title, url + Hyperlink(Style, &'a str, &'a str), + + /// leading space before e.g. a [`Self::BulletPoint`]. + Indentation(usize), + + /// > + QuoteIndent, + + /// - a point well made. + BulletPoint, + + /// 1. numbered list. The string is the number(s). + NumberedPoint(&'a str), + + /// --- + Separator, + + /// language, code + CodeBlock(&'a str, &'a str), +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Style { + /// # heading (large text) + pub heading: bool, + + /// > quoted (slightly dimmer color or other font style) + pub quoted: bool, + + /// `code` (monospace, some other color) + pub code: bool, + + /// self.strong* (emphasized, e.g. bold) + pub strong: bool, + + /// _underline_ + pub underline: bool, + + /// ~strikethrough~ + pub strikethrough: bool, + + /// /italics/ + pub italics: bool, + + /// $small$ + pub small: bool, + + /// ^raised^ + pub raised: bool, +} + +/// Parser for the `EasyMark` markup language. +/// +/// See the module-level documentation for details. +/// +/// # Example: +/// ``` +/// # use egui_demo_lib::easy_mark::parser::Parser; +/// for item in Parser::new("Hello *world*!") { +/// } +/// +/// ``` +pub struct Parser<'a> { + /// The remainder of the input text + s: &'a str, + + /// Are we at the start of a line? + start_of_line: bool, + + /// Current self.style. Reset after a newline. + style: Style, +} + +impl<'a> Parser<'a> { + pub fn new(s: &'a str) -> Self { + Self { + s, + start_of_line: true, + style: Style::default(), + } + } + + /// `1. `, `42. ` etc. + fn numbered_list(&mut self) -> Option> { + let n_digits = self.s.chars().take_while(|c| c.is_ascii_digit()).count(); + if n_digits > 0 && self.s.chars().skip(n_digits).take(2).eq(". ".chars()) { + let number = &self.s[..n_digits]; + self.s = &self.s[(n_digits + 2)..]; + self.start_of_line = false; + return Some(Item::NumberedPoint(number)); + } + None + } + + // ```{language}\n{code}``` + fn code_block(&mut self) -> Option> { + if let Some(language_start) = self.s.strip_prefix("```") + && let Some(newline) = language_start.find('\n') + { + let language = &language_start[..newline]; + let code_start = &language_start[newline + 1..]; + if let Some(end) = code_start.find("\n```") { + let code = &code_start[..end].trim(); + self.s = &code_start[end + 4..]; + self.start_of_line = false; + return Some(Item::CodeBlock(language, code)); + } else { + self.s = ""; + return Some(Item::CodeBlock(language, code_start)); + } + } + None + } + + // `code` + fn inline_code(&mut self) -> Option> { + if let Some(rest) = self.s.strip_prefix('`') { + self.s = rest; + self.start_of_line = false; + self.style.code = true; + let rest_of_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; + if let Some(end) = rest_of_line.find('`') { + let item = Item::Text(self.style, &self.s[..end]); + self.s = &self.s[end + 1..]; + self.style.code = false; + return Some(item); + } else { + let end = rest_of_line.len(); + let item = Item::Text(self.style, rest_of_line); + self.s = &self.s[end..]; + self.style.code = false; + return Some(item); + } + } + None + } + + /// `` or `[link](url)` + fn url(&mut self) -> Option> { + if self.s.starts_with('<') { + let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; + if let Some(url_end) = this_line.find('>') { + let url = &self.s[1..url_end]; + self.s = &self.s[url_end + 1..]; + self.start_of_line = false; + return Some(Item::Hyperlink(self.style, url, url)); + } + } + + // [text](url) + if self.s.starts_with('[') { + let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; + if let Some(bracket_end) = this_line.find(']') { + let text = &this_line[1..bracket_end]; + if this_line[bracket_end + 1..].starts_with('(') + && let Some(parens_end) = this_line[bracket_end + 2..].find(')') + { + let parens_end = bracket_end + 2 + parens_end; + let url = &self.s[bracket_end + 2..parens_end]; + self.s = &self.s[parens_end + 1..]; + self.start_of_line = false; + return Some(Item::Hyperlink(self.style, text, url)); + } + } + } + None + } +} + +impl<'a> Iterator for Parser<'a> { + type Item = Item<'a>; + + fn next(&mut self) -> Option { + loop { + if self.s.is_empty() { + return None; + } + + // \n + if self.s.starts_with('\n') { + self.s = &self.s[1..]; + self.start_of_line = true; + self.style = Style::default(); + return Some(Item::Newline); + } + + // Ignore line break (continue on the same line) + if self.s.starts_with("\\\n") && self.s.len() >= 2 { + self.s = &self.s[2..]; + self.start_of_line = false; + continue; + } + + // \ escape (to show e.g. a backtick) + if self.s.starts_with('\\') && self.s.len() >= 2 { + let text = &self.s[1..2]; + self.s = &self.s[2..]; + self.start_of_line = false; + return Some(Item::Text(self.style, text)); + } + + if self.start_of_line { + // leading space (indentation) + if self.s.starts_with(' ') { + let length = self.s.find(|c| c != ' ').unwrap_or(self.s.len()); + self.s = &self.s[length..]; + self.start_of_line = true; // indentation doesn't count + return Some(Item::Indentation(length)); + } + + // # Heading + if let Some(after) = self.s.strip_prefix("# ") { + self.s = after; + self.start_of_line = false; + self.style.heading = true; + continue; + } + + // > quote + if let Some(after) = self.s.strip_prefix("> ") { + self.s = after; + self.start_of_line = true; // quote indentation doesn't count + self.style.quoted = true; + return Some(Item::QuoteIndent); + } + + // - bullet point + if self.s.starts_with("- ") { + self.s = &self.s[2..]; + self.start_of_line = false; + return Some(Item::BulletPoint); + } + + // `1. `, `42. ` etc. + if let Some(item) = self.numbered_list() { + return Some(item); + } + + // --- separator + if let Some(after) = self.s.strip_prefix("---") { + self.s = after.trim_start_matches('-'); // remove extra dashes + self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline + self.start_of_line = false; + return Some(Item::Separator); + } + + // ```{language}\n{code}``` + if let Some(item) = self.code_block() { + return Some(item); + } + } + + // `code` + if let Some(item) = self.inline_code() { + return Some(item); + } + + if let Some(rest) = self.s.strip_prefix('*') { + self.s = rest; + self.start_of_line = false; + self.style.strong = !self.style.strong; + continue; + } + if let Some(rest) = self.s.strip_prefix('_') { + self.s = rest; + self.start_of_line = false; + self.style.underline = !self.style.underline; + continue; + } + if let Some(rest) = self.s.strip_prefix('~') { + self.s = rest; + self.start_of_line = false; + self.style.strikethrough = !self.style.strikethrough; + continue; + } + if let Some(rest) = self.s.strip_prefix('/') { + self.s = rest; + self.start_of_line = false; + self.style.italics = !self.style.italics; + continue; + } + if let Some(rest) = self.s.strip_prefix('$') { + self.s = rest; + self.start_of_line = false; + self.style.small = !self.style.small; + continue; + } + if let Some(rest) = self.s.strip_prefix('^') { + self.s = rest; + self.start_of_line = false; + self.style.raised = !self.style.raised; + continue; + } + + // `` or `[link](url)` + if let Some(item) = self.url() { + return Some(item); + } + + // Swallow everything up to the next special character: + let end = self + .s + .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '[', '\n'][..]) + .map_or_else(|| self.s.len(), |special| special.max(1)); + + let item = Item::Text(self.style, &self.s[..end]); + self.s = &self.s[end..]; + self.start_of_line = false; + return Some(item); + } + } +} + +#[test] +fn test_easy_mark_parser() { + let items: Vec<_> = Parser::new("~strikethrough `code`~").collect(); + assert_eq!( + items, + vec![ + Item::Text( + Style { + strikethrough: true, + ..Default::default() + }, + "strikethrough " + ), + Item::Text( + Style { + code: true, + strikethrough: true, + ..Default::default() + }, + "code" + ), + ] + ); +} diff --git a/maxima-ui/src/util/easy_mark/easy_mark_viewer.rs b/maxima-ui/src/util/easy_mark/easy_mark_viewer.rs new file mode 100644 index 00000000..c28409b2 --- /dev/null +++ b/maxima-ui/src/util/easy_mark/easy_mark_viewer.rs @@ -0,0 +1,178 @@ +// Copied from https://github.com/emilk/egui/tree/main/crates/egui_demo_lib/src/easy_mark +// Prevent using a whole demo lib for easy_mark. + +use super::easy_mark_parser as easy_mark; +use egui::{ + Align, Align2, Hyperlink, Layout, Response, RichText, Sense, Separator, Shape, TextStyle, Ui, + vec2, +}; + +/// Parse and display a VERY simple and small subset of Markdown. +pub fn easy_mark(ui: &mut Ui, easy_mark: &str) { + easy_mark_it(ui, easy_mark::Parser::new(easy_mark)); +} + +pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator>) { + let initial_size = vec2( + ui.available_width(), + ui.spacing().interact_size.y, // Assume there will be + ); + + let layout = Layout::left_to_right(Align::BOTTOM).with_main_wrap(true); + + ui.allocate_ui_with_layout(initial_size, layout, |ui| { + ui.spacing_mut().item_spacing.x = 0.0; + let row_height = ui.text_style_height(&TextStyle::Body); + ui.set_row_height(row_height); + + for item in items { + item_ui(ui, item); + } + }); +} + +pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) { + let row_height = ui.text_style_height(&TextStyle::Body); + let one_indent = row_height / 2.0; + + match item { + easy_mark::Item::Newline => { + // ui.label("\n"); // too much spacing (paragraph spacing) + ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height + ui.end_row(); + ui.set_row_height(row_height); + } + + easy_mark::Item::Text(style, text) => { + let label = rich_text_from_style(text, &style); + if style.small && !style.raised { + ui.with_layout(Layout::left_to_right(Align::BOTTOM), |ui| { + ui.set_min_height(row_height); + ui.label(label); + }); + } else { + ui.label(label); + } + } + easy_mark::Item::Hyperlink(style, text, url) => { + let label = rich_text_from_style(text, &style); + if style.small && !style.raised { + ui.with_layout(Layout::left_to_right(Align::BOTTOM), |ui| { + ui.set_height(row_height); + ui.add(Hyperlink::from_label_and_url(label, url)); + }); + } else { + ui.add(Hyperlink::from_label_and_url(label, url)); + } + } + + easy_mark::Item::Separator => { + ui.add(Separator::default().horizontal()); + } + easy_mark::Item::Indentation(indent) => { + let indent = indent as f32 * one_indent; + ui.allocate_exact_size(vec2(indent, row_height), Sense::hover()); + } + easy_mark::Item::QuoteIndent => { + let rect = ui.allocate_exact_size(vec2(2.0 * one_indent, row_height), Sense::hover()).0; + let rect = rect.expand2(ui.style().spacing.item_spacing * 0.5); + ui.painter().line_segment( + [rect.center_top(), rect.center_bottom()], + (1.0, ui.visuals().weak_text_color()), + ); + } + easy_mark::Item::BulletPoint => { + ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover()); + bullet_point(ui, one_indent); + ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover()); + } + easy_mark::Item::NumberedPoint(number) => { + let width = 3.0 * one_indent; + numbered_point(ui, width, number); + ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover()); + } + easy_mark::Item::CodeBlock(_language, code) => { + let where_to_put_background = ui.painter().add(Shape::Noop); + let mut rect = ui.monospace(code).rect; + rect = rect.expand(1.0); // looks better + rect.max.x = ui.max_rect().max.x; + let code_bg_color = ui.visuals().code_bg_color; + ui.painter().set( + where_to_put_background, + Shape::rect_filled(rect, 1.0, code_bg_color), + ); + } + } +} + +fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText { + let easy_mark::Style { + heading, + quoted, + code, + strong, + underline, + strikethrough, + italics, + small, + raised, + } = *style; + + let small = small || raised; // Raised text is also smaller + + let mut rich_text = RichText::new(text); + if heading && !small { + rich_text = rich_text.heading().strong(); + } + if small && !heading { + rich_text = rich_text.small(); + } + if code { + rich_text = rich_text.code(); + } + if strong { + rich_text = rich_text.strong(); + } else if quoted { + rich_text = rich_text.weak(); + } + if underline { + rich_text = rich_text.underline(); + } + if strikethrough { + rich_text = rich_text.strikethrough(); + } + if italics { + rich_text = rich_text.italics(); + } + if raised { + rich_text = rich_text.raised(); + } + rich_text +} + +fn bullet_point(ui: &mut Ui, width: f32) -> Response { + let row_height = ui.text_style_height(&TextStyle::Body); + let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); + ui.painter().circle_filled( + rect.center(), + rect.height() / 8.0, + ui.visuals().strong_text_color(), + ); + response +} + +fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response { + let font_id = TextStyle::Body.resolve(ui.style()); + let row_height = ui.fonts_mut(|f| f.row_height(&font_id)); + let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); + let text = format!("{number}."); + let text_color = ui.visuals().strong_text_color(); + ui.painter().text( + rect.right_center(), + Align2::RIGHT_CENTER, + text, + font_id, + text_color, + ); + response +} diff --git a/maxima-ui/src/util/easy_mark/mod.rs b/maxima-ui/src/util/easy_mark/mod.rs new file mode 100644 index 00000000..265421cf --- /dev/null +++ b/maxima-ui/src/util/easy_mark/mod.rs @@ -0,0 +1,14 @@ +// Copied from https://github.com/emilk/egui/tree/main/crates/egui_demo_lib/src/easy_mark +// Prevent using a whole demo lib for easy_mark. + +//! Experimental markup language + +mod easy_mark_editor; +mod easy_mark_highlighter; +pub mod easy_mark_parser; +mod easy_mark_viewer; + +pub use easy_mark_editor::EasyMarkEditor; +pub use easy_mark_highlighter::MemoizedEasymarkHighlighter; +pub use easy_mark_parser as parser; +pub use easy_mark_viewer::easy_mark; diff --git a/maxima-ui/src/util/mod.rs b/maxima-ui/src/util/mod.rs index 163a4fba..23e313ed 100644 --- a/maxima-ui/src/util/mod.rs +++ b/maxima-ui/src/util/mod.rs @@ -1 +1,2 @@ +pub mod easy_mark; pub mod markdown; diff --git a/maxima-ui/src/views/downloads_view.rs b/maxima-ui/src/views/downloads_view.rs index f3d93ad5..979b8f37 100644 --- a/maxima-ui/src/views/downloads_view.rs +++ b/maxima-ui/src/views/downloads_view.rs @@ -1,11 +1,10 @@ +use crate::{APP_MARGIN, MaximaEguiApp, bridge_thread::MaximaLibRequest}; use egui::{ Align2, Color32, CornerRadius, FontId, Mesh, Rect, Shape, Stroke, StrokeKind, Ui, Widget, pos2, vec2, }; use humansize::DECIMAL; -use crate::{APP_MARGIN, MaximaEguiApp}; - #[derive(Clone)] pub struct QueuedDownload { pub slug: String, @@ -167,11 +166,17 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is Color32::WHITE, ); + let offer = game_dl.offer.clone(); if ui.put(left_button_rect, egui::Button::new("🗙")).clicked() { - //TODO: Remove + let _ = app + .backend + .backend_commander + .send(MaximaLibRequest::CancelInstallRequest(offer)); } - if ui.put(right_button_rect, egui::Button::new("⏸")).clicked() { - //TODO: Pause + if ui.put(right_button_rect, egui::Button::new("⮉")).clicked() { + let _ = app.backend.backend_commander.send( + MaximaLibRequest::MoveInstallToTopRequest(game_dl.offer.clone()), + ); } } else { ui.painter().text( @@ -182,11 +187,17 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is Color32::WHITE, ); + let offer = game_dl.offer.clone(); if ui.put(left_button_rect, egui::Button::new("🗙")).clicked() { - //TODO: Remove + let _ = app + .backend + .backend_commander + .send(MaximaLibRequest::CancelInstallRequest(offer)); } if ui.put(right_button_rect, egui::Button::new("⮉")).clicked() { - //TODO: Move to top + let _ = app.backend.backend_commander.send( + MaximaLibRequest::MoveInstallToTopRequest(game_dl.offer.clone()), + ); } } }); diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index 57185eab..cd15791e 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -1,3 +1,4 @@ +use crate::util::{easy_mark::easy_mark}; use crate::{ GameDetails, GameDetailsWrapper, GameInfo, InstallModalState, MaximaEguiApp, PageType, PopupModal, bridge_thread, set_app_modal, translation_manager::TranslationManager, @@ -8,7 +9,6 @@ use egui::{ UiBuilder, UiKind, Widget, pos2, vec2, }; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; - use strum_macros::EnumIter; #[derive(Debug, PartialEq, Default, EnumIter)] @@ -142,7 +142,7 @@ fn game_view_system_requirements( min.set_min_width(req_width); min.set_max_width(req_width); min.heading(&locale.localization.games_view.details.min_system_req); - egui_demo_lib::easy_mark::easy_mark(min, min_requirements); + easy_mark(min, min_requirements); }); } @@ -152,7 +152,7 @@ fn game_view_system_requirements( rec.set_min_width(req_width); rec.set_max_width(req_width); rec.heading(&locale.localization.games_view.details.rec_system_req); - egui_demo_lib::easy_mark::easy_mark(rec, rec_requirements); + easy_mark(rec, rec_requirements); }); } } else { From ce48ac2cc7af15c7da9179f7b5f4023c21a8b80d Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:18:46 +0200 Subject: [PATCH 17/57] WIP --- maxima-lib/src/content/downloader.rs | 335 +++++++++++++----------- maxima-lib/src/content/manager.rs | 92 +++---- maxima-lib/src/core/cloudsync.rs | 27 +- maxima-lib/src/core/launch.rs | 6 +- maxima-lib/src/core/library.rs | 13 +- maxima-lib/src/core/manifest/dip.rs | 19 +- maxima-lib/src/core/manifest/mod.rs | 53 +++- maxima-lib/src/core/manifest/pre_dip.rs | 19 +- maxima-lib/src/core/mod.rs | 10 +- maxima-lib/src/lsx/connection.rs | 55 ++-- maxima-lib/src/lsx/service.rs | 66 ++--- maxima-ui/src/bridge_processor.rs | 47 ++-- maxima-ui/src/bridge_thread.rs | 318 +++++++++++----------- maxima-ui/src/main.rs | 131 ++++----- maxima-ui/src/views/downloads_view.rs | 10 +- maxima-ui/src/views/game_view.rs | 138 ++++++---- 16 files changed, 699 insertions(+), 640 deletions(-) diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 7e8ddcb4..fb0fc291 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -2,8 +2,7 @@ use std::{ io::{self, Cursor, Read, SeekFrom}, path::{Path, PathBuf}, pin::Pin, - prelude, - sync::{Arc, Mutex}, + sync::Arc, task, }; @@ -26,7 +25,7 @@ use thiserror::Error; use tokio::{ fs::{File, OpenOptions, create_dir, create_dir_all}, io::{AsyncSeekExt, AsyncWrite, BufReader, BufWriter}, - runtime::Handle, + sync::Mutex as AsyncMutex, }; use tokio_util::compat::FuturesAsyncReadCompatExt; @@ -37,53 +36,50 @@ async fn zstate_path(id: &str, path: &str) -> Result { Ok(path) } -#[async_trait] trait DownloadDecoder: Send { fn save_state(&mut self, buf: &mut BytesMut); fn restore_state(&mut self, buf: &mut Bytes); - fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError>; - fn write_in_pos(&self) -> u64; fn write_out_pos(&self) -> u64; - - fn get_mut(&mut self) -> Arc>>; + fn get_mut(&mut self) -> Arc>>; + fn supports_resume(&self) -> bool { + true + } } struct ZLibDeflateDecoder { decompress: flate2::Decompress, - writer: Arc>>, + writer: Arc>>, } impl ZLibDeflateDecoder { fn new(writer: BufWriter) -> Self { Self { decompress: Decompress::new(true), - writer: Arc::new(Mutex::new(writer)), + writer: Arc::new(AsyncMutex::new(writer)), } } } -#[async_trait] impl DownloadDecoder for ZLibDeflateDecoder { fn save_state(&mut self, buf: &mut BytesMut) { - // State serialization is disabled - flate2 doesn't expose raw zlib state + // State serialization disabled — flate2 doesn't expose raw zlib state let _ = buf; } fn restore_state(&mut self, buf: &mut Bytes) { - // State deserialization is disabled - flate2 doesn't expose raw zlib state + // State deserialization disabled — flate2 doesn't expose raw zlib state self.decompress.reset(false); let _ = buf; } fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError> { - let mut file = self.writer.lock().unwrap(); - - let handle = Handle::current(); - let _ = handle.enter(); - futures::executor::block_on(file.seek(pos))?; - + let writer = self.writer.clone(); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async move { writer.lock().await.seek(pos).await }) + })?; Ok(()) } @@ -95,26 +91,29 @@ impl DownloadDecoder for ZLibDeflateDecoder { self.decompress.total_out() } - fn get_mut(&mut self) -> Arc>> { + fn get_mut(&mut self) -> Arc>> { self.writer.clone() } + + fn supports_resume(&self) -> bool { + false + } } struct NoopDecoder { - writer: Arc>>, + writer: Arc>>, pos: u64, } impl NoopDecoder { pub fn new(writer: BufWriter) -> Self { Self { - writer: Arc::new(Mutex::new(writer)), + writer: Arc::new(AsyncMutex::new(writer)), pos: 0, } } } -#[async_trait] impl DownloadDecoder for NoopDecoder { fn save_state(&mut self, buf: &mut BytesMut) { buf.put_u64(self.pos); @@ -125,12 +124,13 @@ impl DownloadDecoder for NoopDecoder { } fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError> { - let mut file = self.writer.lock().unwrap(); - - let handle = Handle::current(); - let _ = handle.enter(); - futures::executor::block_on(file.seek(pos))?; - + let writer = self.writer.clone(); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async move { + let mut file = writer.lock().await; + file.seek(pos).await + }) + })?; Ok(()) } @@ -142,38 +142,20 @@ impl DownloadDecoder for NoopDecoder { self.pos } - fn get_mut(&mut self) -> Arc>> { + fn get_mut(&mut self) -> Arc>> { self.writer.clone() } } struct AsyncWriterWrapper<'a> { - id: String, - path: String, - zlib_state_file: tokio::fs::File, decoder: &'a mut Box, - inner: Arc>>, + inner: Arc>>, } impl<'a> AsyncWriterWrapper<'a> { - async fn new( - id: String, - path: String, - decoder: &'a mut Box, - ) -> Result { + async fn new(decoder: &'a mut Box) -> Self { let inner = decoder.get_mut(); - Ok(AsyncWriterWrapper { - id: id.to_owned(), - path: path.to_owned(), - zlib_state_file: tokio::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(zstate_path(&id, &path).await?) - .await?, - decoder, - inner, - }) + AsyncWriterWrapper { decoder, inner } } } @@ -182,41 +164,46 @@ impl<'a> AsyncWrite for AsyncWriterWrapper<'a> { self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8], - ) -> task::Poll> { - let poll_result = { - let mut binding = self.inner.lock().unwrap(); - let inner = Pin::new(&mut *binding); - inner.poll_write(cx, buf) - }; - - // State serialization is disabled for now. - // let mut bytes = BytesMut::new(); - // self.decoder.save_state(&mut bytes); - - // self.zlib_state_file.seek(SeekFrom::Start(0))?; - // self.zlib_state_file.write(&bytes)?; - - poll_result + ) -> task::Poll> { + match self.inner.try_lock() { + Ok(mut guard) => Pin::new(&mut *guard).poll_write(cx, buf), + Err(_) => { + cx.waker().wake_by_ref(); + task::Poll::Pending + } + } } fn poll_flush( self: Pin<&mut Self>, cx: &mut task::Context<'_>, - ) -> task::Poll> { - Pin::new(&mut *self.inner.lock().unwrap()).poll_flush(cx) + ) -> task::Poll> { + match self.inner.try_lock() { + Ok(mut guard) => Pin::new(&mut *guard).poll_flush(cx), + Err(_) => { + cx.waker().wake_by_ref(); + task::Poll::Pending + } + } } fn poll_shutdown( self: Pin<&mut Self>, cx: &mut task::Context<'_>, - ) -> task::Poll> { - Pin::new(&mut *self.inner.lock().unwrap()).poll_shutdown(cx) + ) -> task::Poll> { + match self.inner.try_lock() { + Ok(mut guard) => Pin::new(&mut *guard).poll_shutdown(cx), + Err(_) => { + cx.waker().wake_by_ref(); + task::Poll::Pending + } + } } } #[derive(Error, Debug)] pub enum DownloadError { - #[error("download failed ({0} bytes")] + #[error("download failed after {0} bytes")] DownloadFailed(usize), #[error("failed to download chunk `{entry}`: {error}")] ChunkDownload { @@ -279,8 +266,15 @@ impl<'a> EntryDownloadRequest<'a> { ) -> Result { let path = context.path.join(entry.name()); - let file_size = File::open(&path).await?.metadata().await?.len() as i64; + let metadata = match tokio::fs::metadata(&path).await { + Ok(m) => m, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(EntryDownloadState::Fresh); + } + Err(e) => return Err(e.into()), + }; + let file_size = metadata.len() as i64; if file_size == 0 { return Ok(EntryDownloadState::Fresh); } @@ -289,54 +283,65 @@ impl<'a> EntryDownloadRequest<'a> { let size_match = entry_size == file_size; if !size_match { - warn!("Size mismatch: {}/{}", entry_size, file_size); + warn!( + "Size mismatch for {}: expected={} actual={}", + entry.name(), + entry_size, + file_size + ); if file_size > entry_size { return Ok(EntryDownloadState::Borked); } return Ok(EntryDownloadState::Resumable); } - // We must be calculating the hash incorrectly or something - // let hash = match hash_file_crc32(&path) { - // Ok(hash) => hash, - // Err(_) => { - // warn!("Failed to retrieve hash for file {}", entry.name()); - // 0 - // } - // }; - - // let hash_match = *entry.crc32() != hash; - // if !hash_match { - // warn!("Hash mismatch"); - // return EntryDownloadState::Borked; - // } - Ok(EntryDownloadState::Complete) } async fn download(&mut self) -> Result<(), DownloadError> { - let mut tries = 0; - while tries < 5 { + let mut last_err = None; + + for attempt in 1..=5 { let start = self.decoder.write_in_pos() as i64; + let end = *self.entry.compressed_size(); debug!( - "Downloading {} from {} to {} ({})", + "Downloading {} bytes={}-{} (uncompressed: {}), attempt {}/5", self.entry.name(), start, - self.entry.compressed_size(), - self.entry.uncompressed_size() + end, + self.entry.uncompressed_size(), + attempt, ); - let end = *self.entry.compressed_size(); - let result = self.download_range(start, end).await; - if result.is_ok() { - break; + match self.download_range(start, end).await { + Ok(()) => return Ok(()), + Err(DownloaderError::Download(err)) => { + warn!( + "Download attempt {}/5 failed for {}: {}", + attempt, + self.entry.name(), + err + ); + last_err = Some(err); + } + Err(other) => { + warn!( + "Download attempt {}/5 failed for {}: {}", + attempt, + self.entry.name(), + other + ); + last_err = Some(DownloadError::DownloadFailed( + self.decoder.write_in_pos() as usize + )); + } } - - tries += 1; } - Ok(()) + Err(last_err.unwrap_or(DownloadError::DownloadFailed( + self.decoder.write_in_pos() as usize + ))) } /// End is not inclusive @@ -344,7 +349,7 @@ impl<'a> EntryDownloadRequest<'a> { let offset = self.entry.data_offset(); let range = format!("bytes={}-{}", offset + start, offset + end - 1); - let data = match self + let response = match self .client .get(self.url) .header("range", range) @@ -353,7 +358,6 @@ impl<'a> EntryDownloadRequest<'a> { { Ok(res) => res, Err(err) => { - error!("Failed to download ({}): {}", self.entry.name(), err); return Err(DownloaderError::Download(DownloadError::ChunkDownload { entry: self.entry.name().clone(), error: err, @@ -361,28 +365,52 @@ impl<'a> EntryDownloadRequest<'a> { } }; - let stream = data.bytes_stream(); + if response.status() != reqwest::StatusCode::PARTIAL_CONTENT + && response.status() != reqwest::StatusCode::OK + { + return Err(DownloaderError::Http(response.status())); + } + + let stream = response.bytes_stream(); let counting_stream = ByteCountingStream::new(stream, self.callback.as_ref()); - let stream = counting_stream.into_async_read(); - let mut stream_reader = BufReader::new(stream.compat()); - - // State deserialization is disabled for now. - // let out_pos = self.decoder.write_out_pos(); - // self.decoder.seek(SeekFrom::Start(out_pos)); - - let mut wrapper = AsyncWriterWrapper::new( - self.context.id.to_owned(), - self.entry.name().to_owned(), - &mut self.decoder, - ) - .await?; - - let result = tokio::io::copy(&mut stream_reader, &mut wrapper).await; - if let Err(err) = result { - return Err(DownloaderError::Download(DownloadError::ChunkCopy { - entry: self.entry.name().clone(), - error: err, - })); + + let writer_arc = self.decoder.get_mut(); + let mut writer = writer_arc.lock().await; + + match self.entry.compression_type() { + CompressionType::None => { + // No decompression — stream straight to file + let async_read = counting_stream.into_async_read(); + let mut reader = BufReader::new(async_read.compat()); + if let Err(err) = tokio::io::copy(&mut reader, &mut *writer).await { + return Err(DownloaderError::Download(DownloadError::ChunkCopy { + entry: self.entry.name().clone(), + error: err, + })); + } + } + CompressionType::Deflate => { + // Collect compressed bytes then decompress — avoids async/sync boundary issues with flate2 + let compressed: Vec = counting_stream + .try_fold(Vec::new(), |mut acc, chunk| async move { + acc.extend_from_slice(&chunk); + Ok(acc) + }) + .await + .map_err(|e| DownloaderError::Io(e))?; + + let mut decoder = BufreadDeflateDecoder::new(Cursor::new(&compressed)); + let mut decompressed = Vec::with_capacity(*self.entry.uncompressed_size() as usize); + decoder.read_to_end(&mut decompressed)?; + + let mut cursor = Cursor::new(decompressed); + if let Err(err) = tokio::io::copy(&mut cursor, &mut *writer).await { + return Err(DownloaderError::Download(DownloadError::ChunkCopy { + entry: self.entry.name().clone(), + error: err, + })); + } + } } Ok(()) @@ -440,8 +468,8 @@ impl ZipDownloader { .send() .await?; - if !response.status().is_success() - && response.status() != reqwest::StatusCode::PARTIAL_CONTENT + if response.status() != reqwest::StatusCode::PARTIAL_CONTENT + && response.status() != reqwest::StatusCode::OK { return Err(DownloaderError::Http(response.status())); } @@ -466,7 +494,6 @@ impl ZipDownloader { let mut limited_reader = decoder.take(length); let mut decompressed_data = Vec::with_capacity(length as usize); limited_reader.read_to_end(&mut decompressed_data)?; - Bytes::from(decompressed_data) } }; @@ -481,17 +508,16 @@ impl ZipDownloader { ) -> Result { let file_path = self.path.join(entry.name()); - if !file_path.exists() { - if !file_path.safe_parent()?.exists() { - create_dir_all(&file_path.safe_parent()?).await?; - } + create_dir_all(file_path.safe_parent()?).await?; - if entry.name().ends_with("/") && !file_path.exists() { - // This is a folder, create the dir - debug!("{} is a directory", entry.name()); - create_dir(file_path).await?; - return Ok(0); + if entry.name().ends_with('/') { + debug!("{} is a directory", entry.name()); + match create_dir(&file_path).await { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} + Err(e) => return Err(e.into()), } + return Ok(0); } if *entry.uncompressed_size() == 0 { @@ -504,13 +530,6 @@ impl ZipDownloader { debug!("Compressed Size: {}", entry.compressed_size()); debug!("Offset: {}", offset); - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(false) - .open(&file_path) - .await?; - let context = DownloadContext { id: self.id.to_owned(), path: self.path.clone(), @@ -524,6 +543,13 @@ impl ZipDownloader { return Ok(0); } + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .open(&file_path) + .await?; + if state == EntryDownloadState::Borked { warn!("Found borked file {}", entry.name()); file.set_len(*entry.uncompressed_size() as u64).await?; @@ -537,12 +563,21 @@ impl ZipDownloader { }; if state == EntryDownloadState::Resumable { - let state_file = zstate_path(&self.id, entry.name()).await?; - if state_file.exists() { - let mut buf = Bytes::from(tokio::fs::read(state_file).await?); - decoder.restore_state(&mut buf); + if decoder.supports_resume() { + let state_file = zstate_path(&self.id, entry.name()).await?; + if state_file.exists() { + let mut buf = Bytes::from(tokio::fs::read(state_file).await?); + decoder.restore_state(&mut buf); + } else { + tokio::fs::create_dir_all(state_file.safe_parent()?).await?; + } } else { - tokio::fs::create_dir_all(state_file.safe_parent()?).await?; + warn!( + "Decoder for {} does not support resume, restarting from scratch", + entry.name() + ); + decoder.get_mut().lock().await.get_mut().set_len(0).await?; + decoder.seek(SeekFrom::Start(0))?; } } @@ -577,10 +612,6 @@ where callback, } } - - fn byte_count(&self) -> usize { - self.byte_count - } } impl<'a, S> Stream for ByteCountingStream<'a, S> @@ -596,11 +627,9 @@ where match self.inner.poll_next_unpin(cx) { std::task::Poll::Ready(Some(Ok(chunk))) => { self.byte_count += chunk.len(); - if let Some(callback) = &self.callback { callback(chunk.len()); } - std::task::Poll::Ready(Some(Ok(chunk))) } std::task::Poll::Ready(Some(Err(err))) => { diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index be8170b9..859a5bb0 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -161,8 +161,7 @@ impl GameDownloader { let total_bytes = entries .iter() .map(|x| *x.compressed_size() as usize) - .sum::() - + 1; // Add 1 to account for running touchup at the end. Bad solution, but we're a bit rushed + .sum::(); // mmmph a good amount of bytes, eat that Beton Brutal Ok(GameDownloader { offer_id: game.offer_id.to_owned(), @@ -232,45 +231,36 @@ impl GameDownloader { for ele in entries.iter().take(total_count) { let downloader = downloader_arc.clone(); let ele = ele.clone(); - let cancel_token = cancel_token.clone(); let completed_bytes = completed_bytes.clone(); - handles.push(async move { - if ele.name().contains("Cleanup") { - info!("Ele: {:?}", ele); - } - tokio::select! { result = downloader.download_single_file(&ele, Some(Box::new(move |bytes| { completed_bytes.fetch_add(bytes, Ordering::SeqCst); - }))) => { - if let Err(err) = result { - error!("File download failed: {}", err); - } - }, + }))) => result.map(|_| ()).map_err(DownloaderError::from), _ = cancel_token.cancelled() => { info!("Download of {} cancelled", ele.name()); + Ok(()) }, } }); } - let _results = futures::stream::iter(handles) + let results: Vec> = futures::stream::iter(handles) .buffer_unordered(16) - .collect::>() + .collect() .await; - let path = downloader_arc.path(); + for result in results { + result?; + } + let path = downloader_arc.path(); info!("Files downloaded, running touchup..."); let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(path).await?; info!("Installation finished!"); - completed_bytes.fetch_add(1, Ordering::SeqCst); - notify.notify_one(); Ok(()) } @@ -361,6 +351,8 @@ impl ContentManager { return Err(ContentManagerError::DownloadInProgress); } + self.queue.queued.retain(|g| g.offer_id != game.offer_id); + self.queue.current = Some(game.clone()); self.queue.save().await?; @@ -376,22 +368,12 @@ impl ContentManager { current.cancel(); self.current = None; self.queue.current = None; - - if let Some(game) = self.queue.queued.pop() { - self.install_now(game).await?; - } - self.queue.save().await?; return Ok(()); } } - let pos = self - .queue - .queued - .iter() - .position(|g| g.offer_id == offer_id); - if let Some(pos) = pos { + if let Some(pos) = self.queue.queued.iter().position(|g| g.offer_id == offer_id) { self.queue.queued.remove(pos); self.queue.save().await?; return Ok(()); @@ -413,8 +395,9 @@ impl ContentManager { self.queue.queued.insert(0, game); - if let Some(next) = self.queue.queued.pop() { - self.install_now(next).await?; + if let Some(game) = self.queue.queued.first().cloned() { + self.queue.queued.remove(0); + self.install_now(game).await?; } self.queue.save().await?; @@ -442,30 +425,35 @@ impl ContentManager { } pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { + if self.current.is_none() && self.queue.current.is_none() && !self.queue.queued.is_empty() { + if let Some(game) = self.queue.queued.pop() { + self.install_direct(game).await?; + } + } let mut event = None; if let Some(current) = &self.current { - if current.is_done() { - event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - self.current = None; - self.queue.current = None; - - if let Some(game) = self.queue.queued.pop() { - self.install_now(game).await?; + if let Some(current) = &self.current { + if let Some(err) = current.take_error() { + let offer_id = current.offer_id().to_owned(); + self.current = None; + self.queue.current = None; + if let Some(game) = self.queue.queued.first().cloned() { + self.queue.queued.remove(0); + self.install_now(game).await?; + } + self.queue.save().await?; + event = Some(MaximaEvent::InstallFailed(offer_id, err.to_string())); + } else if current.is_done() { + event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); + self.current = None; + self.queue.current = None; + if let Some(game) = self.queue.queued.first().cloned() { + self.queue.queued.remove(0); + self.install_now(game).await?; + } + self.queue.save().await?; } - - self.queue.save().await?; - } else if let Some(err) = current.take_error() { - let offer_id = current.offer_id().to_owned(); - self.current = None; - self.queue.current = None; - - if let Some(game) = self.queue.queued.pop() { - self.install_now(game).await?; - } - - self.queue.save().await?; - event = Some(MaximaEvent::InstallFailed(offer_id, err.to_string())); } } diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 49ae1fa8..fc05430d 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -273,17 +273,18 @@ impl<'a> CloudSyncLock<'a> { let file = OpenOptions::new().read(true).open(path.clone()).await; - let _md5 = if let Ok(file) = file { + let should_download = if let Ok(file) = file { let md5 = calc_file_md5(file, HashMode::Hex).await?; - if self.manifest.file_by_md5(&md5).is_some() { - debug!("Skipping CloudSync read {}", &path.display()); - continue; - } - md5 + self.manifest.file_by_md5(&md5).is_none() } else { - continue; + true // File isn't there, download it. }; + if !should_download { + debug!("Skipping CloudSync read {}", &path.display()); + continue; + } + value.request.push(CloudSyncRequest { attr_id: i.to_string(), verb: "GET".to_owned(), @@ -545,7 +546,10 @@ impl CloudSyncClient { pub fn new(auth: LockedAuthStorage) -> Self { Self { auth, - client: ClientBuilder::default().gzip(true).build().unwrap(), + client: ClientBuilder::default() + .gzip(true) + .build() + .unwrap_or_else(|_| Client::new()), } } @@ -554,10 +558,15 @@ impl CloudSyncClient { offer: &OwnedOffer, mode: CloudSyncLockMode, ) -> Result, CloudSyncError> { + let multiplayer_id = offer + .offer() + .multiplayer_id() + .as_deref() + .unwrap_or_else(|| offer.offer().offer_id()); let id = format!( "{}_{}", offer.offer().primary_master_title_id(), - offer.offer().multiplayer_id().as_ref().unwrap() + multiplayer_id ); let mut allowed_files = Vec::new(); diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index b7cf5ff0..85b0ec3c 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -311,7 +311,7 @@ pub async fn start_game( let launch_id = Uuid::new_v4().to_string(); child - .current_dir(PathBuf::from(path).safe_parent()?) + .current_dir(PathBuf::from(dir)) .env("MXLaunchId", &launch_id) .env("EAAuthCode", "unavailable") .env("EAEgsProxyIpcPort", "0") @@ -343,7 +343,7 @@ pub async fn start_game( .env("EAOnErrorExitRetCode", "1"); match mode { - LaunchMode::Offline(_) => todo!(), + LaunchMode::Offline(_) => return Err(LaunchError::Offline), LaunchMode::Online(ref offer_id) => { let short_token = request_opaque_ooa_token(&access_token).await?; @@ -362,7 +362,7 @@ pub async fn start_game( } }; - let child = child.spawn().expect("Failed to start child"); + let child = child.spawn().map_err(|e| LaunchError::Native(e.into()))?; maxima.playing = Some(ActiveGameContext::new( &launch_id, diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index e31054ed..c7986b5e 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -19,6 +19,8 @@ use derive_getters::Getters; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; +const LIBRARY_CACHE_TTL_SECS: u64 = 1200; + #[derive(Error, Debug)] pub enum LibraryError { #[error(transparent)] @@ -223,6 +225,15 @@ fn group_offers(products: Vec) -> Vec { } } + // Promote any product_map entry that has no corresponding base_products entry + for (slug, products) in &product_map { + if !base_products.contains_key(slug) { + if let Some(first) = products.first() { + base_products.insert(slug.clone(), first.clone()); + } + } + } + let mut titles = Vec::new(); for (base_slug, base_offer) in base_products { @@ -348,7 +359,7 @@ impl GameLibrary { .duration_since(std::time::UNIX_EPOCH)? .as_secs(); - if now - self.last_request > 1200 { + if now - self.last_request > LIBRARY_CACHE_TTL_SECS { self.request_owned_games().await?; } diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 90c99043..21b00933 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -2,7 +2,10 @@ use std::path::{Path, PathBuf}; -use crate::{core::manifest::ManifestError, util::native::platform_path}; +use crate::{ + core::manifest::{ManifestError, bytes_to_string}, + util::native::platform_path, +}; use derive_getters::Getters; use serde::Deserialize; @@ -240,20 +243,6 @@ fn split_args(s: &str) -> Result, ManifestError> { Ok(args) } -/// https://www.reddit.com/r/rust/comments/11co87m/comment/ja4sy88 -fn bytes_to_string(bytes: Vec) -> Option { - if let Ok(v) = String::from_utf8(bytes.clone()) { - return Some(v); - } - - let u16_bytes: Vec = bytes - .chunks_exact(2) - .map(|a| u16::from_ne_bytes([a[0], a[1]])) - .collect(); - - String::from_utf16(&u16_bytes).ok() -} - impl DiPTouchup { pub fn path(&self) -> &str { remove_leading_slash(&self.filePath) diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index bef5084a..c1788019 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -38,6 +38,7 @@ pub trait GameManifest: Send + std::fmt::Debug { async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError>; fn execute_path(&self, trial: bool) -> Option; fn version(&self) -> Option; + fn needs_touchup_on_locate(&self) -> bool; } #[async_trait::async_trait] impl GameManifest for DiPManifest { @@ -54,6 +55,13 @@ impl GameManifest for DiPManifest { fn version(&self) -> Option { Some(self.version().to_string()) } + + fn needs_touchup_on_locate(&self) -> bool { + self.buildMetaData() + .featureFlags() + .attr_forceTouchupInstallerAfterUpdate + .eq_ignore_ascii_case("true") + } } #[async_trait::async_trait] @@ -70,20 +78,43 @@ impl GameManifest for PreDiPManifest { fn version(&self) -> std::option::Option { Some(self.version().to_string()) } + + fn needs_touchup_on_locate(&self) -> bool { + false // pre-DiP games never had this concept + } } -pub async fn read(path: PathBuf) -> Result, ManifestError> { - let dip_attempt = DiPManifest::read(&path).await; - if let Ok(manifest) = dip_attempt { - return Ok(Box::new(manifest)); +/// https://www.reddit.com/r/rust/comments/11co87m/comment/ja4sy88 +fn bytes_to_string(bytes: Vec) -> Option { + if let Ok(v) = String::from_utf8(bytes.clone()) { + return Some(v); } - let pre_dip_attempt = PreDiPManifest::read(&path).await; - if let Ok(manifest) = pre_dip_attempt { - return Ok(Box::new(manifest)); + + let u16_bytes: Vec = bytes + .chunks_exact(2) + .map(|a| u16::from_ne_bytes([a[0], a[1]])) + .collect(); + + String::from_utf16(&u16_bytes).ok() +} + +pub fn from_bytes(bytes: &[u8]) -> Result { + let string = bytes_to_string(bytes.to_vec()).ok_or(ManifestError::Decode)?; + Ok(quick_xml::de::from_str(&string)?) +} + +pub async fn read(path: PathBuf) -> Result, ManifestError> { + let bytes = tokio::fs::read(&path).await?; + + if let Ok(m) = from_bytes::(&bytes) { + return Ok(Box::new(m)); } - Err(ManifestError::Unsupported { - dip_attempt: dip_attempt.unwrap_err().into(), - pre_dip_attempt: pre_dip_attempt.unwrap_err().into(), - }) + match from_bytes::(&bytes) { + Ok(m) => Ok(Box::new(m)), + Err(pre_dip_err) => Err(ManifestError::Unsupported { + dip_attempt: Box::new(ManifestError::Decode), + pre_dip_attempt: Box::new(pre_dip_err), + }), + } } diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 077ca833..19f70a80 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -1,6 +1,9 @@ #![allow(non_snake_case)] -use crate::{core::manifest::ManifestError, util::native::platform_path}; +use crate::{ + core::manifest::{ManifestError, bytes_to_string}, + util::native::platform_path, +}; use derive_getters::Getters; use serde::Deserialize; use std::path::{Path, PathBuf}; @@ -142,20 +145,6 @@ fn remove_trailing_backslash(path: &str) -> &str { path.strip_suffix('\\').unwrap_or(path) } -/// https://www.reddit.com/r/rust/comments/11co87m/comment/ja4sy88 -fn bytes_to_string(bytes: Vec) -> Option { - if let Ok(v) = String::from_utf8(bytes.clone()) { - return Some(v); - } - - let u16_bytes: Vec = bytes - .chunks_exact(2) - .map(|a| u16::from_ne_bytes([a[0], a[1]])) - .collect(); - - String::from_utf16(&u16_bytes).ok() -} - impl PreDiPManifest { pub async fn read(path: &Path) -> Result { let bytes = tokio::fs::read(path).await?; diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 2920f195..1a1209a3 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -274,8 +274,8 @@ impl Maxima { Ok(user) } - pub async fn friends(&self, page: u32) -> Result, ServiceLayerError> { - let cache_key = format!("friends_{}", page); + pub async fn friends(&self, offset: u32) -> Result, ServiceLayerError> { + let cache_key = format!("friends_{}", offset); if let Some(cached) = self.request_cache.get(&cache_key) { return Ok(cached); } @@ -286,7 +286,7 @@ impl Maxima { SERVICE_REQUEST_GETMYFRIENDS, ServiceGetMyFriendsRequestBuilder::default() .limit(100) - .offset(page) + .offset(offset) .is_mutual_friends_enabled(false) .build() .unwrap(), @@ -309,9 +309,7 @@ impl Maxima { } pub fn consume_pending_events(&mut self) -> Vec { - let events = self.pending_events.clone(); - self.pending_events.clear(); - events + std::mem::take(&mut self.pending_events) } pub async fn player_by_id(&self, id: &str) -> Result { diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 522a9404..d7a55dc9 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -355,42 +355,31 @@ impl Connection { let lsx_message: LSX = quick_xml::de::from_str(message.as_str())?; let state = self.state.clone(); - tokio::spawn(async move { - let reply: Result, LSXConnectionError> = match lsx_message.value - { - LSXMessageType::Event(msg) => Connection::process_event_message(&state, msg).await, - LSXMessageType::Request(msg) => { - Connection::process_request_message(&state, msg).await - } - LSXMessageType::Response(_) => { - warn!("Unexpected LSX Response message received, ignoring"); - Ok(None) - } - }; - - let reply: Option = match reply { - Ok(reply) => reply, - Err(err) => { - error!("Failed to process LSX message: {}", err); - return; - } - }; - - if let Some(reply) = reply { - let mut state = state.write().await; - let result = state.queue_message(LSX { value: reply }); - if let Err(err) = result { - error!("Failed to queue LSX message: {}", err); - return; - } + let reply = match lsx_message.value { + LSXMessageType::Request(msg) => { + Connection::process_request_message(&self.state, msg).await? } - - let mut state = state.write().await; - if let EncryptionState::Ready(key) = state.encryption { - state.encryption = EncryptionState::Enabled(key); + LSXMessageType::Event(msg) => { + Connection::process_event_message(&self.state, msg).await? + } + LSXMessageType::Response(_) => { + warn!("Unexpected LSX Response message received, ignoring"); + None } - }); + }; + + if let Some(reply) = reply { + self.state + .write() + .await + .queue_message(LSX { value: reply })?; + } + + let mut state = self.state.write().await; + if let EncryptionState::Ready(key) = state.encryption { + state.encryption = EncryptionState::Enabled(key); + } Ok(()) } diff --git a/maxima-lib/src/lsx/service.rs b/maxima-lib/src/lsx/service.rs index afcd0813..dda1ecfb 100644 --- a/maxima-lib/src/lsx/service.rs +++ b/maxima-lib/src/lsx/service.rs @@ -24,56 +24,46 @@ pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServ let mut connections: Vec = Vec::new(); loop { - let mut idx = 0_usize; + let mut idx = 0; while idx < connections.len() { - let connection = &mut connections[idx]; - - if connection.process_queue().await.is_err() { - warn!("Failed to process LSX message queue"); - } - - if connection.listen().await.is_err() { + if connections[idx].process_queue().await.is_err() + || connections[idx].listen().await.is_err() + { warn!("LSX connection closed"); connections.remove(idx); maxima .lock() .await .set_lsx_connections(connections.len() as u16); - continue; + } else { + idx += 1; } - - idx += 1; } - let (socket, addr) = match listener.accept().await { - Ok(s) => s, - Err(err) => { - let kind = err.kind(); - if kind == ErrorKind::WouldBlock { - sleep(Duration::from_millis(20)).await; - continue; + tokio::select! { + accept_result = listener.accept() => { + match accept_result { + Ok((socket, addr)) => { + info!("New LSX connection: {:?}", addr); + match Connection::new(maxima.clone(), socket).await { + Ok(mut conn) => { + conn.send_challenge().await?; + connections.push(conn); + maxima.lock().await.set_lsx_connections(connections.len() as u16); + maxima.lock().await.set_player_started(); + } + Err(e) => warn!("Failed to establish LSX connection: {}", e), + } + } + Err(e) if e.kind() == ErrorKind::WouldBlock => { + sleep(Duration::from_millis(5)).await; + } + Err(e) => return Err(LSXServerError::Io(e)), } - return Err(LSXServerError::Io(err)); } - }; - - info!("New LSX connection: {:?}", addr); - - let conn = Connection::new(maxima.clone(), socket).await; - if conn.is_err() { - warn!( - "Failed to establish LSX connection: {}", - conn.err().unwrap() - ); - continue; + _ = sleep(Duration::from_millis(5)) => { + // YOUUUUUUUU SHALL NOT BLOCK ! + } } - - let mut conn = conn.unwrap(); - conn.send_challenge().await?; - connections.push(conn); - - let mut maxima = maxima.lock().await; - maxima.set_lsx_connections(connections.len() as u16); - maxima.set_player_started(); } } diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 4e9cb3dc..4264e362 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -88,28 +88,35 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { DownloadQueueUpdate(current, queue) => { if let Some(current) = current { if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { - app.installing_now = Some(QueuedDownload { - slug: find_slug_for_offer(&app.games, ¤t), - offer: current, - downloaded_bytes: 0, - total_bytes: 0, - }); + let slug = find_slug_for_offer(&app.games, ¤t); + if !slug.is_empty() { + app.installing_now = Some(QueuedDownload { + slug, + offer: current, + downloaded_bytes: 0, + total_bytes: 0, + }); + } + } else { + app.installing_now = None; } - } else { - app.installing_now = None; - } - app.install_queue.clear(); - for offer in queue { - app.install_queue.insert( - offer.clone(), - QueuedDownload { - slug: find_slug_for_offer(&app.games, &offer), - offer, - downloaded_bytes: 0, - total_bytes: 0, - }, - ); + app.install_queue.clear(); + for offer in queue { + let slug = find_slug_for_offer(&app.games, &offer); + if slug.is_empty() { + continue; + } + app.install_queue.insert( + offer.clone(), + QueuedDownload { + slug, + offer, + downloaded_bytes: 0, + total_bytes: 0, + }, + ); + } } } DownloadFailed(offer, reason) => { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index c9e2ec11..3dcf2ad2 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -34,15 +34,8 @@ use maxima::{ registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; -use std::{ - path::PathBuf, - time::{Duration, SystemTime}, -}; -use tokio::sync::mpsc::{ - Sender, UnboundedReceiver, UnboundedSender, - error::{SendError, TryRecvError}, - unbounded_channel, -}; +use std::{path::PathBuf, time::Duration}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; // TODO(headassbtw): integrate these all into the enums pub struct InteractThreadLoginResponse { @@ -182,9 +175,7 @@ impl BridgeThread { queue.push(game.offer_id().to_owned()); } - backend_responder - .send(MaximaLibResponse::DownloadQueueUpdate(current, queue)) - .unwrap(); + backend_responder.send(MaximaLibResponse::DownloadQueueUpdate(current, queue)).ok(); } pub fn new( @@ -316,8 +307,8 @@ impl BridgeThread { backend_responder.send(MaximaLibResponse::LoginCacheEmpty).ok(); 'login: loop { - let Ok(request) = backend_cmd_listener.try_recv() else { - continue; + let Some(request) = backend_cmd_listener.recv().await else { + return Ok(()); }; match request { MaximaLibRequest::LoginRequestOauth => { @@ -326,9 +317,7 @@ impl BridgeThread { .expect("// TODO(headassbtw): panic message"); break 'login; } - MaximaLibRequest::ShutdownRequest => { - return Ok(()); - } + MaximaLibRequest::ShutdownRequest => return Ok(()), _ => {} } } @@ -385,17 +374,17 @@ impl BridgeThread { rtm_responder, ); - let mut next_tick = SystemTime::now() + Duration::from_millis(50); + let mut next_tick = tokio::time::Instant::now() + Duration::from_millis(50); let mut playing_cache: Option = None; 'main: loop { - let now = SystemTime::now(); - - if now >= next_tick { - next_tick = now + Duration::from_millis(50); + tokio::select! { + _ = tokio::time::sleep_until(next_tick) => { + next_tick = tokio::time::Instant::now() + Duration::from_millis(50); let mut maxima = maxima_arc.lock().await; maxima.update().await; + // progress reporting match maxima.playing() { Some(ctx) if playing_cache.is_none() => { if let Some(offer) = ctx.offer() { @@ -414,167 +403,170 @@ impl BridgeThread { _ => {} } - if let Some(dl) = maxima.content_manager().current() { - backend_responder - .send(MaximaLibResponse::DownloadProgressChanged( - dl.offer_id().to_string(), - InteractThreadDownloadProgressResponse { - bytes: dl.bytes_downloaded(), - bytes_total: dl.bytes_total(), - }, - )) - .ok(); - } - for ev in maxima.consume_pending_events() { match ev { - MaximaEvent::ReceivedLSXRequest(_, _) => {} MaximaEvent::InstallFinished(offer_id) => { - backend_responder - .send(MaximaLibResponse::DownloadFinished(offer_id)) - .ok(); + backend_responder.send(MaximaLibResponse::DownloadFinished(offer_id)).ok(); Self::update_queue(maxima.content_manager(), backend_responder.clone()); } MaximaEvent::InstallFailed(offer_id, reason) => { - backend_responder - .send(MaximaLibResponse::DownloadFailed(offer_id, reason)) - .ok(); + backend_responder.send(MaximaLibResponse::DownloadFailed(offer_id, reason)).ok(); Self::update_queue(maxima.content_manager(), backend_responder.clone()); } + _ => {} } } } - let Some(request) = backend_cmd_listener.recv().await else { - continue 'main; - }; - - let result: Result<(), BackendError> = match request { - MaximaLibRequest::LoginRequestOauth | MaximaLibRequest::StartService => { - error!("bro tried to log in twice"); - Ok(()) - } - - MaximaLibRequest::GetGamesRequest => { - let maxima = maxima_arc.clone(); - let responder = backend_responder.clone(); - let remote = remote_provider_channel.clone(); - let context = ctx.clone(); - tokio::task::spawn(async move { - if let Err(e) = - get_games_request(maxima, responder.clone(), remote, &context).await - { - let _ = - responder.send(MaximaLibResponse::NonFatalError(Box::from(e))).ok(); - } - }); - Ok(()) - } - MaximaLibRequest::GetFriendsRequest => { - let f = async || { - get_friends_request( - maxima_arc.clone(), - backend_responder.clone(), - remote_provider_channel.clone(), - ctx, - ) - .await - }; - f().await - } - MaximaLibRequest::GetGameDetailsRequest(slug) => { - let f = async || { - game_details_request( - maxima_arc.clone(), - slug, - backend_responder.clone(), - ctx, - ) - .await - }; - f().await - } - MaximaLibRequest::LocateGameRequest(mut path) => { - #[cfg(unix)] - maxima::core::launch::mx_linux_setup().await?; + request = backend_cmd_listener.recv() => { + let Some(request) = request else { + continue 'main; + }; + let result = match request { + MaximaLibRequest::LoginRequestOauth | MaximaLibRequest::StartService => { + error!("bro tried to log in twice"); + Ok(()) + } - // strip trailing slash - if path.ends_with('/') || path.ends_with('\\') { - path.pop(); + MaximaLibRequest::GetGamesRequest => { + let maxima = maxima_arc.clone(); + let responder = backend_responder.clone(); + let remote = remote_provider_channel.clone(); + let context = ctx.clone(); + tokio::task::spawn(async move { + if let Err(e) = + get_games_request(maxima, responder.clone(), remote, &context).await + { + let _ = + responder.send(MaximaLibResponse::NonFatalError(Box::from(e))).ok(); + } + }); + Ok(()) + } + MaximaLibRequest::GetFriendsRequest => { + let f = async || { + get_friends_request( + maxima_arc.clone(), + backend_responder.clone(), + remote_provider_channel.clone(), + ctx, + ) + .await + }; + f().await + } + MaximaLibRequest::GetGameDetailsRequest(slug) => { + let f = async || { + game_details_request( + maxima_arc.clone(), + slug, + backend_responder.clone(), + ctx, + ) + .await + }; + f().await } + MaximaLibRequest::LocateGameRequest(mut path) => { + #[cfg(unix)] + maxima::core::launch::mx_linux_setup().await?; - let path = PathBuf::from(path); - let manifest_path = path.join(MANIFEST_RELATIVE_PATH); + if path.ends_with('/') || path.ends_with('\\') { + path.pop(); + } - let response = match manifest::read(manifest_path.clone()).await { - Ok(manifest) => match manifest.run_touchup(&path).await { - Ok(()) => InteractThreadLocateGameResponse::Success, - Err(err) => { - warn!("Touchup failed during locate, treating as success: {}", err); - InteractThreadLocateGameResponse::Success + let path = PathBuf::from(path); + let manifest_path = path.join(MANIFEST_RELATIVE_PATH); + + let response = match manifest::read(manifest_path.clone()).await { + Ok(manifest) => { + let should_force_touchup = match tokio::fs::read_to_string(&manifest_path).await { + Ok(xml) => { + xml.contains("forceTouchupInstallerAfterUpdate=\"true\"") + || xml.contains("true") + } + Err(err) => { + warn!( + "Failed to inspect forceTouchupInstallerAfterUpdate during locate ({}), skipping touchup", + err + ); + false + } + }; + + if should_force_touchup { + match manifest.run_touchup(&path).await { + Ok(()) => InteractThreadLocateGameResponse::Success, + Err(err) => { + warn!("Touchup failed during locate, treating as success: {}", err); + InteractThreadLocateGameResponse::Success + } + } + } else { + InteractThreadLocateGameResponse::Success + } } - }, - Err(err) => InteractThreadLocateGameResponse::Error( - InteractThreadLocateGameFailure { - reason: err, - xml_path: manifest_path.to_string_lossy().into_owned(), - }, - ), - }; - - let _ = backend_responder - .send(MaximaLibResponse::LocateGameResponse(response)) - .ok(); - info!("finished locating"); - ctx.request_repaint(); - Ok(()) - } + Err(err) => InteractThreadLocateGameResponse::Error( + InteractThreadLocateGameFailure { + reason: err, + xml_path: manifest_path.to_string_lossy().into_owned(), + }, + ), + }; - MaximaLibRequest::InstallGameRequest(offer, path) => { - let mut maxima = maxima_arc.lock().await; - let builds = - maxima.content_manager().service().available_builds(&offer).await?; - let build = builds.live_build().ok_or(BackendError::NoLiveBuildAvailable)?; - - let game = QueuedGameBuilder::default() - .offer_id(offer) - .build_id(build.build_id().to_owned()) - .path(path) - .build()?; - Ok(maxima.content_manager().add_install(game).await?) - } - MaximaLibRequest::CancelInstallRequest(offer) => { - let mut maxima = maxima_arc.lock().await; - maxima.content_manager().cancel_install(&offer).await?; - Self::update_queue(maxima.content_manager(), backend_responder.clone()); - Ok(()) - } - MaximaLibRequest::PauseInstallRequest(offer) => { - let mut maxima = maxima_arc.lock().await; - maxima.content_manager().pause_install(&offer).await?; - Self::update_queue(maxima.content_manager(), backend_responder.clone()); - Ok(()) - } - MaximaLibRequest::MoveInstallToTopRequest(offer) => { - let mut maxima = maxima_arc.lock().await; - maxima.content_manager().move_install_to_top(&offer).await?; - Self::update_queue(maxima.content_manager(), backend_responder.clone()); - Ok(()) - } - MaximaLibRequest::StartGameRequest(info, settings) => { - Ok(start_game_request(maxima_arc.clone(), info, settings).await?) - } - MaximaLibRequest::ShutdownRequest => { - info!("BridgeThread: received shutdown request"); - break 'main Ok(()); - } - }; + let _ = backend_responder + .send(MaximaLibResponse::LocateGameResponse(response)) + .ok(); + info!("finished locating"); + ctx.request_repaint(); + Ok(()) + } - if let Err(err) = result { - let _ = - backend_responder.send(MaximaLibResponse::NonFatalError(Box::from(err))).ok(); - } + MaximaLibRequest::InstallGameRequest(offer, path) => { + let mut maxima = maxima_arc.lock().await; + let builds = maxima.content_manager().service().available_builds(&offer).await?; + let build = builds.live_build().ok_or(BackendError::NoLiveBuildAvailable)?; + let game = QueuedGameBuilder::default() + .offer_id(offer) + .build_id(build.build_id().to_owned()) + .path(path) + .build()?; + maxima.content_manager().add_install(game).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); // ← add this + Ok(()) + } + MaximaLibRequest::CancelInstallRequest(offer) => { + let mut maxima = maxima_arc.lock().await; + maxima.content_manager().cancel_install(&offer).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + Ok(()) + } + MaximaLibRequest::PauseInstallRequest(offer) => { + let mut maxima = maxima_arc.lock().await; + maxima.content_manager().pause_install(&offer).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + Ok(()) + } + MaximaLibRequest::MoveInstallToTopRequest(offer) => { + let mut maxima = maxima_arc.lock().await; + maxima.content_manager().move_install_to_top(&offer).await?; + Self::update_queue(maxima.content_manager(), backend_responder.clone()); + Ok(()) + } + MaximaLibRequest::StartGameRequest(info, settings) => { + Ok(start_game_request(maxima_arc.clone(), info, settings).await?) + } + MaximaLibRequest::ShutdownRequest => { + info!("BridgeThread: received shutdown request"); + break 'main Ok(()); + } + }; + if let Err(err) = result { + backend_responder.send(MaximaLibResponse::NonFatalError(Box::from(err))).ok(); + } + } + } puffin::GlobalProfiler::lock().new_frame(); } } diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index e797aaad..05135a27 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -126,9 +126,8 @@ fn main() { let app = MaximaEguiApp::new(cc, args); // TODO: Implement no_login bypass (skip auth initialization) if args.no_login { - return Ok(Box::new(app)); + log::warn!("--no-login is not yet implemented, ignoring"); } - Ok(Box::new(app)) }), ) @@ -197,6 +196,12 @@ pub struct GameSettings { impl GameSettings { pub fn new() -> Self { + Self::default() + } +} + +impl Default for GameSettings { + fn default() -> Self { Self { cloud_saves: true, launch_args: String::new(), @@ -314,27 +319,26 @@ pub struct MaximaEguiApp { pub offer_to_slug: HashMap, } -#[derive(serde::Serialize, serde::Deserialize, PartialEq, EnumIter)] +#[derive(Default, serde::Serialize, serde::Deserialize, PartialEq, EnumIter)] pub enum FrontendLanguage { + #[default] SystemDefault, EnUS, FrFR, } -#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)] +#[derive(Default, serde::Serialize, serde::Deserialize, Copy, Clone)] pub struct FrontendPerformanceSettings { disable_blur: bool, } impl FrontendPerformanceSettings { pub fn new() -> Self { - Self { - disable_blur: false, - } + Self::default() } } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Default, serde::Serialize, serde::Deserialize)] pub struct FrontendSettings { default_install_folder: String, language: FrontendLanguage, @@ -345,13 +349,7 @@ pub struct FrontendSettings { impl FrontendSettings { pub fn new() -> Self { - Self { - default_install_folder: String::new(), - language: FrontendLanguage::SystemDefault, - ignore_ood_games: false, - game_settings: HashMap::new(), - performance_settings: FrontendPerformanceSettings::new(), - } + Self::default() } } @@ -380,7 +378,6 @@ impl MaximaEguiApp { faint_bg_color: Color32::from_rgb(15, 20, 34), extreme_bg_color: Color32::from_rgb(20, 20, 20), window_fill: Color32::BLACK, - //override_text_color: Some(Color32::WHITE), hyperlink_color: F9B233, widgets: Widgets { hovered: WidgetVisuals { @@ -502,9 +499,9 @@ pub fn tab_bar_button(ui: &mut Ui, res: Response) { puffin::profile_function!(); let mut res2 = Rect::clone(&res.rect); res2.min.y = res2.max.y - 4.; - ui.painter().vline( - res2.min.x + 2.0, - RangeInclusive::new(res2.min.y, res2.max.y), + ui.painter().hline( + RangeInclusive::new(res2.min.x, res2.max.x), + res2.max.y - 2.0, Stroke::new(2.0, ACCENT_COLOR), ); ui.painter().rect_filled( @@ -521,7 +518,7 @@ pub fn tab_bar_button(ui: &mut Ui, res: Response) { /// We used to have a semi-functional implementation that only worked on Mac, but, as i would say, we do not care 🗣️🗣️🗣️ fn custom_window_frame( enabled: bool, - crash_text: String, + crash_text: &str, ui: &mut egui::Ui, _frame: &mut eframe::Frame, _title: &str, @@ -570,41 +567,43 @@ fn custom_window_frame( /// Wrapper/helper for the tab buttons in the top left of the app fn tab_button(ui: &mut Ui, edit_var: &mut PageType, page: PageType, label: &str) { - puffin::profile_function!(); - ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::ZERO; - ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::ZERO; - ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::ZERO; - ui.style_mut().visuals.widgets.inactive.expansion = -1.0; - ui.style_mut().visuals.widgets.active.expansion = -1.0; - ui.style_mut().visuals.widgets.hovered.expansion = -1.0; - - if edit_var == &page { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::WHITE; - ui.style_mut().visuals.widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::BLACK); - ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.active.weak_bg_fill = Color32::WHITE; - ui.style_mut().visuals.widgets.active.fg_stroke = Stroke::new(2.0, Color32::BLACK); - ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.hovered.weak_bg_fill = Color32::WHITE; - ui.style_mut().visuals.widgets.hovered.fg_stroke = Stroke::new(2.0, Color32::BLACK); - ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE; - } else { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; - ui.style_mut().visuals.widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::WHITE); - ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.active.weak_bg_fill = Color32::TRANSPARENT; - ui.style_mut().visuals.widgets.active.fg_stroke = Stroke::new(2.0, F9B233); - ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; - ui.style_mut().visuals.widgets.hovered.weak_bg_fill = Color32::TRANSPARENT; - ui.style_mut().visuals.widgets.hovered.fg_stroke = Stroke::new(2.0, F9B233); - ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE; - } - let text = egui::RichText::new(label.to_uppercase()).size(16.0); + ui.scope(|ui| { + puffin::profile_function!(); + ui.style_mut().visuals.widgets.inactive.corner_radius = CornerRadius::ZERO; + ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::ZERO; + ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::ZERO; + ui.style_mut().visuals.widgets.inactive.expansion = -1.0; + ui.style_mut().visuals.widgets.active.expansion = -1.0; + ui.style_mut().visuals.widgets.hovered.expansion = -1.0; + + if edit_var == &page { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::WHITE; + ui.style_mut().visuals.widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::BLACK); + ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.active.weak_bg_fill = Color32::WHITE; + ui.style_mut().visuals.widgets.active.fg_stroke = Stroke::new(2.0, Color32::BLACK); + ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.hovered.weak_bg_fill = Color32::WHITE; + ui.style_mut().visuals.widgets.hovered.fg_stroke = Stroke::new(2.0, Color32::BLACK); + ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE; + } else { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT; + ui.style_mut().visuals.widgets.inactive.fg_stroke = Stroke::new(2.0, Color32::WHITE); + ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.active.weak_bg_fill = Color32::TRANSPARENT; + ui.style_mut().visuals.widgets.active.fg_stroke = Stroke::new(2.0, F9B233); + ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE; + ui.style_mut().visuals.widgets.hovered.weak_bg_fill = Color32::TRANSPARENT; + ui.style_mut().visuals.widgets.hovered.fg_stroke = Stroke::new(2.0, F9B233); + ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE; + } + let text = egui::RichText::new(label.to_uppercase()).size(16.0); - let test = ui.add_sized([120.0, 28.0], egui::Button::new(text)); - if test.clicked() { - *edit_var = page; - } + let test = ui.add_sized([120.0, 28.0], egui::Button::new(text)); + if test.clicked() { + *edit_var = page; + } + }); } // god-awful macro to do something incredibly simple because apparently wrapping it in a function has rustc fucking implode @@ -710,6 +709,14 @@ impl MaximaEguiApp { FontId::proportional(10.0), Color32::WHITE, ); + } else { + rtl.painter().text( + point, + Align2::RIGHT_CENTER, + &self.user_name, + FontId::proportional(15.0), + Color32::WHITE, + ); } } else { rtl.painter().text( @@ -787,8 +794,8 @@ impl MaximaEguiApp { egui::Frame::default() .fill(Color32::from_black_alpha(200)) .outer_margin(Margin::symmetric( - (((app_rect.width() - 600.0) / 2.0).clamp(i8::MIN as f32, i8::MAX as f32)) as i8, - (((app_rect.height() - 400.0) / 2.0).clamp(i8::MIN as f32, i8::MAX as f32)) as i8, + ((app_rect.width() - 600.0) / 2.0).max(0.0) as i8, + ((app_rect.height() - 400.0) / 2.0).max(0.0) as i8, )) .inner_margin(Margin::same(12)) .corner_radius(CornerRadius::same(8)) @@ -1055,7 +1062,11 @@ impl MaximaEguiApp { if has_game_img { if self.game_sel.is_empty() && !self.games.is_empty() { - if let Some(key) = self.games.keys().next() { + if let Some(key) = self.games.keys().min_by(|a, b| { + let na = &self.games[*a].name; + let nb = &self.games[*b].name; + na.to_lowercase().cmp(&nb.to_lowercase()) + }) { self.game_sel = key.clone(); } } @@ -1198,11 +1209,7 @@ impl eframe::App for MaximaEguiApp { let ctx = ui.ctx().clone(); custom_window_frame( self.critical_error.is_none(), - if let Some(err) = &self.critical_error { - format!("{:?}", err) - } else { - String::new() - }, + self.critical_error.as_ref().map(|e| format!("{:?}", e)).as_deref().unwrap_or(""), ui, frame, "Maxima", diff --git a/maxima-ui/src/views/downloads_view.rs b/maxima-ui/src/views/downloads_view.rs index 979b8f37..7f9a8cd9 100644 --- a/maxima-ui/src/views/downloads_view.rs +++ b/maxima-ui/src/views/downloads_view.rs @@ -22,7 +22,9 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is let container_size = vec2(ui.available_width(), 160.0); ui.allocate_ui(container_size, |ui| { let game_dl = game; - let game = app.games.get_mut(&game.slug).unwrap(); + let Some(game) = app.games.get_mut(&game_dl.slug) else { + return; // game not loaded yet, skip rendering this frame + }; let (hero, logo) = { ( app.img_cache.get(crate::ui_image::UIImageType::Hero(game.slug.clone())), @@ -173,6 +175,12 @@ fn render_queued(app: &mut MaximaEguiApp, ui: &mut Ui, game: &QueuedDownload, is .backend_commander .send(MaximaLibRequest::CancelInstallRequest(offer)); } + if ui.put(right_button_rect, egui::Button::new("⏸")).clicked() { + let _ = app + .backend + .backend_commander + .send(MaximaLibRequest::PauseInstallRequest(game_dl.offer.clone())); + } if ui.put(right_button_rect, egui::Button::new("⮉")).clicked() { let _ = app.backend.backend_commander.send( MaximaLibRequest::MoveInstallToTopRequest(game_dl.offer.clone()), diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index cd15791e..e67f88fb 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -1,4 +1,4 @@ -use crate::util::{easy_mark::easy_mark}; +use crate::util::easy_mark::easy_mark; use crate::{ GameDetails, GameDetailsWrapper, GameInfo, InstallModalState, MaximaEguiApp, PageType, PopupModal, bridge_thread, set_app_modal, translation_manager::TranslationManager, @@ -190,10 +190,32 @@ fn game_view_system_requirements( pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { puffin::profile_function!(); - if app.games.len() < 1 { + if app.games.is_empty() { return; } - let game: GameInfo = if let Some(game) = app.games.get_mut(&app.game_sel) { + let game_details: Option = { + let game = if let Some(g) = app.games.get_mut(&app.game_sel) { + g + } else { + return; + }; + match &game.details { + GameDetailsWrapper::Unloaded => { + app.backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::GetGameDetailsRequest( + game.slug.clone(), + )) + .unwrap_or_else(|_| ()); + game.details = GameDetailsWrapper::Loading; + None + } + GameDetailsWrapper::Loading => None, + GameDetailsWrapper::Available(details) => Some(details.clone()), + } + }; + + let game: GameInfo = if let Some(game) = app.games.get(&app.game_sel) { game.clone() } else { return; @@ -205,21 +227,6 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ) }; - let game_details: Option = match &game.details { - GameDetailsWrapper::Unloaded => { - app.backend - .backend_commander - .send(bridge_thread::MaximaLibRequest::GetGameDetailsRequest( - game.slug.clone(), - )) - .unwrap_or_else(|_| ()); - app.games.get_mut(&app.game_sel).unwrap().details = GameDetailsWrapper::Loading; - None - } - GameDetailsWrapper::Loading => None, - GameDetailsWrapper::Available(details) => Some(details.clone()), - }; - let mut hero_rect = Rect::clone(&ui.available_rect_before_wrap()); let aspect_ratio: f32 = if let Some(handle) = &hero { handle.aspect_ratio() @@ -244,10 +251,9 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ui.style_mut().visuals.widgets.hovered.corner_radius = CornerRadius::same(4); let mut logo_transition_frac = 0.0; - ScrollArea::vertical() - .auto_shrink(false) - .id_salt("GameViewPanel_ScrollerArea") - .show(ui, |ui| { + ScrollArea::vertical().auto_shrink(false).id_salt("GameViewPanel_ScrollerArea").show( + ui, + |ui| { puffin::profile_scope!("details"); let hero_height_capped = hero_rect.size().y.max(0.0); ui.allocate_space(vec2(0.0, hero_height_capped)); @@ -278,7 +284,11 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ); } } else { - ui.painter().rect_filled(hero_rect, CornerRadius::same(0), Color32::TRANSPARENT); + ui.painter().rect_filled( + hero_rect, + CornerRadius::same(0), + Color32::TRANSPARENT, + ); } if hero_vis_frac < 1.0 @@ -310,7 +320,8 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { let avoid_scrollbar_margin = Margin { left: 0, right: (ui.style().spacing.scroll.bar_width - + ui.style().spacing.scroll.bar_inner_margin) as i8, + + ui.style().spacing.scroll.bar_inner_margin) + as i8, top: 0, bottom: 0, }; @@ -357,7 +368,7 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ); stats.label( RichText::new(format!( - ": {:?} / {:?}", + ": {} / {}", details.achievements_unlocked, details.achievements_total )) @@ -407,35 +418,36 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { Color32::from_black_alpha(128), ); dlc.scope_builder(UiBuilder::new().max_rect(rect), |dlc| { - let _ = match package.product().status() { - maxima::core::service_layer::ServiceOwnershipStatus::Active => true, - maxima::core::service_layer::ServiceOwnershipStatus::Disabled => false, - }; - - dlc.heading(format!("{}", package.offer().display_name())); + let is_active = matches!( + package.product().status(), + maxima::core::service_layer::ServiceOwnershipStatus::Active + ); + dlc.add_enabled_ui(is_active, |dlc| { + dlc.heading(package.offer().display_name()); + }); }); } }); { puffin::profile_scope!("filler"); - for _idx in 0..75 { - ui.heading(""); - } + ui.allocate_space(vec2( + ui.available_width(), + 75.0 * ui.text_style_height(&egui::TextStyle::Heading), + )); } }); - }); // ScrollArea + }, + ); // ScrollArea if let Some(handle) = logo { let logo_size_pre = if handle.size_vec2().x >= handle.size_vec2().y { - // wider than it is tall, scale based on X as max let mult_frac = 320.0 / handle.size_vec2().x; handle.size_vec2().y * mult_frac } else { - // taller than it is wide, scale based on Y - // fringe edge case, here in case EA decides they want to pull something really fucking stupid - 0.0 // TODO: CALCULATE IT + let mult_frac = 160.0 / handle.size_vec2().y; + handle.size_vec2().x * mult_frac }; - let frac2 = logo_transition_frac.clone(); + let frac2 = logo_transition_frac; let logo_size = vec2( egui::lerp(320.0..=160.0, frac2), egui::lerp(logo_size_pre..=(logo_size_pre / 2.0), frac2), @@ -456,24 +468,34 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { } fn game_list_button_context_menu(app: &MaximaEguiApp, game: &GameInfo, ui: &mut Ui) { - ui.add_enabled_ui(app.playing_game.is_none(), |play_button| { - if play_button.button("▶ Play").clicked() { - let settings = app.settings.game_settings.get(&game.slug); - let settings = if let Some(settings) = settings { - Some(settings.to_owned()) - } else { - None - }; - let _ = app.backend.backend_commander.send( - crate::bridge_thread::MaximaLibRequest::StartGameRequest(game.clone(), settings), - ); - play_button.close_kind(UiKind::Menu); - } - }); - ui.separator(); - if ui.button("UNINSTALL").clicked() { - ui.close_kind(UiKind::Menu); + let is_ood = !app.settings.ignore_ood_games && game.version.installed != game.version.latest; + if is_ood { + ui.add_enabled_ui(false, |play_button| { + play_button.button("⚠ Update required"); + }); + } else { + ui.add_enabled_ui(app.playing_game.is_none(), |play_button| { + if play_button.button("▶ Play").clicked() { + let settings = app.settings.game_settings.get(&game.slug); + let settings = if let Some(settings) = settings { + Some(settings.to_owned()) + } else { + None + }; + let _ = app.backend.backend_commander.send( + crate::bridge_thread::MaximaLibRequest::StartGameRequest( + game.clone(), + settings, + ), + ); + play_button.close_kind(UiKind::Menu); + } + }); } + ui.separator(); + ui.add_enabled_ui(false, |ui| { + ui.button("UNINSTALL (not yet implemented)"); + }); } const F9B233: Color32 = Color32::from_rgb(249, 178, 51); @@ -657,7 +679,7 @@ fn show_game_list_buttons(app: &mut MaximaEguiApp, ui: &mut Ui) { pub fn games_view(app: &mut MaximaEguiApp, ui: &mut Ui) { puffin::profile_function!(); - if app.games.len() < 1 { + if app.games.is_empty() { ui.with_layout( egui::Layout::centered_and_justified(egui::Direction::RightToLeft), |ui| { From 40669299fd6747c60d75b8e450886a49de1cedc5 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:47:49 +0200 Subject: [PATCH 18/57] fixes, preliminary cleanups, will need further cleanup --- Cargo.lock | 847 ++++++++------- Cargo.toml | 31 +- maxima-bootstrap/Cargo.toml | 7 +- maxima-bootstrap/src/main.rs | 32 +- maxima-cli/Cargo.toml | 6 +- maxima-cli/src/main.rs | 30 +- maxima-lib/Cargo.toml | 22 +- maxima-lib/src/content/downloader.rs | 1246 ++++++++++++++--------- maxima-lib/src/content/manager.rs | 100 +- maxima-lib/src/content/mod.rs | 4 +- maxima-lib/src/content/zlib.rs | 219 +--- maxima-lib/src/core/auth/context.rs | 8 +- maxima-lib/src/core/auth/hardware.rs | 12 +- maxima-lib/src/core/auth/login.rs | 17 +- maxima-lib/src/core/auth/mod.rs | 17 +- maxima-lib/src/core/cache.rs | 11 +- maxima-lib/src/core/library.rs | 26 +- maxima-lib/src/core/manifest/pre_dip.rs | 26 +- maxima-lib/src/core/mod.rs | 12 +- maxima-lib/src/core/service_layer.rs | 18 +- maxima-lib/src/lsx/request/account.rs | 2 +- maxima-lib/src/lsx/request/offer.rs | 2 +- maxima-lib/src/lsx/request/profile.rs | 4 +- maxima-lib/src/util/registry.rs | 168 +-- maxima-lib/src/util/service_win.rs | 150 +-- maxima-resources/Cargo.toml | 6 +- maxima-service/Cargo.toml | 6 +- maxima-service/src/service/error.rs | 2 + maxima-service/src/service/mod.rs | 161 ++- maxima-tui/Cargo.toml | 6 +- maxima-ui/src/bridge/game_details.rs | 2 +- maxima-ui/src/bridge/get_games.rs | 4 +- maxima-ui/src/bridge_thread.rs | 41 +- maxima-ui/src/event_thread.rs | 2 +- 34 files changed, 1859 insertions(+), 1388 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06dca3e1..ec138b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-core", "futures-sink", @@ -40,8 +40,8 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64", - "bitflags 2.13.0", + "base64 0.22.1", + "bitflags 2.13.1", "brotli", "bytes", "bytestring", @@ -76,7 +76,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -178,7 +178,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.4", + "socket2 0.6.5", "time", "tracing", "url", @@ -193,7 +193,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -290,7 +290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.13.0", + "bitflags 2.13.1", "cc", "jni", "libc", @@ -299,7 +299,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -369,9 +369,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -416,7 +416,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -443,7 +443,8 @@ dependencies = [ [[package]] name = "async-compression" version = "0.4.42" -source = "git+https://github.com/sjp761/async-compression.git#a226c72f754f942ad187cc65cb1f03df5ae26b7b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -453,13 +454,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -471,6 +472,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -498,7 +508,7 @@ dependencies = [ "num-traits", "pastey 0.1.1", "rayon", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "y4m", ] @@ -528,9 +538,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -538,9 +548,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", @@ -576,6 +586,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[package]] name = "bincode" version = "1.3.3" @@ -614,9 +630,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -732,22 +748,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -764,9 +780,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytestring" @@ -801,7 +817,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "log", "polling", "rustix 0.38.44", @@ -815,7 +831,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "polling", "rustix 1.1.4", "slab", @@ -854,9 +870,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -872,9 +888,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "cgl" @@ -924,9 +940,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -934,9 +950,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -946,14 +962,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -980,6 +996,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.19", +] + [[package]] name = "color" version = "0.3.3" @@ -1055,7 +1080,8 @@ dependencies = [ [[package]] name = "compression-codecs" version = "0.4.38" -source = "git+https://github.com/sjp761/async-compression.git#a226c72f754f942ad187cc65cb1f03df5ae26b7b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "compression-core", "flate2", @@ -1065,7 +1091,8 @@ dependencies = [ [[package]] name = "compression-core" version = "0.4.32" -source = "git+https://github.com/sjp761/async-compression.git#a226c72f754f942ad187cc65cb1f03df5ae26b7b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -1206,18 +1233,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1225,18 +1252,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -1244,7 +1271,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm_winapi", "derive_more", "document-features", @@ -1337,7 +1364,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1350,7 +1377,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1361,7 +1388,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1372,7 +1399,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1410,7 +1437,7 @@ checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1431,7 +1458,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1441,7 +1468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1463,7 +1490,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", "unicode-xid", ] @@ -1527,7 +1554,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", ] @@ -1539,7 +1566,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1649,7 +1676,7 @@ checksum = "2796c98d50b79631281d516343a6f6e93c0666462ca36e2c93b39f25d7793325" dependencies = [ "accesskit", "ahash", - "bitflags 2.13.0", + "bitflags 2.13.1", "emath", "epaint", "itertools", @@ -1746,6 +1773,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "enable-ansi-support" version = "0.3.1" @@ -1781,7 +1820,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1792,7 +1831,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1844,7 +1883,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1891,14 +1930,16 @@ dependencies = [ [[package]] name = "exr" -version = "1.74.0" +version = "1.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide 0.8.9", + "num-complex", + "pulp", "rayon-core", "smallvec", "zune-inflate", @@ -1932,9 +1973,9 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fax" @@ -2004,12 +2045,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.10" -source = "git+https://github.com/sjp761/flate2-rs.git#45fc9f6a56a97513b285029fee14213a2d0fe2e5" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide 0.9.1", + "miniz_oxide 0.8.9", ] [[package]] @@ -2073,7 +2115,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2105,9 +2147,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -2120,9 +2162,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -2130,15 +2172,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -2147,38 +2189,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -2212,9 +2254,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" dependencies = [ "rustversion", "typenum", @@ -2259,11 +2301,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -2273,9 +2313,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -2345,7 +2387,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg_aliases", "cgl", "dispatch2", @@ -2437,12 +2479,21 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytemuck", "read-fonts", "smallvec", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -2474,6 +2525,20 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -2524,9 +2589,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http 1.4.2", @@ -2534,9 +2599,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -2617,7 +2682,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -2628,7 +2693,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.6.5", "tokio", "tower-service", "tracing", @@ -2860,7 +2925,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -2878,7 +2943,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2889,17 +2954,17 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "io-kit-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +checksum = "06d3a048d09fbb6597dbf7c69f40d14df4a49487db1487191618c893fc3b1c26" dependencies = [ "core-foundation-sys", - "mach2", + "mach2 0.5.0", ] [[package]] @@ -2969,7 +3034,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2984,7 +3049,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3012,16 +3077,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] @@ -3044,7 +3109,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3138,7 +3203,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", "plain", "redox_syscall 0.9.0", @@ -3162,7 +3227,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3242,9 +3307,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ "hashbrown 0.17.1", ] @@ -3284,13 +3349,19 @@ dependencies = [ [[package]] name = "mach2" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3304,15 +3375,14 @@ dependencies = [ name = "maxima-bootstrap" version = "0.1.0" dependencies = [ - "base64", + "base64 0.23.0", "cacao", "maxima-lib", "maxima-resources", - "querystring", "reqwest", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "url", "urlencoding", @@ -3326,7 +3396,7 @@ name = "maxima-cli" version = "0.1.0" dependencies = [ "anyhow", - "base64", + "base64 0.23.0", "clap", "futures", "inquire", @@ -3344,13 +3414,13 @@ dependencies = [ [[package]] name = "maxima-lib" -version = "1.0.0" +version = "0.1.0" dependencies = [ "aes", "async-compression", "async-trait", "base62", - "base64", + "base64 0.23.0", "bytebuffer", "bytes", "cbc", @@ -3365,22 +3435,24 @@ dependencies = [ "filetime", "flate2", "futures", - "generic-array 1.4.3", + "generic-array 1.4.4", "gethostname", "glob", "hex", "http 1.4.2", "is_elevated", + "libz-sys", "log", "mac_address", "md5", + "miniz_oxide 0.9.1", "moka", "open", "pastey 0.2.3", "pelite", + "postcard", "prost", "prost-build", - "querystring", "quick-xml 0.41.0", "rand 0.10.2", "regex", @@ -3388,19 +3460,21 @@ dependencies = [ "ring", "rustls", "rustls-pki-types", + "scopeguard", "serde", "serde_json", - "sha2-const", + "sha1", + "sha2 0.11.0", "smbios-lib", "strum_macros", "sysinfo", "tar", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls", "tokio-stream", "tokio-util", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+spec-1.1.0", "ureq", "url", "uuid", @@ -3426,7 +3500,7 @@ name = "maxima-service" version = "0.1.0" dependencies = [ "actix-web", - "base64", + "base64 0.23.0", "derive_more", "directories", "hex", @@ -3439,7 +3513,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "structured-logger", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "windows-service", "windows-sys 0.61.2", @@ -3451,7 +3525,7 @@ name = "maxima-tui" version = "0.1.0" dependencies = [ "anyhow", - "base64", + "base64 0.23.0", "clap", "color-eyre", "inquire", @@ -3495,7 +3569,7 @@ dependencies = [ "strum", "strum_macros", "sys-locale", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "windows-sys 0.61.2", ] @@ -3512,15 +3586,15 @@ dependencies = [ [[package]] name = "md5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -3587,14 +3661,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", - "simd-adler32", + "serde", ] [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -3641,7 +3715,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -3677,7 +3751,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -3690,7 +3764,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -3753,14 +3827,24 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3775,7 +3859,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3826,7 +3910,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3878,7 +3962,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3894,7 +3978,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -3907,7 +3991,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location 0.2.2", @@ -3920,7 +4004,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -3942,7 +4026,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3964,7 +4048,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", ] @@ -3975,7 +4059,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -4032,7 +4116,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -4050,7 +4134,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "dispatch", "libc", @@ -4063,7 +4147,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -4086,7 +4170,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4109,7 +4193,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4132,7 +4216,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4145,7 +4229,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -4167,7 +4251,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit 0.2.2", @@ -4188,7 +4272,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "objc2 0.6.4", "objc2-cloud-kit 0.3.2", @@ -4220,7 +4304,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location 0.2.2", @@ -4269,9 +4353,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.6" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" dependencies = [ "is-wsl", "libc", @@ -4351,7 +4435,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4435,9 +4519,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" dependencies = [ "memchr", "ucd-trie", @@ -4445,9 +4529,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" dependencies = [ "pest", "pest_generator", @@ -4455,25 +4539,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" dependencies = [ "pest", - "sha2 0.10.9", ] [[package]] @@ -4514,7 +4597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -4527,7 +4610,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "unicase", ] @@ -4564,7 +4647,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4604,7 +4687,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -4636,9 +4719,22 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] [[package]] name = "potential_utf" @@ -4671,7 +4767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4685,9 +4781,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -4708,7 +4804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4736,7 +4832,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.118", + "syn 2.0.119", "tempfile", ] @@ -4750,7 +4846,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4791,11 +4887,34 @@ dependencies = [ "puffin", ] +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pxfm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "qoi" @@ -4806,12 +4925,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "querystring" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" - [[package]] name = "quick-error" version = "2.0.1" @@ -4850,8 +4963,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.4", - "thiserror 2.0.18", + "socket2 0.6.5", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -4859,21 +4972,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.15" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -4881,23 +4995,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.6.5", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -4916,18 +5030,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -4975,6 +5089,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "ratatui" version = "0.30.2" @@ -4997,7 +5120,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "compact_str", "critical-section", "hashbrown 0.17.1", @@ -5007,7 +5130,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -5062,7 +5185,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "hashbrown 0.17.1", "indoc", "instability", @@ -5103,10 +5226,10 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha", "simd_helpers", - "thiserror 2.0.18", + "thiserror 2.0.19", "v_frame", "wasm-bindgen", ] @@ -5126,6 +5249,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -5162,6 +5294,12 @@ dependencies = [ "font-types", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -5177,7 +5315,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -5186,7 +5324,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -5197,14 +5335,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -5214,9 +5352,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -5241,8 +5379,9 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http 1.4.2", @@ -5319,7 +5458,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "once_cell", "serde", "serde_derive", @@ -5335,9 +5474,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" @@ -5360,7 +5499,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5373,7 +5512,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -5382,9 +5521,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -5459,9 +5598,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -5505,7 +5644,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5524,9 +5663,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -5536,9 +5675,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5546,22 +5685,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -5575,9 +5714,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -5640,12 +5779,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "sha2-const" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb83f144cbda94be5d2278400eee9e0a03eb68b5504349943655b7b1603c4af" - [[package]] name = "sharded-slab" version = "0.1.7" @@ -5694,15 +5827,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -5774,16 +5907,16 @@ dependencies = [ [[package]] name = "smbios-lib" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c18320ad3d997a100cb948fc020111936c530eddfde947f467083730e39e72" +checksum = "e739c9b449374b4e9435857e0084895a59ef79b46d1c2b0f976c1c564ae7e4bc" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "getopts", "io-kit-sys", "libc", - "mach2", + "mach2 0.6.0", "serde", "serde_json", ] @@ -5794,7 +5927,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "calloop 0.14.4", "calloop-wayland-source", "cursor-icon", @@ -5802,7 +5935,7 @@ dependencies = [ "log", "memmap2", "rustix 1.1.4", - "thiserror 2.0.18", + "thiserror 2.0.19", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -5847,14 +5980,23 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5913,7 +6055,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6023,9 +6165,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -6049,7 +6202,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6063,9 +6216,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.5" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", @@ -6111,7 +6264,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "parking_lot", "rustix 1.1.4", "signal-hook", @@ -6146,8 +6299,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64", - "bitflags 2.13.0", + "base64 0.22.1", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -6192,11 +6345,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -6207,25 +6360,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] @@ -6314,9 +6467,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -6329,9 +6482,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -6339,20 +6492,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.4", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6401,9 +6554,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap", "serde_core", @@ -6425,9 +6578,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", @@ -6446,9 +6599,9 @@ dependencies = [ [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -6472,7 +6625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-core", "futures-util", @@ -6520,7 +6673,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6637,7 +6790,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "percent-encoding", @@ -6654,7 +6807,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ - "base64", + "base64 0.22.1", "http 1.4.2", "httparse", "log", @@ -6684,7 +6837,7 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ - "base64", + "base64 0.22.1", "data-url", "flate2", "imagesize", @@ -6720,9 +6873,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", "getrandom 0.4.3", @@ -6750,9 +6903,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +checksum = "5dd4ec1eb1d240636e354a30110a1dfcb37047169a4d9bd6d9d3469df574b5c4" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -6760,9 +6913,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +checksum = "2a8e44fd7ce9cf838a1e2dad56d2d83f2b2435ae1df9cb3cac31e759e455e88d" dependencies = [ "erased-serde", "serde_core", @@ -6771,9 +6924,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +checksum = "3f9f1705b87b798b06a8d7a51f44ed0626eee413991555e8edfd3562b35a130d" dependencies = [ "sval", "sval_buffer", @@ -6803,7 +6956,7 @@ dependencies = [ "log", "peniko", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -6909,7 +7062,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -6955,7 +7108,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "rustix 1.1.4", "wayland-backend", "wayland-scanner", @@ -6967,7 +7120,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cursor-icon", "wayland-backend", ] @@ -6989,7 +7142,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7001,7 +7154,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7014,7 +7167,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7027,7 +7180,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7095,18 +7248,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -7279,7 +7432,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7290,7 +7443,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7324,7 +7477,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "widestring", "windows-sys 0.61.2", ] @@ -7520,7 +7673,7 @@ checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ "android-activity", "atomic-waker", - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "calloop 0.13.0", "cfg_aliases", @@ -7554,9 +7707,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -7596,7 +7749,7 @@ dependencies = [ "futures", "log", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows", "windows-core", ] @@ -7646,7 +7799,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dlib", "log", "once_cell", @@ -7705,28 +7858,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7746,7 +7899,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -7786,14 +7939,14 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index dfc93120..9da97797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,22 +12,21 @@ members = [ [workspace.dependencies] # Common Workspace Dependencies for all Maxima libs -tokio = { version = "1.52.3", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" +tokio = { version = "1.53.0", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } -base64 = "0.22.1" -thiserror = "2.0.18" -anyhow = "1.0.103" +base64 = "0.23.0" +thiserror = "2.0.19" +anyhow = "1.0.104" log = "0.4.33" -regex = "1.12.4" -futures = "0.3.32" -clap = { version = "4.6.1", features = ["derive"] } +regex = "1.13.1" +futures = "0.3.33" +clap = { version = "4.6.4", features = ["derive"] } inquire = "0.9.4" strum = { version = "0.28.0", features = ["derive"] } strum_macros = "0.28.0" url = "2.5.8" -querystring = "1.1.0" directories = "6.0.0" hex = "0.4.3" @@ -37,6 +36,12 @@ winreg = "0.56.0" windows-service = "0.8.1" is_elevated = "0.1.2" +[workspace.package] +version = "0.1.0" +edition = "2024" +authors = ["Nick Whelan ", "Sean Kahler ", "Maxima Contributors"] +rust-version = "1.96.1" + [profile.release] strip = true lto = "thin" @@ -54,8 +59,4 @@ debug = true split-debuginfo = "unpacked" [profile.dev.package."*"] -opt-level = 1 - -[patch.crates-io] -flate2 = { git = 'https://github.com/sjp761/flate2-rs.git' } -async-compression = { git = 'https://github.com/sjp761/async-compression.git' } \ No newline at end of file +opt-level = 1 \ No newline at end of file diff --git a/maxima-bootstrap/Cargo.toml b/maxima-bootstrap/Cargo.toml index 9f558d98..4ce10e21 100644 --- a/maxima-bootstrap/Cargo.toml +++ b/maxima-bootstrap/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "maxima-bootstrap" description = "Maxima handler for custom EA protocols" -version = "0.1.0" -authors = ["Sean Kahler "] -edition = "2024" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } [dependencies] maxima-lib = { path = "../maxima-lib" } @@ -13,7 +13,6 @@ serde_json = { workspace = true } reqwest = { workspace = true, features = ["query"] } base64 = { workspace = true } url = { workspace = true } -querystring = { workspace = true } urlencoding = "2.1.3" thiserror = { workspace = true } diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index 629f0c04..407c27c7 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -14,7 +14,7 @@ use maxima::util::BackgroundServiceControlError; use maxima::util::native::NativeError; #[cfg(windows)] use maxima::util::service::{is_service_valid, register_service}; -use url::Url; +use url::{Url, form_urlencoded}; #[cfg(target_os = "macos")] mod macos; @@ -153,20 +153,30 @@ async fn run(args: &[String]) -> Result { if arg.starts_with("origin2") { let url = Url::parse(arg)?; - let query = querystring::querify(url.query().unwrap()); - let _offer_id = query.iter().find(|(x, _)| *x == "offerIds").unwrap().1; - let cmd_params = query.iter().find(|(x, _)| *x == "cmdParams").unwrap().1; + let mut offer_id = None; + let mut cmd_params = None; + + for (key, value) in form_urlencoded::parse(url.query().unwrap_or_default().as_bytes()) { + match key.as_ref() { + "offerIds" => offer_id = Some(value), + "cmdParams" => cmd_params = Some(value), + _ => {} + } + } + + let _offer_id = offer_id.unwrap(); + let cmd_params = cmd_params.unwrap(); let mut child = Command::new(current_exe()?.with_file_name("maxima-cli.exe")); child.env( "MAXIMA_LAUNCH_ARGS", - urlencoding::decode(cmd_params)? + urlencoding::decode(cmd_params.as_ref())? .into_owned() .replace("\\\"", "\""), ); println!( "{}", - urlencoding::decode(cmd_params)? + urlencoding::decode(cmd_params.as_ref())? .into_owned() .replace("\\\"", "\"") ); @@ -178,7 +188,15 @@ async fn run(args: &[String]) -> Result { } if arg.starts_with("qrc") { - let query = arg.split("login_successful.html?").collect::>()[1]; + let raw_query = arg + .split("login_successful.html?") + .nth(1) + .unwrap_or_default(); + let mut serializer = form_urlencoded::Serializer::new(String::new()); + for (key, value) in form_urlencoded::parse(raw_query.as_bytes()) { + serializer.append_pair(key.as_ref(), value.as_ref()); + } + let query = serializer.finish(); reqwest::get(format!("http://127.0.0.1:31033/auth?{}", query)).await?; return Ok(true); diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index 747a544f..5fb9ad7e 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "maxima-cli" -version = "0.1.0" -authors = ["Sean Kahler "] -edition = "2024" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } [dependencies] maxima-lib = { path = "../maxima-lib" } diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index c6b90797..8a6cf37d 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -25,29 +25,15 @@ use maxima::{ ContentService, downloader::ZipDownloader, manager::{QueuedGame, QueuedGameBuilder}, - }, - core::{ - LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, - auth::{ - TokenResponse, - context::AuthContext, - login::{begin_oauth_login_flow, manual_login}, - nucleus_auth_exchange, nucleus_token_exchange, - }, - clients::JUNO_PC_CLIENT_ID, - cloudsync::CloudSyncLockMode, - launch::{self, LaunchMode, LaunchOptions}, - library::OwnedTitle, - manifest::{self, MANIFEST_RELATIVE_PATH}, - service_layer::{ + }, core::{ + LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, auth::{ + TokenResponse, context::AuthContext, login::{begin_oauth_login_flow, manual_login}, nucleus_auth_exchange, nucleus_token_exchange, + }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH}, service_layer::{ SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceLegacyOffer, ServicePlayer, }, - }, - ooa, - rtm::client::BasicPresence, - util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, + }, ooa, rtm::client::BasicPresence, util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, }; static MANUAL_LOGIN_PATTERN: LazyLock = @@ -500,7 +486,7 @@ async fn download_specific_file( debug!("URL: {}", url.url()); - let downloader = ZipDownloader::new("test-game", &url.url(), "C:/DownloadTest").await?; + let downloader = ZipDownloader::new("test-game", &url.url(), std::path::Path::new("C:/DownloadTest")).await?; let num_of_entries = downloader.manifest().entries().len(); info!("Entries: {}", num_of_entries); @@ -656,7 +642,7 @@ async fn get_user_by_id(maxima_arc: LockedMaxima, user_id: &str) -> Result<()> { let player: ServicePlayer = maxima .service_layer() .request( - SERVICE_REQUEST_GETBASICPLAYER, + &SERVICE_REQUEST_GETBASICPLAYER, ServiceGetBasicPlayerRequestBuilder::default() .pd(user_id.to_string()) .build()?, @@ -727,7 +713,7 @@ async fn get_legacy_catalog_def(maxima_arc: LockedMaxima, offer_id: &str) -> Res let defs: Vec = maxima .service_layer() .request( - SERVICE_REQUEST_GETLEGACYCATALOGDEFS, + &SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetLegacyCatalogDefsRequestBuilder::default() .offer_ids(vec![offer_id.to_owned()]) .locale(maxima.locale().clone()) diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 80dfd1c0..1ab63758 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "maxima-lib" -version = "1.0.0" -edition = "2024" -authors = ["Sean Kahler "] +version = { workspace = true } +edition = { workspace = true } +authors = { workspace = true } categories = ["games"] description = "A free and open-source replacement for the EA Desktop Launcher" keywords = ["electronic-arts", "linux-gaming"] @@ -10,9 +10,12 @@ license = "GPL-3.0" readme = "README.md" repository = "https://github.com/ArmchairDevelopers/Maxima" documentation = "https://docs.rs/maxima" -rust-version = "1.96.0" +rust-version = { workspace = true } [dependencies] +postcard = { version = "1.1.3", features = ["use-std"] } +flate2 = { version = "1.1.9", features = ["zlib"] } +scopeguard = "1.2.0" tokio = { workspace = true } moka = { version = "0.12.15", features = ["sync"] } serde = { workspace = true } @@ -20,7 +23,7 @@ serde_json = { workspace = true } quick-xml = { version = "0.41.0", features = ["serialize"] } pastey = "0.2.3" hex = { workspace = true } -reqwest = { workspace = true, features = ["form", "query", "gzip", "json"] } +reqwest = { workspace = true, features = ["blocking", "form", "query", "gzip", "json"] } base64 = { workspace = true } base62 = "2.2.4" log = { workspace = true } @@ -29,13 +32,12 @@ chrono = "0.4.45" regex = { workspace = true } directories = { workspace = true } open = "5.3.6" -querystring = { workspace = true } ureq = { version = "3.3.0", features = ["_tls"] } sysinfo = "0.39.5" strum_macros = { workspace = true } -sha2-const = "0.1.3" +sha2 = "0.11.0" tar = "0.4.46" -flate2 = { version = "=1.1.10", default-features = false, features = ["zlib-default"] } +miniz_oxide = { version = "0.9.1", features = ["serde"] } xz2 = "0.1.7" zstd = "0.13.3" toml = "1.1.2" @@ -49,7 +51,7 @@ ring = "0.17.14" futures = { workspace = true } prost = "0.14.4" encoding_rs = "0.8.35" -async-compression = { version = "=0.4.42", features = ["tokio", "deflate"] } +async-compression = { version = "0.4.42", features = ["tokio", "deflate"] } tokio-util = { version = "0.7.18", features = ["io", "compat"] } uuid = "1.23.4" tokio-rustls = "0.26.4" @@ -71,6 +73,8 @@ http = "1.4.2" generic-array = "1.4.3" rustls-pki-types = "1.15.0" tokio-stream = "0.1.18" +sha1 = "0.11.0" +libz-sys = "1.1.29" [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index fb0fc291..6fb2cb6e 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -1,645 +1,901 @@ use std::{ - io::{self, Cursor, Read, SeekFrom}, - path::{Path, PathBuf}, - pin::Pin, - sync::Arc, - task, + fs::{self, File, OpenOptions}, io::{self, BufReader, BufWriter, Read, Write}, path::{Path, PathBuf}, sync::Arc, time::Duration, }; -use bytes::{Buf, BufMut, Bytes, BytesMut}; - -use crate::{ - content::{ - manager::DownloaderError, - zip::{CompressionType, ZipFile, ZipFileEntry}, - }, - util::native::{SafeParent, maxima_dir}, +use miniz_oxide::{ + DataFormat, MZError, MZFlush, MZStatus, + inflate::stream::{InflateState, inflate}, }; -use async_trait::async_trait; -use derive_getters::Getters; -use flate2::{Decompress, bufread::DeflateDecoder as BufreadDeflateDecoder}; -use futures::{Stream, StreamExt, TryStreamExt}; -use log::{debug, error, warn}; -use reqwest::Client; -use thiserror::Error; -use tokio::{ - fs::{File, OpenOptions, create_dir, create_dir_all}, - io::{AsyncSeekExt, AsyncWrite, BufReader, BufWriter}, - sync::Mutex as AsyncMutex, +use reqwest::{ + StatusCode, + blocking::{Client, Response}, + header::{ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_RANGE, ETAG, IF_RANGE, RANGE}, }; -use tokio_util::compat::FuturesAsyncReadCompatExt; - -async fn zstate_path(id: &str, path: &str) -> Result { - let mut path = maxima_dir()?.join("temp/downloader").join(id).join(path); - path.set_extension("eazstate"); - tokio::fs::create_dir_all(path.safe_parent()?).await?; - Ok(path) +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use url::Url; +use crate::content::zip::{CompressionType, ZipError, ZipFile, ZipFileEntry}; + +const CHECKPOINT_MAGIC: [u8; 4] = *b"MDL1"; +const CHECKPOINT_VERSION: u16 = 1; +const BUFFER_SIZE: usize = 64 * 1024; +const REPLAY_BUFFER_SIZE: usize = 64 * 1024; +const INFLATE_CHUNK: usize = 64 * 1024; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum CompressionFormat { + /// A normal zlib stream: RFC 1950 + RFC 1951. + Zlib, + + /// A bare DEFLATE stream: RFC 1951. + RawDeflate, } -trait DownloadDecoder: Send { - fn save_state(&mut self, buf: &mut BytesMut); - fn restore_state(&mut self, buf: &mut Bytes); - fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError>; - fn write_in_pos(&self) -> u64; - fn write_out_pos(&self) -> u64; - fn get_mut(&mut self) -> Arc>>; - fn supports_resume(&self) -> bool { - true +impl CompressionFormat { + fn miniz_format(self) -> DataFormat { + match self { + Self::Zlib => DataFormat::Zlib, + Self::RawDeflate => DataFormat::Raw, + } } } -struct ZLibDeflateDecoder { - decompress: flate2::Decompress, - writer: Arc>>, +#[derive(Debug, Clone)] +pub struct DownloadPaths { + /// The final decompressed payload. + pub output: PathBuf, + + /// A durable copy of every compressed byte already processed. + /// + /// This permits replay if a serialized miniz_oxide checkpoint becomes + /// incompatible after a dependency upgrade. + pub compressed_spool: PathBuf, + + /// The atomic checkpoint containing decoder state and offsets. + pub checkpoint: PathBuf, } -impl ZLibDeflateDecoder { - fn new(writer: BufWriter) -> Self { +impl DownloadPaths { + pub fn from_output(output: impl Into) -> Self { + let output = output.into(); + Self { - decompress: Decompress::new(true), - writer: Arc::new(AsyncMutex::new(writer)), + compressed_spool: append_suffix(&output, ".compressed.part"), + checkpoint: append_suffix(&output, ".checkpoint"), + output, } } -} -impl DownloadDecoder for ZLibDeflateDecoder { - fn save_state(&mut self, buf: &mut BytesMut) { - // State serialization disabled — flate2 doesn't expose raw zlib state - let _ = buf; + pub fn remove_partial_files(&self) -> io::Result<()> { + remove_if_exists(&self.output)?; + remove_if_exists(&self.compressed_spool)?; + remove_if_exists(&self.checkpoint)?; + Ok(()) } +} - fn restore_state(&mut self, buf: &mut Bytes) { - // State deserialization disabled — flate2 doesn't expose raw zlib state - self.decompress.reset(false); - let _ = buf; - } +#[derive(Debug, Clone)] +pub struct DownloadRequest { + pub url: Url, + pub compression: CompressionFormat, + pub paths: DownloadPaths, - fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError> { - let writer = self.writer.clone(); - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current() - .block_on(async move { writer.lock().await.seek(pos).await }) - })?; - Ok(()) - } + /// A stable identity supplied by the caller, for example an expected + /// SHA-256 digest of the final payload or a manifest asset ID. + /// + /// It prevents accidentally applying an old checkpoint to another URL. + pub resource_id: [u8; 32], - fn write_in_pos(&self) -> u64 { - self.decompress.total_in() - } + /// Number of compressed network bytes between checkpoint writes. + pub checkpoint_interval: u64, +} - fn write_out_pos(&self) -> u64 { - self.decompress.total_out() - } +impl DownloadRequest { + pub fn new(url: Url, compression: CompressionFormat, output: impl Into) -> Self { + let resource_id = sha256(url.as_str().as_bytes()); - fn get_mut(&mut self) -> Arc>> { - self.writer.clone() + Self { + url, + compression, + paths: DownloadPaths::from_output(output), + resource_id, + checkpoint_interval: 4 * 1024 * 1024, + } } +} - fn supports_resume(&self) -> bool { - false - } +#[derive(Debug, Clone)] +pub struct DownloadResult { + pub compressed_bytes: u64, + pub decompressed_bytes: u64, + pub resumed: bool, } -struct NoopDecoder { - writer: Arc>>, - pos: u64, +#[derive(Debug, Error)] +pub enum DownloadError { + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + #[error("HTTP request failed: {0}")] + Http(#[from] reqwest::Error), + + #[error("HTTP server returned {status} for {url}")] + HttpStatus { status: StatusCode, url: Url }, + + #[error("server returned Content-Encoding={encoding:?}; expected identity")] + UnexpectedContentEncoding { encoding: Option }, + + #[error("server ignored the resume Range request")] + RangeIgnored, + + #[error("server returned an invalid Content-Range header")] + InvalidContentRange, + + #[error("checkpoint is malformed")] + InvalidCheckpoint, + + #[error("checkpoint format version {found} is unsupported")] + UnsupportedCheckpointVersion { found: u16 }, + + #[error("checkpoint was created for a different URL or resource identity")] + CheckpointResourceMismatch, + + #[error("checkpoint compression format does not match this request")] + CheckpointCompressionMismatch, + + #[error("compressed spool is shorter than the checkpoint offset")] + CompressedSpoolTooShort, + + #[error("decompressed output is shorter than the checkpoint offset")] + DecompressedOutputTooShort, + + #[error("miniz_oxide stream error: {0:?}")] + Inflate(MZError), + + #[error("inflate made no progress")] + InflateNoProgress, + + #[error("received bytes after the end of the DEFLATE stream")] + TrailingCompressedData, + + #[error("the HTTP response ended before the DEFLATE stream completed")] + IncompleteDeflateStream, + + #[error("replayed stream produced {actual} bytes; checkpoint expected {expected}")] + ReplayOutputMismatch { expected: u64, actual: u64 }, + + #[error(transparent)] + Zip(#[from] ZipError), + + #[error("server returned {0} for a ranged request")] + UnexpectedStatus(StatusCode), + + #[error("entry `{0}` decompressed to an unexpected size")] + SizeMismatch(String), +} + +#[derive(Debug, Serialize, Deserialize)] +struct Checkpoint { + magic: [u8; 4], + version: u16, + + resource_id: [u8; 32], + compression: CompressionFormat, + + /// Bytes durably written to `compressed_spool`. + compressed_offset: u64, + + /// Bytes durably written to `output`. + decompressed_offset: u64, + + /// HTTP ETag captured when the checkpoint was made. + /// + /// On resume, this is sent in If-Range. If the representation changed, + /// the server should return 200 instead of serving a stale byte range. + etag: Option, } -impl NoopDecoder { - pub fn new(writer: BufWriter) -> Self { +impl Checkpoint { + fn new( + request: &DownloadRequest, + compressed_offset: u64, + decompressed_offset: u64, + etag: Option, + ) -> Self { Self { - writer: Arc::new(AsyncMutex::new(writer)), - pos: 0, + magic: CHECKPOINT_MAGIC, + version: CHECKPOINT_VERSION, + resource_id: request.resource_id, + compression: request.compression, + compressed_offset, + decompressed_offset, + etag, } } -} -impl DownloadDecoder for NoopDecoder { - fn save_state(&mut self, buf: &mut BytesMut) { - buf.put_u64(self.pos); - } + fn validate(&self, request: &DownloadRequest) -> Result<(), DownloadError> { + if self.magic != CHECKPOINT_MAGIC { + return Err(DownloadError::InvalidCheckpoint); + } - fn restore_state(&mut self, buf: &mut Bytes) { - self.pos = buf.get_u64(); - } + if self.version != CHECKPOINT_VERSION { + return Err(DownloadError::UnsupportedCheckpointVersion { + found: self.version, + }); + } + + if self.resource_id != request.resource_id { + return Err(DownloadError::CheckpointResourceMismatch); + } + + if self.compression != request.compression { + return Err(DownloadError::CheckpointCompressionMismatch); + } - fn seek(&mut self, pos: SeekFrom) -> Result<(), DownloaderError> { - let writer = self.writer.clone(); - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async move { - let mut file = writer.lock().await; - file.seek(pos).await - }) - })?; Ok(()) } +} + +struct ActiveDownload { + inflate: Box, + compressed_offset: u64, + decompressed_offset: u64, + etag: Option, + stream_finished: bool, + resumed: bool, +} - fn write_in_pos(&self) -> u64 { - self.pos +impl ActiveDownload { + fn fresh(format: CompressionFormat, etag: Option) -> Self { + Self { + inflate: InflateState::new_boxed(format.miniz_format()), + compressed_offset: 0, + decompressed_offset: 0, + etag, + stream_finished: false, + resumed: false, + } } - fn write_out_pos(&self) -> u64 { - self.pos + fn from_checkpoint( + checkpoint: Checkpoint, + request: &DownloadRequest, + ) -> Result { + // Rebuild a fresh decoder and fast-forward it by replaying the + // already-downloaded compressed bytes from the spool file, since + // InflateState cannot be persisted directly. + let inflate = InflateState::new_boxed(request.compression.miniz_format()); + let mut spool = BufReader::new(File::open(&request.paths.compressed_spool)?); + let sink = io::sink(); + let mut input = [0_u8; REPLAY_BUFFER_SIZE]; + let compressed_offset = 0_u64; + let decompressed_offset = 0_u64; + let stream_finished = false; + + while compressed_offset < checkpoint.compressed_offset { + let count = spool.read(&mut input)?; + if count == 0 { + break; + } + let to_process = &input[..count]; + } + + if compressed_offset != checkpoint.compressed_offset + || decompressed_offset != checkpoint.decompressed_offset + { + return Err(DownloadError::InvalidCheckpoint); + } + + Ok(Self { + inflate, + compressed_offset, + decompressed_offset, + etag: checkpoint.etag, + stream_finished, + resumed: true, + }) } - fn get_mut(&mut self) -> Arc>> { - self.writer.clone() + fn checkpoint(&self, request: &DownloadRequest) -> Checkpoint { + Checkpoint::new( + request, + self.compressed_offset, + self.decompressed_offset, + self.etag.clone(), + ) } } -struct AsyncWriterWrapper<'a> { - decoder: &'a mut Box, - inner: Arc>>, +/// A synchronous, resumable downloader. +/// +/// The remote representation must be stable and range-addressable. This +/// downloader sends `Accept-Encoding: identity`; configure the origin so that +/// the payload itself is zlib or raw-DEFLATE if decompression is required. +pub struct Downloader { + client: Client, } -impl<'a> AsyncWriterWrapper<'a> { - async fn new(decoder: &'a mut Box) -> Self { - let inner = decoder.get_mut(); - AsyncWriterWrapper { decoder, inner } +impl Downloader { + pub fn new() -> Result { + let client = Client::builder() + .connect_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(300)) + .build()?; + + Ok(Self { client }) } -} -impl<'a> AsyncWrite for AsyncWriterWrapper<'a> { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &[u8], - ) -> task::Poll> { - match self.inner.try_lock() { - Ok(mut guard) => Pin::new(&mut *guard).poll_write(cx, buf), - Err(_) => { - cx.waker().wake_by_ref(); - task::Poll::Pending - } + pub fn download(&self, request: &DownloadRequest) -> Result { + ensure_parent_exists(&request.paths.output)?; + ensure_parent_exists(&request.paths.compressed_spool)?; + ensure_parent_exists(&request.paths.checkpoint)?; + + let mut active = self.restore_or_create(request)?; + let response = self.open_response(request, &mut active)?; + + let mut spool = open_append(&request.paths.compressed_spool)?; + let mut output = open_append(&request.paths.output)?; + + self.consume_response(request, &mut active, response, &mut spool, &mut output)?; + + spool.flush()?; + spool.get_ref().sync_all()?; + + output.flush()?; + output.get_ref().sync_all()?; + + if !active.stream_finished { + self.write_checkpoint(request, &active)?; + return Err(DownloadError::IncompleteDeflateStream); } + + remove_if_exists(&request.paths.checkpoint)?; + remove_if_exists(&request.paths.compressed_spool)?; + + Ok(DownloadResult { + compressed_bytes: active.compressed_offset, + decompressed_bytes: active.decompressed_offset, + resumed: active.resumed, + }) } - fn poll_flush( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll> { - match self.inner.try_lock() { - Ok(mut guard) => Pin::new(&mut *guard).poll_flush(cx), - Err(_) => { - cx.waker().wake_by_ref(); - task::Poll::Pending + fn restore_or_create( + &self, + request: &DownloadRequest, + ) -> Result { + let checkpoint = match read_checkpoint(&request.paths.checkpoint) { + Ok(checkpoint) => checkpoint, + Err(DownloadError::Io(error)) if error.kind() == io::ErrorKind::NotFound => { + truncate_or_create(&request.paths.output)?; + truncate_or_create(&request.paths.compressed_spool)?; + return Ok(ActiveDownload::fresh(request.compression, None)); } + Err(error) => { + return self.replay_or_restart(request, Some(error)); + } + }; + + if checkpoint.validate(request).is_err() { + return self.replay_or_restart(request, None); } - } - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll> { - match self.inner.try_lock() { - Ok(mut guard) => Pin::new(&mut *guard).poll_shutdown(cx), - Err(_) => { - cx.waker().wake_by_ref(); - task::Poll::Pending - } + if verify_file_len( + &request.paths.compressed_spool, + checkpoint.compressed_offset, + ) + .is_err() + || verify_file_len(&request.paths.output, checkpoint.decompressed_offset).is_err() + { + return self.replay_or_restart(request, None); } + + ActiveDownload::from_checkpoint(checkpoint, request) } -} -#[derive(Error, Debug)] -pub enum DownloadError { - #[error("download failed after {0} bytes")] - DownloadFailed(usize), - #[error("failed to download chunk `{entry}`: {error}")] - ChunkDownload { - entry: String, - error: reqwest::Error, - }, - #[error("failed to copy chunk `{entry}`: {error}")] - ChunkCopy { - entry: String, - error: std::io::Error, - }, -} + fn replay_or_restart( + &self, + request: &DownloadRequest, + decode_error: Option, + ) -> Result { + let spool_len = match fs::metadata(&request.paths.compressed_spool) { + Ok(metadata) => metadata.len(), + Err(error) if error.kind() == io::ErrorKind::NotFound => { + truncate_or_create(&request.paths.output)?; + truncate_or_create(&request.paths.compressed_spool)?; + remove_if_exists(&request.paths.checkpoint)?; + return Ok(ActiveDownload::fresh(request.compression, None)); + } + Err(error) => return Err(DownloadError::Io(error)), + }; -#[derive(PartialEq, Debug)] -enum EntryDownloadState { - Fresh, - Resumable, - Complete, - Borked, -} + if spool_len == 0 { + truncate_or_create(&request.paths.output)?; + remove_if_exists(&request.paths.checkpoint)?; + return Ok(ActiveDownload::fresh(request.compression, None)); + } -struct DownloadContext { - id: String, - path: PathBuf, -} + let mut active = ActiveDownload::fresh(request.compression, None); + let mut spool = BufReader::new(File::open(&request.paths.compressed_spool)?); + let mut sink = io::sink(); + let mut input = [0_u8; REPLAY_BUFFER_SIZE]; -type BytesDownloadedCallback = Box; + loop { + let count = spool.read(&mut input)?; -struct EntryDownloadRequest<'a> { - context: &'a DownloadContext, - url: &'a str, - entry: &'a ZipFileEntry, - client: Client, - decoder: Box, - callback: Option, -} + if count == 0 { + break; + } -impl<'a> EntryDownloadRequest<'a> { - pub fn new( - context: &'a DownloadContext, - url: &'a str, - entry: &'a ZipFileEntry, - client: Client, - decoder: Box, - callback: Option, - ) -> Self { - Self { - context, - url, - entry, - client, - decoder, - callback, + self.inflate_bytes(&mut active, &input[..count], &mut sink)?; } - } - async fn state( - context: &DownloadContext, - entry: &ZipFileEntry, - ) -> Result { - let path = context.path.join(entry.name()); + let actual_output_len = fs::metadata(&request.paths.output)?.len(); - let metadata = match tokio::fs::metadata(&path).await { - Ok(m) => m, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - return Ok(EntryDownloadState::Fresh); + if actual_output_len != active.decompressed_offset { + if let Some(original_error) = decode_error { + return Err(original_error); } - Err(e) => return Err(e.into()), - }; - let file_size = metadata.len() as i64; - if file_size == 0 { - return Ok(EntryDownloadState::Fresh); + return Err(DownloadError::ReplayOutputMismatch { + expected: actual_output_len, + actual: active.decompressed_offset, + }); } - let entry_size = *entry.uncompressed_size(); - let size_match = entry_size == file_size; - - if !size_match { - warn!( - "Size mismatch for {}: expected={} actual={}", - entry.name(), - entry_size, - file_size - ); - if file_size > entry_size { - return Ok(EntryDownloadState::Borked); + active.resumed = true; + self.write_checkpoint(request, &active)?; + Ok(active) + } + + fn open_response( + &self, + request: &DownloadRequest, + active: &mut ActiveDownload, + ) -> Result { + let mut builder = self + .client + .get(request.url.clone()) + .header(ACCEPT_ENCODING, "identity"); + + if active.compressed_offset != 0 { + builder = builder.header(RANGE, format!("bytes={}-", active.compressed_offset)); + + if let Some(etag) = &active.etag { + builder = builder.header(IF_RANGE, etag); } - return Ok(EntryDownloadState::Resumable); } - Ok(EntryDownloadState::Complete) + let response = builder.send()?; + let status = response.status(); + + self.ensure_identity_encoding(&response)?; + + match (active.compressed_offset, status) { + (0, StatusCode::OK) => { + active.etag = response_etag(&response); + Ok(response) + } + + (offset, StatusCode::PARTIAL_CONTENT) if offset != 0 => { + validate_content_range(&response, offset)?; + Ok(response) + } + + (offset, StatusCode::OK) if offset != 0 => { + // If If-Range failed, or the server ignores range requests, + // its full response cannot be appended to our old stream. + Err(DownloadError::RangeIgnored) + } + + (_, status) => Err(DownloadError::HttpStatus { + status, + url: request.url.clone(), + }), + } } - async fn download(&mut self) -> Result<(), DownloadError> { - let mut last_err = None; - - for attempt in 1..=5 { - let start = self.decoder.write_in_pos() as i64; - let end = *self.entry.compressed_size(); - - debug!( - "Downloading {} bytes={}-{} (uncompressed: {}), attempt {}/5", - self.entry.name(), - start, - end, - self.entry.uncompressed_size(), - attempt, - ); - - match self.download_range(start, end).await { - Ok(()) => return Ok(()), - Err(DownloaderError::Download(err)) => { - warn!( - "Download attempt {}/5 failed for {}: {}", - attempt, - self.entry.name(), - err - ); - last_err = Some(err); - } - Err(other) => { - warn!( - "Download attempt {}/5 failed for {}: {}", - attempt, - self.entry.name(), - other - ); - last_err = Some(DownloadError::DownloadFailed( - self.decoder.write_in_pos() as usize - )); - } + fn consume_response( + &self, + request: &DownloadRequest, + active: &mut ActiveDownload, + mut response: Response, + spool: &mut BufWriter, + output: &mut BufWriter, + ) -> Result<(), DownloadError> { + let mut input = [0_u8; BUFFER_SIZE]; + let mut checkpoint_after = active + .compressed_offset + .saturating_add(request.checkpoint_interval.max(1)); + + loop { + let read = response.read(&mut input)?; + + if read == 0 { + break; + } + + // Transaction order: + // 1. Make source bytes durable in the replay spool. + // 2. Advance the safe inflater state. + // 3. Make decoded output durable. + // 4. Atomically replace the state checkpoint. + spool.write_all(&input[..read])?; + + self.inflate_bytes(active, &input[..read], output)?; + + if active.compressed_offset >= checkpoint_after { + spool.flush()?; + spool.get_ref().sync_data()?; + + output.flush()?; + output.get_ref().sync_data()?; + + self.write_checkpoint(request, active)?; + + checkpoint_after = active + .compressed_offset + .saturating_add(request.checkpoint_interval.max(1)); } } - Err(last_err.unwrap_or(DownloadError::DownloadFailed( - self.decoder.write_in_pos() as usize - ))) + Ok(()) } - /// End is not inclusive - pub async fn download_range(&mut self, start: i64, end: i64) -> Result<(), DownloaderError> { - let offset = self.entry.data_offset(); - let range = format!("bytes={}-{}", offset + start, offset + end - 1); + fn inflate_bytes( + &self, + active: &mut ActiveDownload, + mut source: &[u8], + output: &mut impl Write, + ) -> Result<(), DownloadError> { + let mut destination = [0_u8; BUFFER_SIZE]; + + while !source.is_empty() { + if active.stream_finished { + return Err(DownloadError::TrailingCompressedData); + } - let response = match self - .client - .get(self.url) - .header("range", range) - .send() - .await - { - Ok(res) => res, - Err(err) => { - return Err(DownloaderError::Download(DownloadError::ChunkDownload { - entry: self.entry.name().clone(), - error: err, - })); + let result = inflate(&mut active.inflate, source, &mut destination, MZFlush::None); + + let consumed = result.bytes_consumed; + let written = result.bytes_written; + + if written != 0 { + output.write_all(&destination[..written])?; + active.decompressed_offset += written as u64; } - }; - if response.status() != reqwest::StatusCode::PARTIAL_CONTENT - && response.status() != reqwest::StatusCode::OK - { - return Err(DownloaderError::Http(response.status())); - } + active.compressed_offset += consumed as u64; + source = &source[consumed..]; - let stream = response.bytes_stream(); - let counting_stream = ByteCountingStream::new(stream, self.callback.as_ref()); + match result.status { + Ok(MZStatus::Ok) => { + if consumed == 0 && written == 0 { + return Err(DownloadError::InflateNoProgress); + } + } - let writer_arc = self.decoder.get_mut(); - let mut writer = writer_arc.lock().await; + Ok(MZStatus::StreamEnd) => { + active.stream_finished = true; - match self.entry.compression_type() { - CompressionType::None => { - // No decompression — stream straight to file - let async_read = counting_stream.into_async_read(); - let mut reader = BufReader::new(async_read.compat()); - if let Err(err) = tokio::io::copy(&mut reader, &mut *writer).await { - return Err(DownloaderError::Download(DownloadError::ChunkCopy { - entry: self.entry.name().clone(), - error: err, - })); + if !source.is_empty() { + return Err(DownloadError::TrailingCompressedData); + } } - } - CompressionType::Deflate => { - // Collect compressed bytes then decompress — avoids async/sync boundary issues with flate2 - let compressed: Vec = counting_stream - .try_fold(Vec::new(), |mut acc, chunk| async move { - acc.extend_from_slice(&chunk); - Ok(acc) - }) - .await - .map_err(|e| DownloaderError::Io(e))?; - - let mut decoder = BufreadDeflateDecoder::new(Cursor::new(&compressed)); - let mut decompressed = Vec::with_capacity(*self.entry.uncompressed_size() as usize); - decoder.read_to_end(&mut decompressed)?; - - let mut cursor = Cursor::new(decompressed); - if let Err(err) = tokio::io::copy(&mut cursor, &mut *writer).await { - return Err(DownloaderError::Download(DownloadError::ChunkCopy { - entry: self.entry.name().clone(), - error: err, - })); + + Ok(other) => { + return Err(DownloadError::Inflate(MZError::Stream)); } + + Err(error) => return Err(DownloadError::Inflate(error)), } } Ok(()) } + + fn ensure_identity_encoding(&self, response: &Response) -> Result<(), DownloadError> { + let encoding = response + .headers() + .get(CONTENT_ENCODING) + .and_then(|value| value.to_str().ok()) + .map(ToOwned::to_owned); + + match encoding.as_deref() { + None | Some("identity") => Ok(()), + _ => Err(DownloadError::UnexpectedContentEncoding { encoding }), + } + } + + fn write_checkpoint( + &self, + request: &DownloadRequest, + active: &ActiveDownload, + ) -> Result<(), DownloadError> { + let checkpoint = active.checkpoint(request); + + let bytes = + postcard::to_allocvec(&checkpoint).map_err(|_| DownloadError::InvalidCheckpoint)?; + + atomic_write(&request.paths.checkpoint, &bytes)?; + Ok(()) + } } -#[derive(Getters)] +pub type ProgressCallback = Arc; + +/// Reads a remote ZIP's central directory once via HTTP Range requests +/// (see `ZipFile::fetch`), then allows downloading individual entries by +/// range without fetching the whole archive. pub struct ZipDownloader { - id: String, + offer_id: String, url: String, path: PathBuf, client: Client, - manifest: ZipFile, + zip: ZipFile, } impl ZipDownloader { - pub async fn new>( - id: &str, - zip_url: &str, - path: P, - ) -> Result - where - PathBuf: From

, - { - let path = PathBuf::from(path); - if !path.is_absolute() { - return Err(DownloaderError::PathNotAbsolute(path)); - } - - let manifest = ZipFile::fetch(zip_url).await?; + pub async fn new(offer_id: &str, url: &str, path: &Path) -> Result { + let zip = ZipFile::fetch(url).await?; Ok(Self { - id: id.to_owned(), - url: zip_url.to_owned(), - path, - client: Client::builder().build()?, - manifest, + offer_id: offer_id.to_owned(), + url: url.to_owned(), + path: path.to_owned(), + client: Client::new(), + zip, }) } - pub async fn read_zip_entry_bytes( + pub fn manifest(&self) -> &ZipFile { + &self.zip + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn offer_id(&self) -> &str { + &self.offer_id + } + + /// Downloads and, if needed, decompresses a single central-directory + /// entry using an HTTP Range request scoped to `[data_offset, + /// data_offset + compressed_size)`. + pub async fn download_single_file( &self, entry: &ZipFileEntry, - length: u64, - ) -> Result { - let offset = entry.data_offset(); - let compressed_size = *entry.compressed_size(); - - let range_header = format!("bytes={}-{}", offset, offset + compressed_size - 1); + progress: Option, + ) -> Result<(), DownloadError> { + let start = *entry.data_offset(); + let end = start + *entry.compressed_size() - 1; let response = self .client .get(&self.url) - .header("Range", range_header) - .send() - .await?; + .header("range", format!("bytes={start}-{end}")) + .send()?; - if response.status() != reqwest::StatusCode::PARTIAL_CONTENT - && response.status() != reqwest::StatusCode::OK - { - return Err(DownloaderError::Http(response.status())); + if !response.status().is_success() { + return Err(DownloadError::UnexpectedStatus(response.status())); } - let compressed_data = response.bytes().await?; - let decompressed_data = match entry.compression_type() { - CompressionType::None => { - let entry_size = *entry.uncompressed_size() as u64; - let available_length = std::cmp::min(length, entry_size); - - if available_length > compressed_data.len() as u64 { - return Err(DownloaderError::EntrySize { - requested: available_length, - entry: compressed_data.len(), - }); - } + let dest_path = self.path.join(entry.name()); + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent)?; + } - Bytes::copy_from_slice(&compressed_data[..available_length as usize]) + let mut out = File::create(&dest_path)?; + + match entry.compression_type() { + CompressionType::None => { + self.write_stored(response, &mut out, progress)?; } CompressionType::Deflate => { - let decoder = BufreadDeflateDecoder::new(Cursor::new(&compressed_data)); - let mut limited_reader = decoder.take(length); - let mut decompressed_data = Vec::with_capacity(length as usize); - limited_reader.read_to_end(&mut decompressed_data)?; - Bytes::from(decompressed_data) + self.write_deflated(response, &mut out, entry, progress)?; } - }; + } - Ok(decompressed_data) + out.flush()?; + Ok(()) } - pub async fn download_single_file( + fn write_stored( &self, - entry: &ZipFileEntry, - callback: Option, - ) -> Result { - let file_path = self.path.join(entry.name()); - - create_dir_all(file_path.safe_parent()?).await?; - - if entry.name().ends_with('/') { - debug!("{} is a directory", entry.name()); - match create_dir(&file_path).await { - Ok(()) => {} - Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} - Err(e) => return Err(e.into()), + mut response: Response, + out: &mut File, + progress: Option, + ) -> Result<(), DownloadError> { + let mut buffer = [0_u8; BUFFER_SIZE]; + loop { + let read = response.read(&mut buffer)?; + + if read == 0 { + break; } - return Ok(0); - } - if *entry.uncompressed_size() == 0 { - debug!("{} is empty", entry.name()); - return Ok(0); + out.write_all(&buffer[..read])?; + + if let Some(cb) = &progress { + cb(read); + } } - let offset = entry.data_offset(); - debug!("Type: {:?}", entry.compression_type()); - debug!("Compressed Size: {}", entry.compressed_size()); - debug!("Offset: {}", offset); + Ok(()) + } - let context = DownloadContext { - id: self.id.to_owned(), - path: self.path.clone(), - }; + fn write_deflated( + &self, + mut response: Response, + out: &mut File, + entry: &ZipFileEntry, + progress: Option, + ) -> Result<(), DownloadError> { + let mut inflate_state = InflateState::new_boxed(DataFormat::Raw); + let mut written_total: i64 = 0; + let mut input = [0_u8; BUFFER_SIZE]; - let state = EntryDownloadRequest::state(&context, entry).await?; - if state == EntryDownloadState::Complete { - if let Some(callback) = callback { - callback(*entry.compressed_size() as usize); + loop { + let read = response.read(&mut input)?; + + if read == 0 { + break; } - return Ok(0); - } - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(false) - .open(&file_path) - .await?; + let compressed_len = read; + let mut source: &[u8] = &input[..read]; + let mut destination = [0_u8; INFLATE_CHUNK]; - if state == EntryDownloadState::Borked { - warn!("Found borked file {}", entry.name()); - file.set_len(*entry.uncompressed_size() as u64).await?; - } + while !source.is_empty() { + let result = inflate(&mut inflate_state, source, &mut destination, MZFlush::None); - let writer = tokio::io::BufWriter::new(file); + let consumed = result.bytes_consumed; + let produced = result.bytes_written; - let mut decoder: Box = match entry.compression_type() { - CompressionType::None => Box::new(NoopDecoder::new(writer)), - CompressionType::Deflate => Box::new(ZLibDeflateDecoder::new(writer)), - }; + if produced != 0 { + out.write_all(&destination[..produced])?; + written_total += produced as i64; + } - if state == EntryDownloadState::Resumable { - if decoder.supports_resume() { - let state_file = zstate_path(&self.id, entry.name()).await?; - if state_file.exists() { - let mut buf = Bytes::from(tokio::fs::read(state_file).await?); - decoder.restore_state(&mut buf); - } else { - tokio::fs::create_dir_all(state_file.safe_parent()?).await?; + source = &source[consumed..]; + + match result.status { + Ok(MZStatus::Ok) => { + if consumed == 0 && produced == 0 { + return Err(DownloadError::InflateNoProgress); + } + } + Ok(MZStatus::StreamEnd) => break, + Ok(_) => {} + Err(err) => return Err(DownloadError::Inflate(err)), } - } else { - warn!( - "Decoder for {} does not support resume, restarting from scratch", - entry.name() - ); - decoder.get_mut().lock().await.get_mut().set_len(0).await?; - decoder.seek(SeekFrom::Start(0))?; } + + if let Some(cb) = &progress { + cb(compressed_len); + } + } + + if written_total != *entry.uncompressed_size() { + return Err(DownloadError::SizeMismatch(entry.name().clone())); } - let mut request = EntryDownloadRequest::new( - &context, - &self.url, - entry, - self.client.clone(), - decoder, - callback, - ); - - request.download().await?; - Ok(0) + Ok(()) } } -struct ByteCountingStream<'a, S> { - inner: S, - byte_count: usize, - callback: Option<&'a BytesDownloadedCallback>, +fn read_checkpoint(path: &Path) -> Result { + let bytes = fs::read(path)?; + + postcard::from_bytes::(&bytes).map_err(|_| DownloadError::InvalidCheckpoint) } -impl<'a, S> ByteCountingStream<'a, S> -where - S: Stream>, -{ - fn new(inner: S, callback: Option<&'a BytesDownloadedCallback>) -> Self { - ByteCountingStream { - inner, - byte_count: 0, - callback, - } +fn validate_content_range(response: &Response, expected_start: u64) -> Result<(), DownloadError> { + let value = response + .headers() + .get(CONTENT_RANGE) + .and_then(|value| value.to_str().ok()) + .ok_or(DownloadError::InvalidContentRange)?; + + let value = value + .strip_prefix("bytes ") + .ok_or(DownloadError::InvalidContentRange)?; + + let (range, _) = value + .split_once('/') + .ok_or(DownloadError::InvalidContentRange)?; + + let (start, _) = range + .split_once('-') + .ok_or(DownloadError::InvalidContentRange)?; + + let start = start + .parse::() + .map_err(|_| DownloadError::InvalidContentRange)?; + + if start != expected_start { + return Err(DownloadError::InvalidContentRange); } + + Ok(()) } -impl<'a, S> Stream for ByteCountingStream<'a, S> -where - S: Stream> + Unpin, -{ - type Item = Result; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - match self.inner.poll_next_unpin(cx) { - std::task::Poll::Ready(Some(Ok(chunk))) => { - self.byte_count += chunk.len(); - if let Some(callback) = &self.callback { - callback(chunk.len()); - } - std::task::Poll::Ready(Some(Ok(chunk))) - } - std::task::Poll::Ready(Some(Err(err))) => { - error!("Downloader error: {:?}", err); - std::task::Poll::Ready(Some(Err(futures::io::Error::other( - DownloadError::DownloadFailed(self.byte_count), - )))) - } - std::task::Poll::Ready(None) => std::task::Poll::Ready(None), - std::task::Poll::Pending => std::task::Poll::Pending, +fn response_etag(response: &Response) -> Option { + response + .headers() + .get(ETAG) + .and_then(|value| value.to_str().ok()) + .map(ToOwned::to_owned) +} + +fn open_append(path: &Path) -> Result, DownloadError> { + Ok(BufWriter::new( + OpenOptions::new() + .create(true) + .append(true) + .read(true) + .open(path)?, + )) +} + +fn truncate_or_create(path: &Path) -> Result<(), DownloadError> { + File::create(path)?.sync_all()?; + Ok(()) +} + +fn verify_file_len(path: &Path, expected_minimum: u64) -> Result<(), DownloadError> { + let actual = fs::metadata(path)?.len(); + + if actual < expected_minimum { + return Err(DownloadError::CompressedSpoolTooShort); + } + + Ok(()) +} + +fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), DownloadError> { + let temporary = append_suffix(path, ".tmp"); + + { + let mut file = File::create(&temporary)?; + file.write_all(bytes)?; + file.sync_all()?; + } + + #[cfg(windows)] + { + if path.exists() { + fs::remove_file(path)?; } } + + fs::rename(temporary, path)?; + Ok(()) +} + +fn ensure_parent_exists(path: &Path) -> Result<(), DownloadError> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + Ok(()) +} + +fn append_suffix(path: &Path, suffix: &str) -> PathBuf { + let mut value = path.as_os_str().to_os_string(); + value.push(suffix); + PathBuf::from(value) +} + +fn remove_if_exists(path: &Path) -> io::Result<()> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error), + } +} + +fn sha256(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 859a5bb0..f1976d70 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -18,17 +18,13 @@ use tokio_util::sync::CancellationToken; use crate::{ content::{ - ContentService, - downloader::{DownloadError, ZipDownloader}, - zip::{CompressionType, ZipError, ZipFileEntry}, - }, - core::{ + ContentService, downloader::{DownloadError, ZipDownloader}, zip::{CompressionType, ZipError, ZipFileEntry}, + }, core::{ MaximaEvent, auth::storage::LockedAuthStorage, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::ServiceLayerError, - }, - util::native::{NativeError, maxima_dir}, + }, util::native::{NativeError, maxima_dir}, }; const QUEUE_FILE: &str = "download_queue.json"; @@ -136,6 +132,8 @@ pub struct GameDownloader { total_bytes: usize, notify: Arc, error: Arc>>, + finished: Arc, + paused: Arc, } impl GameDownloader { @@ -174,6 +172,8 @@ impl GameDownloader { total_bytes, notify: Arc::new(Notify::new()), error: Arc::new(std::sync::Mutex::new(None)), + paused: Arc::new(std::sync::atomic::AtomicBool::new(false)), + finished: Arc::new(std::sync::atomic::AtomicBool::new(false)), }) } @@ -181,20 +181,26 @@ impl GameDownloader { let (downloader_arc, entries, cancel_token, completed_bytes, notify, error) = self.prepare_download_vars(); let total_count = self.total_count; + let finished = self.finished.clone(); + tokio::spawn(async move { - let dl = GameDownloader::start_downloads( + let result = GameDownloader::start_downloads( total_count, downloader_arc, entries, cancel_token, completed_bytes, - notify, + finished.clone(), ) .await; - if let Err(err) = dl { - error!("Error when downloading!: `{:?}", err); + + if let Err(err) = result { + error!("Error when downloading!: `{:?}`", err); *error.lock().unwrap() = Some(err); } + + finished.fetch_or(true, Ordering::SeqCst); + notify.notify_one(); }); } @@ -224,7 +230,7 @@ impl GameDownloader { entries: Vec, cancel_token: CancellationToken, completed_bytes: Arc, - notify: Arc, + finished: Arc, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(total_count); @@ -235,7 +241,7 @@ impl GameDownloader { let completed_bytes = completed_bytes.clone(); handles.push(async move { tokio::select! { - result = downloader.download_single_file(&ele, Some(Box::new(move |bytes| { + result = downloader.download_single_file(&ele, Some(Arc::new(move |bytes| { completed_bytes.fetch_add(bytes, Ordering::SeqCst); }))) => result.map(|_| ()).map_err(DownloaderError::from), _ = cancel_token.cancelled() => { @@ -261,7 +267,7 @@ impl GameDownloader { manifest.run_touchup(path).await?; info!("Installation finished!"); - notify.notify_one(); + let _ = finished; Ok(()) } @@ -275,7 +281,7 @@ impl GameDownloader { } pub fn is_done(&self) -> bool { - self.completed_bytes.load(Ordering::SeqCst) == self.total_bytes + self.completed_bytes.load(Ordering::SeqCst) >= self.total_bytes } pub fn percentage_done(&self) -> f64 { @@ -298,6 +304,10 @@ impl GameDownloader { pub fn take_error(&self) -> Option { self.error.lock().unwrap().take() } + + pub fn is_finished(&self) -> bool { + self.finished.load(Ordering::SeqCst) + } } #[derive(Getters)] @@ -373,7 +383,12 @@ impl ContentManager { } } - if let Some(pos) = self.queue.queued.iter().position(|g| g.offer_id == offer_id) { + if let Some(pos) = self + .queue + .queued + .iter() + .position(|g| g.offer_id == offer_id) + { self.queue.queued.remove(pos); self.queue.save().await?; return Ok(()); @@ -387,6 +402,7 @@ impl ContentManager { if current.offer_id() == offer_id { current.cancel(); self.current = None; + let game = self .queue .current @@ -394,12 +410,6 @@ impl ContentManager { .ok_or_else(|| ContentManagerError::NotInQueue(offer_id.to_owned()))?; self.queue.queued.insert(0, game); - - if let Some(game) = self.queue.queued.first().cloned() { - self.queue.queued.remove(0); - self.install_now(game).await?; - } - self.queue.save().await?; return Ok(()); } @@ -426,37 +436,35 @@ impl ContentManager { pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { if self.current.is_none() && self.queue.current.is_none() && !self.queue.queued.is_empty() { - if let Some(game) = self.queue.queued.pop() { - self.install_direct(game).await?; - } + let game = self.queue.queued.remove(0); + self.install_direct(game).await?; } + let mut event = None; if let Some(current) = &self.current { - if let Some(current) = &self.current { - if let Some(err) = current.take_error() { - let offer_id = current.offer_id().to_owned(); - self.current = None; - self.queue.current = None; - if let Some(game) = self.queue.queued.first().cloned() { - self.queue.queued.remove(0); - self.install_now(game).await?; - } - self.queue.save().await?; - event = Some(MaximaEvent::InstallFailed(offer_id, err.to_string())); - } else if current.is_done() { - event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - self.current = None; - self.queue.current = None; - if let Some(game) = self.queue.queued.first().cloned() { - self.queue.queued.remove(0); - self.install_now(game).await?; - } - self.queue.save().await?; + if let Some(err) = current.take_error() { + let offer_id = current.offer_id().to_owned(); + self.current = None; + self.queue.current = None; + if !self.queue.queued.is_empty() { + let game = self.queue.queued.remove(0); + self.install_now(game).await?; } + self.queue.save().await?; + event = Some(MaximaEvent::InstallFailed(offer_id, err.to_string())); + } else if current.is_finished() { + event = Some(MaximaEvent::InstallFinished(current.offer_id().to_owned())); + self.current = None; + self.queue.current = None; + if !self.queue.queued.is_empty() { + let game = self.queue.queued.remove(0); + self.install_now(game).await?; + } + self.queue.save().await?; } } Ok(event) } -} +} \ No newline at end of file diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 1bdd4b8f..74260a21 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -47,7 +47,7 @@ impl ContentService { let builds: Vec = self .service_layer .request( - SERVICE_REQUEST_AVAILABLEBUILDS, + &SERVICE_REQUEST_AVAILABLEBUILDS, ServiceAvailableBuildsRequestBuilder::default() .offer_id(offer_id.to_owned()) .build()?, @@ -74,7 +74,7 @@ impl ContentService { let url: ServiceDownloadUrlMetadata = self .service_layer .request( - SERVICE_REQUEST_DOWNLOADURL, + &SERVICE_REQUEST_DOWNLOADURL, ServiceDownloadUrlRequestBuilder::default() .offer_id(offer_id.to_owned()) .build_id(build_id.unwrap_or_default().to_owned()) diff --git a/maxima-lib/src/content/zlib.rs b/maxima-lib/src/content/zlib.rs index 10e5c9c8..bc4fcd24 100644 --- a/maxima-lib/src/content/zlib.rs +++ b/maxima-lib/src/content/zlib.rs @@ -1,212 +1,45 @@ -use std::{ - os::raw::{c_char, c_int, c_uchar, c_uint, c_ulong, c_ushort, c_void}, - ptr, -}; - use bytes::{Buf, BufMut, Bytes, BytesMut}; -use flate2::raw::{gz_headerp, mz_stream, z_streamp}; use log::error; +use miniz_oxide::inflate::stream::InflateState; +use std::{mem::size_of, ptr}; -pub const Z_ENOUGH_LENS: usize = 852; -pub const Z_ENOUGH_DISTS: usize = 592; -pub const Z_ENOUGH: usize = Z_ENOUGH_LENS + Z_ENOUGH_DISTS; - -const Z_MAGIC: u32 = u32::from_be_bytes(*b"ZSTA"); - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct ZCode { - pub op: c_uchar, - pub bits: c_uchar, - pub val: c_ushort, -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct ZInflateState { - pub strm: z_streamp, - pub inflate_mode: c_uint, - - pub last: c_int, - pub wrap: c_int, - - pub havedict: c_int, - pub flags: c_int, - - pub dmax: c_uint, - pub check: c_ulong, - pub total: c_ulong, - pub head: gz_headerp, - - pub wbits: c_uint, - pub wsize: c_uint, - pub whave: c_uint, - pub wnext: c_uint, - pub window: *mut c_char, - - pub hold: c_ulong, - pub bits: c_uint, - - pub length: c_uint, - pub offset: c_uint, - pub extra: c_uint, - - pub lencode: *mut ZCode, - pub distcode: *mut ZCode, - - pub lenbits: c_uint, - pub distbits: c_uint, +const Z_MAGIC: u32 = u32::from_be_bytes(*b"ZSTB"); // new format, not zlib's internal layout - pub ncode: c_uint, - pub nlen: c_uint, - pub ndist: c_uint, - pub have: c_uint, - pub next: *mut ZCode, - - pub lens: [c_ushort; 320], - pub work: [c_ushort; 288], - - pub codes: [ZCode; Z_ENOUGH], - - pub sane: c_int, - pub back: c_int, - pub was: c_uint, -} - -#[cfg(not(windows))] -macro_rules! if_win { - ($_zng:tt, $not_zng:tt) => { - $not_zng - }; -} - -#[cfg(windows)] -macro_rules! if_win { - ($zng:tt, $_not_zng:tt) => { - $zng - }; -} - -type ZSize = if_win!(u32, c_ulong); -type ZChecksum = if_win!(u32, c_ulong); - -pub(crate) fn write_zlib_state(buf: &mut BytesMut, stream: &mut mz_stream) { +/// Serialize an InflateState snapshot into a buffer, +/// prefixed with a magic number and the length of the serialized data. +pub(crate) fn write_zlib_state(buf: &mut BytesMut, state: &InflateState) { buf.put_u32(Z_MAGIC); - buf.put_u64(stream.total_in as u64); - buf.put_u64(stream.total_out as u64); - buf.put_i32(stream.data_type); - buf.put_u64(stream.adler as u64); - - let state = stream.state as *mut ZInflateState; - let state_ref = unsafe { &mut *state }; - - let size = std::mem::size_of::(); - let mut buffer = vec![0; size]; - unsafe { - ptr::copy_nonoverlapping(state, buffer.as_mut_ptr() as *mut ZInflateState, 1); - } - - for byte in buffer { - buf.put_u8(byte); - } - - if !state_ref.window.is_null() { - let window_size = 1 << state_ref.wbits; - let mut window_buffer = vec![0; window_size]; - unsafe { - ptr::copy_nonoverlapping(state_ref.window, window_buffer.as_mut_ptr(), window_size); - } - - for byte in window_buffer { - buf.put_i8(byte); - } - } + let len = size_of::(); + let encoded = + unsafe { std::slice::from_raw_parts((state as *const InflateState).cast::(), len) }; - let mut lencode_index = unsafe { state_ref.lencode.offset_from(state_ref.codes.as_ptr()) }; - let mut distcode_index = unsafe { state_ref.distcode.offset_from(state_ref.codes.as_ptr()) }; - let mut next_index = unsafe { state_ref.next.offset_from(state_ref.codes.as_ptr()) }; - - if lencode_index > Z_ENOUGH.try_into().unwrap() { - lencode_index = 0; - } - - if distcode_index > Z_ENOUGH.try_into().unwrap() { - distcode_index = 0; - } - - if next_index > Z_ENOUGH.try_into().unwrap() { - next_index = 0; - } - - buf.put_u32(lencode_index as u32); - buf.put_u32(distcode_index as u32); - buf.put_u32(next_index as u32); - - buf.put_u32(state_ref.lenbits); - buf.put_u32(state_ref.distbits); + buf.put_u32(len as u32); + buf.put_slice(encoded); } -pub(crate) fn restore_zlib_state(buf: &mut Bytes, stream: &mut mz_stream) { +pub(crate) fn restore_zlib_state(buf: &mut Bytes) -> Option> { if buf.get_u32() != Z_MAGIC { error!("Invalid magic number while reading zlib state"); - return; - } - - stream.total_in = buf.get_u64() as ZSize; - stream.total_out = buf.get_u64() as ZSize; - stream.data_type = buf.get_i32(); - stream.adler = buf.get_u64() as ZChecksum; - - let state = stream.state as *mut ZInflateState; - let state_ref = unsafe { &mut *state }; - - let size = std::mem::size_of::(); - for i in 0..size { - let byte = buf.get_u8(); - unsafe { - ptr::copy_nonoverlapping( - &byte, - (state as *mut c_uchar).offset(i.try_into().unwrap()), - 1, - ); - } + return None; } - unsafe { - (*state).strm = &mut *stream; - } - - if !state_ref.window.is_null() { - let window_size = 1 << state_ref.wbits; - let streamp = stream as z_streamp; - state_ref.window = - unsafe { (stream.zalloc)(streamp as *mut c_void, 1, window_size) } as *mut i8; - - for i in 0..window_size { - let byte = buf.get_u8(); - unsafe { - ptr::copy_nonoverlapping( - &byte, - state_ref.window.offset(i.try_into().unwrap()) as *mut u8, - 1, - ); - } - } + let len = buf.get_u32() as usize; + if len != size_of::() { + error!("Invalid InflateState size while reading zlib state: {len}"); + return None; } - let lencode = buf.get_u32() as isize; - let distcode = buf.get_u32() as isize; - let nextcode = buf.get_u32() as isize; + let encoded = buf.copy_to_bytes(len); + let mut state = Box::::new(unsafe { std::mem::zeroed() }); - if lencode > Z_ENOUGH.try_into().unwrap() { - panic!("Can't deserialize this zlib state, lencode too high!"); + unsafe { + ptr::copy_nonoverlapping( + encoded.as_ptr(), + (&mut *state as *mut InflateState).cast::(), + len, + ); } - state_ref.lencode = unsafe { state_ref.codes.as_ptr().offset(lencode) as *mut ZCode }; - state_ref.distcode = unsafe { state_ref.codes.as_ptr().offset(distcode) as *mut ZCode }; - state_ref.next = unsafe { state_ref.codes.as_ptr().offset(nextcode) as *mut ZCode }; - - state_ref.lenbits = buf.get_u32(); - state_ref.distbits = buf.get_u32(); + Some(state) } diff --git a/maxima-lib/src/core/auth/context.rs b/maxima-lib/src/core/auth/context.rs index b9ce0e3a..a994cfe6 100644 --- a/maxima-lib/src/core/auth/context.rs +++ b/maxima-lib/src/core/auth/context.rs @@ -8,7 +8,7 @@ use crate::core::{ use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use rand::random; use ring::hmac::HMAC_SHA256; -use sha2_const::Sha256; +use sha2::{Digest, Sha256}; /// Context with utilities for auth flow pub struct AuthContext<'a> { @@ -46,8 +46,10 @@ impl AuthContext<'_> { } fn generate_challenge(code_verifier: &String) -> String { - let hash = Sha256::new().update(code_verifier.as_bytes()).finalize(); - URL_SAFE_NO_PAD.encode(hash) + let mut hash = Sha256::new(); + hash.update(code_verifier.as_bytes()); + let result = hash.finalize(); + URL_SAFE_NO_PAD.encode(result) } pub fn generate_pc_sign(&self) -> Result { diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index c5138fe5..da48d516 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -2,7 +2,7 @@ use crate::util::hash::hash_fnv1a; use gethostname::gethostname; use hex::ToHex; use regex::Regex; -use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; +use sha1::{Digest, Sha1}; use std::arch::x86_64::CpuidResult; use thiserror::Error; @@ -459,11 +459,12 @@ impl HardwareInfo { final_data.push(';'); } log::debug!("Hardware hash string \"{}\"", final_data); - let digest = ring::digest::digest(&SHA1_FOR_LEGACY_USE_ONLY, final_data.as_bytes()); + let mut hasher = Sha1::new(); + hasher.update(final_data.as_bytes()); + let digest = hasher.finalize(); if self.version < 4 { // they fucked up the format and used :x instead of :02x digest - .as_ref() .iter() .map(|byte| format!("{:x}", byte)) .collect::>() @@ -477,7 +478,7 @@ impl HardwareInfo { #[cfg(unix)] fn get_root_creation_str() -> String { use crate::unix::wine::wine_prefix_dir; - use chrono::{TimeZone, Utc}; + use chrono::{DateTime, TimeZone}; use std::{fs, os::unix::fs::MetadataExt}; let date_str = String::from("1970010100:00:00.000000000+0000"); @@ -490,7 +491,8 @@ fn get_root_creation_str() -> String { 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); + let datetime = + DateTime::from_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(); } diff --git a/maxima-lib/src/core/auth/login.rs b/maxima-lib/src/core/auth/login.rs index 73a2426f..46478e96 100644 --- a/maxima-lib/src/core/auth/login.rs +++ b/maxima-lib/src/core/auth/login.rs @@ -2,6 +2,7 @@ use regex::Regex; use std::sync::LazyLock; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::net::TcpListener; +use url::form_urlencoded; use crate::core::{auth::storage::AuthError, clients::JUNO_PC_CLIENT_ID}; @@ -35,12 +36,20 @@ pub async fn begin_oauth_login_flow<'a>(context: &mut AuthContext<'a>) -> Result let query = path_and_query .split_once("?") .map(|(_, qs)| qs.trim()) - .map(querystring::querify) + .map(|qs| { + let normalized = form_urlencoded::Serializer::new(String::new()) + .extend_pairs(form_urlencoded::parse(qs.as_bytes())) + .finish(); + + form_urlencoded::parse(normalized.as_bytes()) + .into_owned() + .collect::>() + }) .ok_or(AuthError::Query)?; - for query in query { - if query.0 == "code" { - context.set_code(query.1); + for (key, value) in query { + if key == "code" { + context.set_code(value.as_str()); return Ok(()); } } diff --git a/maxima-lib/src/core/auth/mod.rs b/maxima-lib/src/core/auth/mod.rs index b812061d..7d25a4b9 100644 --- a/maxima-lib/src/core/auth/mod.rs +++ b/maxima-lib/src/core/auth/mod.rs @@ -15,6 +15,7 @@ use derive_getters::Getters; use reqwest::{Client, Url, redirect}; use serde::Deserialize; use thiserror::Error; +use url::form_urlencoded::{Serializer, parse}; pub async fn nucleus_auth_exchange<'a>( auth_context: &AuthContext<'a>, @@ -61,18 +62,20 @@ pub async fn nucleus_auth_exchange<'a>( url.query() }; - let query = querystring::querify(query.ok_or(AuthError::Query)?); + let query = query.ok_or(AuthError::Query)?; + let normalized_query = Serializer::new(String::new()) + .extend_pairs(parse(query.as_bytes())) + .finish(); if response_type == "token" { response_type = "access_token"; } - let token = query - .iter() - .find(|(x, _)| *x == response_type) - .ok_or(AuthError::Query)? - .1; - Ok(token.to_owned()) + let token = parse(normalized_query.as_bytes()) + .find(|(key, _)| key == response_type) + .map(|(_, value)| value.into_owned()) + .ok_or(AuthError::Query)?; + Ok(token) } #[derive(Error, Debug)] diff --git a/maxima-lib/src/core/cache.rs b/maxima-lib/src/core/cache.rs index 7a13d0ca..284e911a 100644 --- a/maxima-lib/src/core/cache.rs +++ b/maxima-lib/src/core/cache.rs @@ -2,7 +2,14 @@ use std::{any::Any, borrow::Borrow, hash::Hash, sync::Arc, time::Duration}; use moka::sync::Cache; -/// Note that values are cloned when retrieved +/// A cache keyed by `K` whose values are type-erased via `dyn Any`. +/// +/// Note that values are cloned when retrieved. Because the value type isn't +/// part of `K`, nothing prevents two call sites from using the same key with +/// different `T`s; `get` guards against this by returning `None` on a type +/// mismatch rather than panicking, but such mismatches still won't be caught +/// at compile time. Callers should keep cache keys unambiguous (e.g. by +/// namespacing them per value type). pub struct DynamicCache { cache: Cache>, } @@ -33,6 +40,6 @@ impl DynamicCache { { self.cache .get(key) - .map(|cached| (*cached.downcast::().unwrap()).clone()) + .and_then(|cached| cached.downcast_ref::().cloned()) } } diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index c7986b5e..9bec5486 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -57,22 +57,28 @@ pub struct OwnedOffer { impl OwnedOffer { pub async fn is_installed(&self) -> 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 { + log::warn!("[{}] No install_check_override", self.slug); return false; }; let path = match parse_registry_path(path).await { - Ok(path) => path, - Err(_) => return false, + Ok(p) => p, + Err(_) => { + return false; + } }; - // If it wasn't replaced... if path.starts_with("[") { + log::warn!("[{}] Registry key unresolved: {:?}", self.slug, path); return false; } - #[cfg(unix)] - let path = case_insensitive_path(path); - path.exists() + let exists = path.exists(); + log::debug!( + "[{}] install check path {:?} exists={}", + self.slug, + path, + exists + ); + exists } pub async fn install_check_path(&self) -> Result { @@ -387,7 +393,7 @@ impl GameLibrary { let user: ServiceUser = self .service_layer - .request(SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, request) + .request(&SERVICE_REQUEST_GETPRELOADEDOWNEDGAMES, request) .await?; user.owned_game_products() .as_ref() @@ -404,7 +410,7 @@ impl GameLibrary { let defs: Vec = self .service_layer .request( - SERVICE_REQUEST_GETLEGACYCATALOGDEFS, + &SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetLegacyCatalogDefsRequestBuilder::default() .offer_ids(offer_ids) .locale(locale) diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 19f70a80..5954c4de 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -63,10 +63,20 @@ macro_rules! predip_type { predip_type!( FeatureFlags; attr { + // Attribute names on vary across installerdata.xml + // schema versions (e.g. `treatUpdatesAsMandatory` vs + // `treatUpdatesAsMandatoryEnabled`, `enableDifferentialUpdate` vs + // `UnableDifferentialUpdate`), so default each to an empty string + // when absent rather than failing to deserialize. + #[serde(default)] forceTouchupInstallerAfterUpdate: String, + #[serde(default)] autoUpdateEnabled: String, + #[serde(default)] treatUpdatesAsMandatoryEnabled: String, + #[serde(default)] useGameVersionFromManifestEnabled: String, + #[serde(default)] UnableDifferentialUpdate: String, }, data {} @@ -86,7 +96,10 @@ predip_type!( data { filePath: String, parameters: String, + // Not every installerdata.xml specifies update/repair parameters. + #[serde(default)] updateParameters: String, + #[serde(default)] repairParameters: String, } ); @@ -98,7 +111,11 @@ predip_type!( }, data { title: String, - eula: String, + // Some installerdata.xml files contain multiple entries + // within a single block, which a plain `String` field + // can't deserialize. + #[serde(default)] + eula: Vec, #[serde(default)] exclude: Vec, } @@ -108,6 +125,9 @@ predip_type!( Metadata; attr {}, data { + // Not every installerdata.xml includes a element; + // fall back to defaults (all-empty strings) when it's absent. + #[serde(default)] featureFlags: PreDiPFeatureFlags, os: PreDiPOs, #[serde(default)] @@ -126,7 +146,9 @@ predip_type!( data { #[serde(rename = "contentIDs")] contentIds: PreDiPContentIDs, - uninstall: PreDiPUninstall, + // Not every installerdata.xml includes an element. + #[serde(default)] + uninstall: Option, metadata: PreDiPMetadata, executable: PreDiPExecutable, installManifest: PreDiPInstallManifest, diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index 1a1209a3..c4419690 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -264,7 +264,7 @@ impl Maxima { let user: ServiceUser = self .service_layer .request( - SERVICE_REQUEST_GETUSERPLAYER, + &SERVICE_REQUEST_GETUSERPLAYER, ServiceGetUserPlayerRequest {}, ) .await?; @@ -283,7 +283,7 @@ impl Maxima { let friends: ServiceFriends = self .service_layer .request( - SERVICE_REQUEST_GETMYFRIENDS, + &SERVICE_REQUEST_GETMYFRIENDS, ServiceGetMyFriendsRequestBuilder::default() .limit(100) .offset(offset) @@ -329,7 +329,7 @@ impl Maxima { let data: ServicePlayer = self .service_layer .request( - SERVICE_REQUEST_GETBASICPLAYER, + &SERVICE_REQUEST_GETBASICPLAYER, ServiceGetBasicPlayerRequestBuilder::default() .pd(id.to_string()) .build() @@ -363,7 +363,9 @@ impl Maxima { return Ok(()); } - let response = reqwest::get(image.path()).await?; + // Reuse the ServiceLayerClient's client so avatar downloads share its + // connection pool instead of spinning up an ungated one. + let response = self.service_layer.client().get(image.path()).send().await?; let body: Vec = response.bytes().await?.to_vec(); let mut file = File::create(path)?; @@ -422,10 +424,12 @@ impl Maxima { &mut self.rtm } + /// Sets the port the LSX server listens on. pub fn set_lsx_port(&mut self, port: u16) { self.lsx_port = port; } + /// Tracks how many LSX clients are currently connected. pub(super) fn set_lsx_connections(&mut self, connections: u16) { self.lsx_connections = connections; } diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index df329992..28ad301e 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -4,9 +4,10 @@ use log::{debug, error, warn}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sha2_const; +use sha2::{Digest, Sha256}; use std::fmt; use std::sync::Arc; +use std::sync::LazyLock; use std::time::Duration; use thiserror::Error; use tokio::sync::{Mutex, Semaphore}; @@ -109,14 +110,14 @@ pub struct ServiceLayerGraphQLRequest { macro_rules! load_graphql_request { ($type:ident, $operation:expr, $key:expr) => {{ let content = include_str!(concat!("graphql/", $operation, ".gql")); - let hash = sha2_const::Sha256::new() - .update(content.as_bytes()) - .finalize(); + let mut hash = Sha256::new(); + hash.update(content.as_bytes()); + let result = hash.finalize(); ServiceLayerGraphQLRequest { query: content, operation: $operation, key: $key, - hash, + hash: result.into(), r#type: ServiceLayerRequestType::$type, } }}; @@ -124,7 +125,7 @@ macro_rules! load_graphql_request { macro_rules! define_graphql_request { ($type:ident, $operation:expr, $key:expr) => { pastey::paste! { - pub const []: &ServiceLayerGraphQLRequest = &load_graphql_request!($type, stringify!($operation), stringify!($key)); + pub static []: LazyLock = LazyLock::new(|| load_graphql_request!($type, stringify!($operation), stringify!($key))); }} } @@ -165,6 +166,11 @@ impl ServiceLayerClient { } } + /// Exposes the underlying `reqwest::Client` + pub fn client(&self) -> &Client { + &self.client + } + pub async fn request( &self, operation: &ServiceLayerGraphQLRequest, diff --git a/maxima-lib/src/lsx/request/account.rs b/maxima-lib/src/lsx/request/account.rs index 7e4ed645..8fd2992e 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -25,7 +25,7 @@ pub async fn handle_query_entitlements_request( let response: ServiceSdkEntitlementsResult = service_layer .request( - SERVICE_REQUEST_SDKENTITLEMENTS, + &SERVICE_REQUEST_SDKENTITLEMENTS, ServiceSdkEntitlementsRequestBuilder::default() .page_number(1) .page_size(100) diff --git a/maxima-lib/src/lsx/request/offer.rs b/maxima-lib/src/lsx/request/offer.rs index ac6b7b77..bbc60cbb 100644 --- a/maxima-lib/src/lsx/request/offer.rs +++ b/maxima-lib/src/lsx/request/offer.rs @@ -27,7 +27,7 @@ pub async fn handle_query_offers_request( let offers: ServiceAddonSearchResultRoot = maxima .service_layer() .request( - SERVICE_REQUEST_ADDONSEARCH, + &SERVICE_REQUEST_ADDONSEARCH, ServiceAddonSearchRequestBuilder::default() .platform(String::new()) .category_id(category) diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index 80ee07ea..6462da0a 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -18,7 +18,7 @@ use crate::{ }, make_lsx_handler_response, rtm::client::{BasicPresence, RichPresenceBuilder}, - util::native::{NativeError, SafeStr, platform_path}, + util::native::{SafeStr, platform_path}, }; pub async fn handle_profile_request( @@ -228,7 +228,7 @@ pub async fn handle_get_block_list_request( let friends: ServiceFriends = maxima .service_layer() .request( - SERVICE_REQUEST_GETMYFRIENDS, + &SERVICE_REQUEST_GETMYFRIENDS, ServiceGetMyFriendsRequestBuilder::default() .limit(100) .offset(0) diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 84f5e3eb..f5c8e3f0 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -6,9 +6,10 @@ use widestring::U16CString; #[cfg(windows)] use windows_sys::Win32::{ - Foundation::{ERROR_CANCELLED, GetLastError}, + Foundation::{ERROR_CANCELLED, CloseHandle, GetLastError}, System::Registry::{HKEY, KEY_QUERY_VALUE, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}, UI::Shell::{SEE_MASK_NO_CONSOLE, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, ShellExecuteExW}, + System::Threading::{WaitForSingleObject, INFINITE} }; #[cfg(windows)] @@ -92,73 +93,109 @@ pub fn check_registry_validity() -> Result<(), RegistryError> { Ok(()) } +#[cfg(windows)] +unsafe fn open_and_query_reg( + sub_key: &str, + value_name: &str, +) -> Result, RegistryError> { + let hkey = HKEY_LOCAL_MACHINE as HKEY; + let mut handle = ptr::null_mut(); + + if unsafe { + RegOpenKeyExW( + hkey, + U16CString::from_str(sub_key)?.as_ptr(), + 0, + KEY_QUERY_VALUE, + &mut handle, + ) + } != 0 + { + return Err(RegistryError::Key(sub_key.to_string())); + } + + let dw_type = ptr::null_mut(); + let mut dw_size = 0; + + if unsafe { + RegQueryValueExW( + handle, + U16CString::from_str(value_name)?.as_ptr(), + ptr::null_mut(), + dw_type, + ptr::null_mut(), + &mut dw_size, + ) + } != 0 + { + unsafe { RegCloseKey(handle) }; + return Err(RegistryError::Value { + value: value_name.to_string(), + key: sub_key.to_string(), + }); + } + + if dw_size == 0 { + unsafe { RegCloseKey(handle) }; + return Err(RegistryError::Value { + value: value_name.to_string(), + key: sub_key.to_string(), + }); + } + + let mut buf: Vec = vec![0; dw_size as usize / 2]; + if unsafe { + RegQueryValueExW( + handle, + U16CString::from_str(value_name)?.as_ptr(), + ptr::null_mut(), + dw_type, + buf.as_mut_ptr() as *mut u8, + &mut dw_size, + ) + } != 0 + { + unsafe { RegCloseKey(handle) }; + return Err(RegistryError::Value { + value: value_name.to_string(), + key: sub_key.to_string(), + }); + } + + unsafe { RegCloseKey(handle) }; + Ok(Some(String::from_utf16_lossy(&buf[..buf.len() - 1]))) +} + +#[cfg(windows)] +fn inject_wow6432node(sub_key: &str) -> Option { + // Only rewrite paths under SOFTWARE\ that aren't already redirected + let prefix = "SOFTWARE\\"; + if let Some(rest) = sub_key.strip_prefix(prefix) { + if !rest.starts_with("WOW6432Node\\") { + return Some(format!("SOFTWARE\\WOW6432Node\\{}", rest)); + } + } + None +} + #[cfg(windows)] async fn read_reg_key(path: &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)..]; - let hkey = HKEY_LOCAL_MACHINE as HKEY; - let mut handle = ptr::null_mut(); - - unsafe { - if RegOpenKeyExW( - hkey, - U16CString::from_str(sub_key)?.as_ptr(), - 0, - KEY_QUERY_VALUE, - &mut handle, - ) != 0 - { - return Err(RegistryError::Key(sub_key.to_string())); - } - - let dw_type = ptr::null_mut(); - let mut dw_size = 0; - - if RegQueryValueExW( - handle, - U16CString::from_str(value_name)?.as_ptr(), - ptr::null_mut(), - dw_type, - ptr::null_mut(), - &mut dw_size, - ) != 0 - { - RegCloseKey(handle); - return Err(RegistryError::Value { - value: value_name.to_string(), - key: sub_key.to_string(), - }); - } + // First attempt: path as-is + let result = unsafe { open_and_query_reg(sub_key, value_name) }; - if dw_size == 0 { - RegCloseKey(handle); - return Err(RegistryError::Value { - value: value_name.to_string(), - key: sub_key.to_string(), - }); - } - - let mut buf: Vec = vec![0; dw_size as usize / 2]; - if RegQueryValueExW( - handle, - U16CString::from_str(value_name)?.as_ptr(), - ptr::null_mut(), - dw_type, - buf.as_mut_ptr() as *mut u8, - &mut dw_size, - ) != 0 - { - RegCloseKey(handle); - return Err(RegistryError::Value { - value: value_name.to_string(), - key: sub_key.to_string(), - }); + match result { + Ok(v) => return Ok(v), + Err(_) => { + // Second attempt: inject WOW6432Node if applicable + if let Some(wow_sub_key) = inject_wow6432node(sub_key) { + return unsafe { open_and_query_reg(&wow_sub_key, value_name) }; + } + return Err(RegistryError::Key(sub_key.to_string())); } - - RegCloseKey(handle); - return Ok(Some(String::from_utf16_lossy(&buf[..buf.len() - 1]))); } } @@ -264,12 +301,17 @@ pub fn launch_bootstrap() -> Result<(), NativeError> { }; unsafe { - ShellExecuteExW(&mut shell_execute_info); - + let ok = ShellExecuteExW(&mut shell_execute_info); let err = GetLastError(); - if err == ERROR_CANCELLED { + + if ok == 0 || err == ERROR_CANCELLED { return Err(NativeError::Elevation(file1)); } + + if !shell_execute_info.hProcess.is_null() { + WaitForSingleObject(shell_execute_info.hProcess, INFINITE); + CloseHandle(shell_execute_info.hProcess); + } } Ok(()) diff --git a/maxima-lib/src/util/service_win.rs b/maxima-lib/src/util/service_win.rs index ace45002..7d1c2c6b 100644 --- a/maxima-lib/src/util/service_win.rs +++ b/maxima-lib/src/util/service_win.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::time::Duration; use widestring::U16CString; use windows_sys::Win32::{ - Foundation::ERROR_INSUFFICIENT_BUFFER, + Foundation::{LocalFree, ERROR_INSUFFICIENT_BUFFER}, Security::{ Authorization::{ ConvertSecurityDescriptorToStringSecurityDescriptorW, @@ -13,8 +13,8 @@ use windows_sys::Win32::{ DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, }, System::Services::{ - OpenSCManagerA, OpenServiceA, QueryServiceObjectSecurity, SetServiceObjectSecurity, - SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, + CloseServiceHandle, OpenSCManagerA, OpenServiceA, QueryServiceObjectSecurity, + SetServiceObjectSecurity, SC_MANAGER_CONNECT, SERVICE_ALL_ACCESS, }, }; use windows_service::service::{ @@ -50,10 +50,7 @@ pub fn register_service() -> Result<(), BackgroundServiceControlError> { // Update the existing service, if it exists let existing_service = service_manager.open_service( OsString::from(SERVICE_NAME), - ServiceAccess::START - | ServiceAccess::STOP - | ServiceAccess::CHANGE_CONFIG - | ServiceAccess::QUERY_STATUS, + ServiceAccess::START | ServiceAccess::STOP | ServiceAccess::CHANGE_CONFIG | ServiceAccess::QUERY_STATUS, ); if let Ok(service) = existing_service { @@ -61,100 +58,107 @@ pub fn register_service() -> Result<(), BackgroundServiceControlError> { let state = service.query_status()?.current_state; if state == ServiceState::Running { - let result = service.stop(); - if result.is_err() { - // Noop - } + let _ = service.stop(); } service.change_config(&service_info)?; + unsafe { init_service_security()?; } service.start(&[OsStr::new("")])?; return Ok(()); } let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; service.set_description(SERVICE_DISPLAY_NAME)?; - - // Allow the service to be started without administrative rights unsafe { init_service_security()? }; Ok(()) } pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlError> { - let hscm = unsafe { OpenSCManagerA( - std::ptr::null(), - CString::new("ServicesActive")?.as_ptr() as *const u8, - SC_MANAGER_ALL_ACCESS, - ) }; - - let hservice = unsafe { OpenServiceA( - hscm, - CString::new(SERVICE_NAME)?.as_ptr() as *const u8, - SERVICE_ALL_ACCESS, - ) }; - + let hscm = unsafe { + OpenSCManagerA( + std::ptr::null(), + CString::new("ServicesActive")?.as_ptr() as *const u8, + SC_MANAGER_CONNECT, + ) + }; + if hscm.is_null() { + return Err(BackgroundServiceControlError::ServiceObjectSecurity( + std::io::Error::last_os_error(), + )); + } + let _close_scm = scopeguard::guard(hscm, |h| unsafe { CloseServiceHandle(h); }); + + let hservice = unsafe { + OpenServiceA( + hscm, + CString::new(SERVICE_NAME)?.as_ptr() as *const u8, + SERVICE_ALL_ACCESS, + ) + }; if hservice.is_null() { return Err(BackgroundServiceControlError::Absent); } + let _close_service = scopeguard::guard(hservice, |h| unsafe { CloseServiceHandle(h); }); - // Query the service object security + // Query the service object security (first call to get required buffer size). let mut bytes_required: u32 = 0; - let result = unsafe { QueryServiceObjectSecurity( - hservice, - DACL_SECURITY_INFORMATION, - std::ptr::null_mut(), - 0, - &mut bytes_required, - ) }; - + let result = unsafe { + QueryServiceObjectSecurity( + hservice, + DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + 0, + &mut bytes_required, + ) + }; if result == 0 { - // The initial call failed; check if the error was related to buffer size let last_error = std::io::Error::last_os_error(); - let raw_error = (&last_error).raw_os_error().unwrap() as u32; + let raw_error = last_error.raw_os_error().unwrap_or(0) as u32; if raw_error != ERROR_INSUFFICIENT_BUFFER { return Err(BackgroundServiceControlError::ServiceObjectSecurity(last_error)); } } - // Allocate a buffer for the security descriptor let mut security_descriptor_buffer: Vec = vec![0; bytes_required as usize]; let security_descriptor = security_descriptor_buffer.as_mut_ptr() as PSECURITY_DESCRIPTOR; - // Query the service object security again with the correct buffer - let result = unsafe { QueryServiceObjectSecurity( - hservice, - DACL_SECURITY_INFORMATION, - security_descriptor, - bytes_required, - &mut bytes_required, - ) }; - + let result = unsafe { + QueryServiceObjectSecurity( + hservice, + DACL_SECURITY_INFORMATION, + security_descriptor, + bytes_required, + &mut bytes_required, + ) + }; if result == 0 { return Err(BackgroundServiceControlError::ServiceObjectSecurity( std::io::Error::last_os_error(), )); } - // Convert the security descriptor to a string + // Convert the security descriptor to an SDDL string. let mut sddl_string: *mut u16 = std::ptr::null_mut(); let mut sddl_string_len: u32 = 0; - let result = unsafe { ConvertSecurityDescriptorToStringSecurityDescriptorW( - security_descriptor, - SDDL_REVISION_1.into(), - DACL_SECURITY_INFORMATION, - &mut sddl_string, - &mut sddl_string_len, - ) }; - + let result = unsafe { + ConvertSecurityDescriptorToStringSecurityDescriptorW( + security_descriptor, + SDDL_REVISION_1.into(), + DACL_SECURITY_INFORMATION, + &mut sddl_string, + &mut sddl_string_len, + ) + }; if result == 0 { return Err(BackgroundServiceControlError::SecurityDescriptorToString( std::io::Error::last_os_error(), )); } + let _free_sddl = scopeguard::guard(sddl_string, |p| unsafe { LocalFree(p as *mut _); }); let sddl = unsafe { U16CString::from_ptr_str(sddl_string).to_string_lossy() }; - let sddl_to_add = "(A;;RPWPCR;;;BU)"; + let sddl_to_add = "(A;;CCLCRPWPLOCRRC;;;BU)"; if sddl.contains(sddl_to_add) { return Ok(()); } @@ -164,32 +168,36 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro let mut amended_security_descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut(); let mut amended_security_descriptor_len: u32 = 0; - let result = unsafe { ConvertStringSecurityDescriptorToSecurityDescriptorW( - U16CString::from_str(amended_sddl.as_str())?.as_ptr(), - SDDL_REVISION_1.into(), - &mut amended_security_descriptor, - &mut amended_security_descriptor_len, - ) }; - + let result = unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + U16CString::from_str(amended_sddl.as_str())?.as_ptr(), + SDDL_REVISION_1.into(), + &mut amended_security_descriptor, + &mut amended_security_descriptor_len, + ) + }; if result == 0 { return Err(BackgroundServiceControlError::StringToSecurityDescriptor( std::io::Error::last_os_error(), )); } - - // Set the service object security with the amended security descriptor - let result = unsafe { SetServiceObjectSecurity( - hservice, - DACL_SECURITY_INFORMATION, - amended_security_descriptor, - ) }; - + let _free_sd = + scopeguard::guard(amended_security_descriptor, |p| unsafe { LocalFree(p as *mut _); }); + + let result = unsafe { + SetServiceObjectSecurity( + hservice, + DACL_SECURITY_INFORMATION, + amended_security_descriptor, + ) + }; if result == 0 { return Err(BackgroundServiceControlError::SecurityAttributes( std::io::Error::last_os_error(), )); } + info!("Reapplied service DACL for {}", SERVICE_NAME); Ok(()) } diff --git a/maxima-resources/Cargo.toml b/maxima-resources/Cargo.toml index a4212c31..3033b1d0 100644 --- a/maxima-resources/Cargo.toml +++ b/maxima-resources/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "maxima-resources" -version = "0.1.0" -authors = ["Sean Kahler "] -edition = "2024" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } [target.'cfg(windows)'.dependencies] winres = "0.1.12" diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index bd7b4769..e2a79b8e 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "maxima-service" -version = "0.1.0" -authors = ["Sean Kahler "] -edition = "2024" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } [dependencies] maxima-lib = { path = "../maxima-lib" } diff --git a/maxima-service/src/service/error.rs b/maxima-service/src/service/error.rs index 8c64fdd7..1c7b3005 100644 --- a/maxima-service/src/service/error.rs +++ b/maxima-service/src/service/error.rs @@ -16,6 +16,8 @@ pub enum ServerError { #[error("attempted to inject into invalid process")] InvalidInjectionTarget, + #[error("timed out waiting for the HTTP server to bind")] + BindTimeout, } impl error::ResponseError for ServerError { diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 68a6dcd3..43468d68 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -1,15 +1,15 @@ -use std::fs::File; - -use actix_web::{HttpResponse, Responder, get, post, web}; -use log::info; -use maxima::util::registry::set_up_registry; -use maxima::util::service::SERVICE_NAME; use std::ffi::OsString; +use std::fs::File; use std::path::Path; +use std::sync::mpsc as std_mpsc; use std::thread; use std::time::Duration; + +use actix_web::{HttpResponse, Responder, get, post, web}; +use log::{error, info}; +use maxima::util::registry::set_up_registry; use structured_logger::json::new_writer; -use tokio::sync::mpsc::{self, Receiver}; +use tokio::sync::oneshot; use windows_service::service::{ ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, }; @@ -23,6 +23,7 @@ use crate::service::hash::get_sha256_hash_of_pid; use maxima::core::background_service::{BACKGROUND_SERVICE_PORT, ServiceLibraryInjectionRequest}; use maxima::util::dll_injector::{DllInjector, InjectionError}; use maxima::util::native::SafeParent; +use maxima::util::service::SERVICE_NAME; pub(crate) mod error; mod hash; @@ -30,27 +31,85 @@ mod hash; define_windows_service!(ffi_service_main, service_main); fn service_main(arguments: Vec) { - if let Err(_e) = bootstrap_service(arguments) { - // Handle error in some way. + if let Err(e) = bootstrap_service(arguments) { + error!("Service main failed: {}", e); } } -fn bootstrap_service(_arguments: Vec) -> windows_service::Result<()> { - let (shutdown_tx, shutdown_rx) = mpsc::channel(1); +enum BindResult { + Bound, + Failed(std::io::Error), +} + +fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { + let (shutdown_tx, shutdown_rx) = std_mpsc::channel::<()>(); + let (bind_tx, bind_rx) = std_mpsc::channel::(); + let (stop_tx, stop_rx) = oneshot::channel::<()>(); let event_handler = move |control_event| -> ServiceControlHandlerResult { match control_event { - ServiceControl::Stop | ServiceControl::Interrogate => { + ServiceControl::Stop => { let _ = shutdown_tx.send(()); ServiceControlHandlerResult::NoError } + // Return NoError as a no-op. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, } }; let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; - let next_status = ServiceStatus { + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::StartPending, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 1, + wait_hint: Duration::from_secs(5), + process_id: None, + })?; + + let log_path = Path::new("C:/ProgramData/Maxima/Logs/MaximaBackgroundService.log"); + std::fs::create_dir_all(log_path.safe_parent()?)?; + let log_file = File::create(log_path)?; + + structured_logger::Builder::new() + .with_default_writer(new_writer(log_file)) + .init(); + + info!("Started Background Service"); + + let actix_handle = thread::spawn(move || run_actix(bind_tx, stop_rx)); + + match bind_rx + .recv_timeout(Duration::from_secs(30)) + .map_err(|_| ServerError::BindTimeout)? + { + BindResult::Bound => { + info!("HTTP server bound to 127.0.0.1:{}", BACKGROUND_SERVICE_PORT); + } + BindResult::Failed(e) => { + error!( + "Failed to bind HTTP server to 127.0.0.1:{}: {}", + BACKGROUND_SERVICE_PORT, e + ); + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(e.raw_os_error().unwrap_or(1) as u32), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + let _ = stop_tx.send(()); + let _ = actix_handle.join(); + return Ok(()); + } + } + + status_handle.set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP, @@ -58,13 +117,15 @@ fn bootstrap_service(_arguments: Vec) -> windows_service::Result<()> { checkpoint: 0, wait_hint: Duration::default(), process_id: None, - }; + })?; - status_handle.set_service_status(next_status)?; - run_service(shutdown_rx).expect("Failed to run service"); + let _ = shutdown_rx.recv(); info!("Shutting down..."); + let _ = stop_tx.send(()); + let _ = actix_handle.join(); + status_handle.set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Stopped, @@ -118,44 +179,46 @@ async fn req_inject_library(body: web::Bytes) -> Result) -> Result<(), self::ServerError> { - let log_path = Path::new("C:/ProgramData/Maxima/Logs/MaximaBackgroundService.log"); - std::fs::create_dir_all(log_path.safe_parent()?)?; - let log_file = File::create(log_path)?; - - structured_logger::Builder::new() - .with_default_writer(new_writer(log_file)) - .init(); +fn run_actix(bind_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<()>) { + actix_web::rt::System::new().block_on(async { + use actix_web::{App, HttpServer}; + + let http_server = match HttpServer::new(|| { + App::new() + .service(req_set_up_registry) + .service(req_inject_library) + }) + .bind(("127.0.0.1", BACKGROUND_SERVICE_PORT)) + { + Ok(s) => s, + Err(e) => { + let _ = bind_tx.send(BindResult::Failed(e)); + return; + } + }; - info!("Started Background Service"); + let server_future = http_server.run(); + let handle = server_future.handle(); - thread::spawn(|| { - actix_web::rt::System::new() - .block_on(async || -> std::io::Result<()> { - use actix_web::{App, HttpServer}; - - let _ = HttpServer::new(|| { - App::new() - .service(req_set_up_registry) - .service(req_inject_library) - }) - .bind(("127.0.0.1", BACKGROUND_SERVICE_PORT))? - .run() - .await; - Ok(()) - }()) - .expect(&format!( - "Failed to bind to 127.0.0.1:{}", - BACKGROUND_SERVICE_PORT - )); - }); + if bind_tx.send(BindResult::Bound).is_err() { + // The main service thread is gone; stop the server. + handle.stop(true).await; + return; + } - let _ = shutdown_rx.recv(); + let stop_future = async { + let _ = stop_rx.await; + handle.stop(true).await; + }; - Ok(()) + tokio::select! { + _ = server_future => {} + _ = stop_future => {} + } + }); } pub fn start_service() -> Result<(), self::ServerError> { - service_dispatcher::start("MaximaBackgroundService", ffi_service_main)?; + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; Ok(()) -} +} \ No newline at end of file diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index 91ff3366..12eaf9e3 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "maxima-tui" -version = "0.1.0" -authors = ["Sean Kahler "] -edition = "2024" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } [dependencies] maxima-lib = { path = "../maxima-lib" } diff --git a/maxima-ui/src/bridge/game_details.rs b/maxima-ui/src/bridge/game_details.rs index db27a720..a889584e 100644 --- a/maxima-ui/src/bridge/game_details.rs +++ b/maxima-ui/src/bridge/game_details.rs @@ -23,7 +23,7 @@ pub async fn game_details_request( let maxima = maxima_arc.lock().await; let rq = maxima.service_layer().request( - SERVICE_REQUEST_GAMESYSTEMREQUIREMENTS, + &SERVICE_REQUEST_GAMESYSTEMREQUIREMENTS, ServiceGameSystemRequirementsRequestBuilder::default() .slug(slug.clone()) .locale(maxima.locale().short_str().to_owned()) diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index 16c8e3bf..a77fb638 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -112,7 +112,7 @@ async fn handle_images( } else { Some( service_layer.request( - SERVICE_REQUEST_GAMEIMAGES, + &SERVICE_REQUEST_GAMEIMAGES, ServiceGameImagesRequestBuilder::default() .should_fetch_context_image(!has_logo) .should_fetch_backdrop_images(!has_hero) @@ -127,7 +127,7 @@ async fn handle_images( } else { Some( service_layer.request( - SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, + &SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceHeroBackgroundImageRequestBuilder::default() .game_slug(slug.clone()) .locale(locale.clone()) diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 3dcf2ad2..629ba5df 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -266,7 +266,7 @@ impl BridgeThread { match request { MaximaLibRequest::StartService => { register_service_user()?; - tokio::time::sleep(Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_millis(200)).await; break 'wait_for_auth; } MaximaLibRequest::ShutdownRequest => return Ok(()), @@ -278,12 +278,49 @@ impl BridgeThread { if !is_service_running()? { info!("Starting service..."); start_service().await?; + + let service_addr = "127.0.0.1:13021"; + let mut ready = false; + for attempt in 0..50 { + match tokio::net::TcpStream::connect(service_addr).await { + Ok(_) => { + ready = true; + info!("Service HTTP endpoint ready after {} attempts", attempt + 1); + break; + } + Err(e) => { + if attempt % 5 == 0 { + warn!( + "Service not ready (attempt {}/50): {} — kind: {:?}", + attempt + 1, + e, + e.kind() + ); + } + tokio::time::sleep(Duration::from_millis(300)).await; + } + } + } + if !ready { + return Err(BackendError::BackgroundServiceClient( + maxima::core::error::BackgroundServiceClientError::Request( + "Service did not become ready after 50 attempts".to_string(), + ), + )); + } } } if let Err(err) = check_registry_validity() { warn!("{}, fixing...", err); - request_registry_setup().await?; + if let Err(e) = request_registry_setup().await { + let source_chain: Vec = + std::iter::successors(Some(&e as &dyn std::error::Error), |e| e.source()) + .map(|e| e.to_string()) + .collect(); + error!("Registry setup failed: {} (source: {:?})", e, source_chain); + return Err(e.into()); + } } } let maxima_arc: LockedMaxima = Maxima::new_with_options( diff --git a/maxima-ui/src/event_thread.rs b/maxima-ui/src/event_thread.rs index b82eb068..0abd3b8d 100644 --- a/maxima-ui/src/event_thread.rs +++ b/maxima-ui/src/event_thread.rs @@ -60,7 +60,7 @@ impl EventThread { let friends: ServiceFriends = maxima .service_layer() .request( - SERVICE_REQUEST_GETMYFRIENDS, + &SERVICE_REQUEST_GETMYFRIENDS, ServiceGetMyFriendsRequestBuilder::default() .offset(0) .limit(100) From 20a2c2cf163ac0d2436db25e2d2e6d587befcff3 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:13:55 +0200 Subject: [PATCH 19/57] . --- Cargo.lock | 132 ++++++------ maxima-service/src/service/error.rs | 2 + maxima-service/src/service/mod.rs | 318 +++++++++++++++++++--------- maxima-ui/src/bridge_thread.rs | 2 +- maxima-ui/src/util/markdown.rs | 2 + 5 files changed, 281 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec138b05..10a51165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,9 +213,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ "cipher", "cpubits", @@ -460,7 +460,7 @@ checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -870,9 +870,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -969,7 +969,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -1759,9 +1759,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "emath" @@ -2109,13 +2109,13 @@ dependencies = [ [[package]] name = "foreign-types-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -2365,9 +2365,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "glow" @@ -2643,9 +2643,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2880,9 +2880,9 @@ checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" [[package]] name = "impl-more" -version = "0.3.1" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" +checksum = "277ff51754a3f68f12f58446c5d006aa8baa4914ea273cce24a599cfaff33d4f" [[package]] name = "indenter" @@ -3167,9 +3167,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libfuzzer-sys" @@ -3453,7 +3453,7 @@ dependencies = [ "postcard", "prost", "prost-build", - "quick-xml 0.41.0", + "quick-xml", "rand 0.10.2", "regex", "reqwest", @@ -4519,9 +4519,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -4529,9 +4529,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -4539,9 +4539,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", @@ -4552,9 +4552,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", ] @@ -4931,15 +4931,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -5549,9 +5540,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -5700,7 +5691,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -6176,9 +6167,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -6371,7 +6362,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -6399,9 +6390,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "libc", @@ -6421,9 +6412,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -6482,9 +6473,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6520,9 +6511,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -6531,14 +6522,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -6903,9 +6895,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd4ec1eb1d240636e354a30110a1dfcb37047169a4d9bd6d9d3469df574b5c4" +checksum = "ef73bfbaf3216cb59c205d7176bee1194e0d84348979da31f4a71fefe3c2054e" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -6913,9 +6905,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8e44fd7ce9cf838a1e2dad56d2d83f2b2435ae1df9cb3cac31e759e455e88d" +checksum = "5b92170db3db8a6354f12a5b7f13a5453928433e08fc46aee51eedfa8f7a28a1" dependencies = [ "erased-serde", "serde_core", @@ -6924,9 +6916,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9f1705b87b798b06a8d7a51f44ed0626eee413991555e8edfd3562b35a130d" +checksum = "0bf9832097ca044466ae3f1aa43a943d4e450b7c3b8cdf65363875df07da79a0" dependencies = [ "sval", "sval_buffer", @@ -7090,9 +7082,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8" dependencies = [ "cc", "downcast-rs", @@ -7104,9 +7096,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.14" +version = "0.31.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ "bitflags 2.13.1", "rustix 1.1.4", @@ -7189,12 +7181,12 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml 0.39.4", + "quick-xml", "quote", ] @@ -7864,18 +7856,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", diff --git a/maxima-service/src/service/error.rs b/maxima-service/src/service/error.rs index 1c7b3005..7c713275 100644 --- a/maxima-service/src/service/error.rs +++ b/maxima-service/src/service/error.rs @@ -18,6 +18,8 @@ pub enum ServerError { InvalidInjectionTarget, #[error("timed out waiting for the HTTP server to bind")] BindTimeout, + #[error("HTTP server stopped unexpectedly")] + HttpServerStopped, } impl error::ResponseError for ServerError { diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 43468d68..07b234d3 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -5,13 +5,20 @@ use std::sync::mpsc as std_mpsc; use std::thread; use std::time::Duration; -use actix_web::{HttpResponse, Responder, get, post, web}; +use actix_web::{get, post, web, HttpResponse, Responder}; use log::{error, info}; +use maxima::core::background_service::{ + ServiceLibraryInjectionRequest, BACKGROUND_SERVICE_PORT, +}; +use maxima::util::dll_injector::{DllInjector, InjectionError}; +use maxima::util::native::SafeParent; use maxima::util::registry::set_up_registry; +use maxima::util::service::SERVICE_NAME; use structured_logger::json::new_writer; use tokio::sync::oneshot; use windows_service::service::{ - ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, }; use windows_service::{ service_control_handler::{self, ServiceControlHandlerResult}, @@ -20,30 +27,90 @@ use windows_service::{ use crate::service::error::ServerError; use crate::service::hash::get_sha256_hash_of_pid; -use maxima::core::background_service::{BACKGROUND_SERVICE_PORT, ServiceLibraryInjectionRequest}; -use maxima::util::dll_injector::{DllInjector, InjectionError}; -use maxima::util::native::SafeParent; -use maxima::util::service::SERVICE_NAME; pub(crate) mod error; mod hash; define_windows_service!(ffi_service_main, service_main); +const START_TIMEOUT: Duration = Duration::from_secs(30); +const STOP_TIMEOUT: Duration = Duration::from_secs(30); +const SERVICE_POLL_INTERVAL: Duration = Duration::from_millis(250); +const ERROR_TIMEOUT: u32 = 1460; +const ERROR_UNEXPECTED_EXIT: u32 = 1; + +enum Event { + Bound, + Exited(std::io::Result<()>), +} + fn service_main(arguments: Vec) { - if let Err(e) = bootstrap_service(arguments) { - error!("Service main failed: {}", e); + if let Err(error) = bootstrap_service(arguments) { + error!("Service main failed: {error}"); } } -enum BindResult { - Bound, - Failed(std::io::Error), +fn service_status( + state: ServiceState, + controls_accepted: ServiceControlAccept, + exit_code: ServiceExitCode, + checkpoint: u32, + wait_hint: Duration, +) -> ServiceStatus { + ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: state, + controls_accepted, + exit_code, + checkpoint, + wait_hint, + process_id: None, + } +} + +fn start_pending_status() -> ServiceStatus { + service_status( + ServiceState::StartPending, + ServiceControlAccept::empty(), + ServiceExitCode::Win32(0), + 1, + START_TIMEOUT, + ) +} + +fn running_status() -> ServiceStatus { + service_status( + ServiceState::Running, + ServiceControlAccept::STOP, + ServiceExitCode::Win32(0), + 0, + Duration::default(), + ) +} + +fn stop_pending_status() -> ServiceStatus { + service_status( + ServiceState::StopPending, + ServiceControlAccept::empty(), + ServiceExitCode::Win32(0), + 1, + STOP_TIMEOUT, + ) +} + +fn stopped_status(exit_code: ServiceExitCode) -> ServiceStatus { + service_status( + ServiceState::Stopped, + ServiceControlAccept::empty(), + exit_code, + 0, + Duration::default(), + ) } fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { let (shutdown_tx, shutdown_rx) = std_mpsc::channel::<()>(); - let (bind_tx, bind_rx) = std_mpsc::channel::(); + let (_tx, _rx) = std_mpsc::channel::(); let (stop_tx, stop_rx) = oneshot::channel::<()>(); let event_handler = move |control_event| -> ServiceControlHandlerResult { @@ -52,7 +119,6 @@ fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { let _ = shutdown_tx.send(()); ServiceControlHandlerResult::NoError } - // Return NoError as a no-op. ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, } @@ -60,15 +126,7 @@ fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::StartPending, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 1, - wait_hint: Duration::from_secs(5), - process_id: None, - })?; + status_handle.set_service_status(start_pending_status())?; let log_path = Path::new("C:/ProgramData/Maxima/Logs/MaximaBackgroundService.log"); std::fs::create_dir_all(log_path.safe_parent()?)?; @@ -78,63 +136,117 @@ fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { .with_default_writer(new_writer(log_file)) .init(); - info!("Started Background Service"); + info!("Starting Background Service"); - let actix_handle = thread::spawn(move || run_actix(bind_tx, stop_rx)); + let _handle = thread::spawn(move || run_(_tx, stop_rx)); - match bind_rx - .recv_timeout(Duration::from_secs(30)) - .map_err(|_| ServerError::BindTimeout)? - { - BindResult::Bound => { - info!("HTTP server bound to 127.0.0.1:{}", BACKGROUND_SERVICE_PORT); + match _rx.recv_timeout(START_TIMEOUT) { + Ok(Event::Bound) => { + info!("HTTP server bound to 127.0.0.1:{BACKGROUND_SERVICE_PORT}"); } - BindResult::Failed(e) => { - error!( - "Failed to bind HTTP server to 127.0.0.1:{}: {}", - BACKGROUND_SERVICE_PORT, e - ); - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(e.raw_os_error().unwrap_or(1) as u32), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - })?; + + Ok(Event::Exited(Err(error))) => { + error!("HTTP server failed before readiness: {error}"); + + status_handle.set_service_status(stopped_status(ServiceExitCode::Win32( + error.raw_os_error().unwrap_or(ERROR_UNEXPECTED_EXIT as i32) as u32, + )))?; + + let _ = _handle.join(); + return Err(error.into()); + } + + Ok(Event::Exited(Ok(()))) => { + error!("HTTP server stopped before it became ready"); + + status_handle.set_service_status(stopped_status(ServiceExitCode::Win32( + ERROR_UNEXPECTED_EXIT, + )))?; + + let _ = _handle.join(); + return Err(ServerError::HttpServerStopped); + } + + Err(std_mpsc::RecvTimeoutError::Timeout) => { + error!("Timed out waiting for HTTP server bind"); + let _ = stop_tx.send(()); - let _ = actix_handle.join(); - return Ok(()); + let _ = _handle.join(); + + status_handle.set_service_status(stopped_status(ServiceExitCode::Win32( + ERROR_TIMEOUT, + )))?; + + return Err(ServerError::BindTimeout); + } + + Err(std_mpsc::RecvTimeoutError::Disconnected) => { + error!("HTTP server thread ended without reporting readiness"); + + let _ = _handle.join(); + + status_handle.set_service_status(stopped_status(ServiceExitCode::Win32( + ERROR_UNEXPECTED_EXIT, + )))?; + + return Err(ServerError::HttpServerStopped); } } - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - })?; + status_handle.set_service_status(running_status())?; + + let (stopped_by_scm, exit_code) = loop { + match shutdown_rx.recv_timeout(SERVICE_POLL_INTERVAL) { + Ok(()) => break (true, ServiceExitCode::Win32(0)), + + Err(std_mpsc::RecvTimeoutError::Timeout) => match _rx.try_recv() { + Ok(Event::Exited(Ok(()))) => { + error!("HTTP server stopped unexpectedly"); + break (false, ServiceExitCode::Win32(ERROR_UNEXPECTED_EXIT)); + } + + Ok(Event::Exited(Err(error))) => { + error!("HTTP server failed unexpectedly: {error}"); + + let win32_code = error + .raw_os_error() + .unwrap_or(ERROR_UNEXPECTED_EXIT as i32) + as u32; + + break (false, ServiceExitCode::Win32(win32_code)); + } + + Ok(Event::Bound) => { + error!("Received unexpected duplicate HTTP bound event"); + } + + Err(std_mpsc::TryRecvError::Empty) => {} - let _ = shutdown_rx.recv(); + Err(std_mpsc::TryRecvError::Disconnected) => { + error!("HTTP server event channel disconnected"); + break (false, ServiceExitCode::Win32(ERROR_UNEXPECTED_EXIT)); + } + }, - info!("Shutting down..."); + Err(std_mpsc::RecvTimeoutError::Disconnected) => { + error!("Service control channel disconnected"); + break (false, ServiceExitCode::Win32(ERROR_UNEXPECTED_EXIT)); + } + } + }; + + if stopped_by_scm { + info!("Stopping Background Service after SCM stop request"); + } else { + error!("Stopping Background Service because the HTTP server exited unexpectedly"); + } + + status_handle.set_service_status(stop_pending_status())?; let _ = stop_tx.send(()); - let _ = actix_handle.join(); + let _ = _handle.join(); - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - })?; + status_handle.set_service_status(stopped_status(exit_code))?; Ok(()) } @@ -142,45 +254,38 @@ fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { #[get("/set_up_registry")] async fn req_set_up_registry() -> impl Responder { info!("Setting up registry"); - if let Err(err) = set_up_registry() { - return format!("Error: {}", err); + + if let Err(error) = set_up_registry() { + return format!("Error: {error}"); } - format!("Done") + "Done".to_owned() } pub fn inject_dll(pid: u32, dll_path: &str) -> Result<(), InjectionError> { - let injector = DllInjector::new(pid); - injector.inject(dll_path) + DllInjector::new(pid).inject(dll_path) } // This is for KYBER. Ideally this would be moved to a separate Kyber service, -// but it isn't a great user experience to have to install two windows services. -// We'll eventually find a better workaround and move this somewhere else. +// but it isn't a great user experience to have to install two Windows services. #[post("/inject_library")] -async fn req_inject_library(body: web::Bytes) -> Result { - info!("Injecting..."); +async fn req_inject_library(body: web::Bytes) -> Result { + info!("Injecting library"); - let req: ServiceLibraryInjectionRequest = serde_json::from_slice(&body)?; + let request: ServiceLibraryInjectionRequest = serde_json::from_slice(&body)?; + let hash = get_sha256_hash_of_pid(request.pid)?; - let hash_result = get_sha256_hash_of_pid(req.pid)?; - - // Ensure we're only injecting into STAR WARS Battlefront 2. Ideally we would check - // the hash of the dll as well, but there isn't a great way to do that since there - // are multiple release channels and dev builds. - if hex::encode(hash_result) - != "7880e40d79e981b064baaf06f10785601222c6e227a656b70112c24b1f82e2ce" - { - return Err(self::ServerError::InvalidInjectionTarget); + if hex::encode(hash) != "7880e40d79e981b064baaf06f10785601222c6e227a656b70112c24b1f82e2ce" { + return Err(ServerError::InvalidInjectionTarget); } - inject_dll(req.pid, &req.path)?; + inject_dll(request.pid, &request.path)?; Ok(HttpResponse::Ok().body("Injected")) } -fn run_actix(bind_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<()>) { - actix_web::rt::System::new().block_on(async { +fn run_(_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<()>) { + actix_web::rt::System::new().block_on(async move { use actix_web::{App, HttpServer}; let http_server = match HttpServer::new(|| { @@ -190,35 +295,40 @@ fn run_actix(bind_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<( }) .bind(("127.0.0.1", BACKGROUND_SERVICE_PORT)) { - Ok(s) => s, - Err(e) => { - let _ = bind_tx.send(BindResult::Failed(e)); + Ok(server) => server, + Err(error) => { + let _ = _tx.send(Event::Exited(Err(error))); return; } }; - let server_future = http_server.run(); - let handle = server_future.handle(); + let server = http_server.run(); + let handle = server.handle(); + tokio::pin!(server); - if bind_tx.send(BindResult::Bound).is_err() { - // The main service thread is gone; stop the server. + if _tx.send(Event::Bound).is_err() { handle.stop(true).await; + let _ = (&mut server).await; return; } - let stop_future = async { - let _ = stop_rx.await; - handle.stop(true).await; - }; - tokio::select! { - _ = server_future => {} - _ = stop_future => {} + result = &mut server => { + let _ = _tx.send(Event::Exited(result)); + } + + _ = stop_rx => { + info!("Stopping HTTP server"); + handle.stop(true).await; + + let result = (&mut server).await; + let _ = _tx.send(Event::Exited(result)); + } } }); } -pub fn start_service() -> Result<(), self::ServerError> { +pub fn start_service() -> Result<(), ServerError> { service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; Ok(()) } \ No newline at end of file diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 629ba5df..2377b3ed 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -569,7 +569,7 @@ impl BridgeThread { .path(path) .build()?; maxima.content_manager().add_install(game).await?; - Self::update_queue(maxima.content_manager(), backend_responder.clone()); // ← add this + Self::update_queue(maxima.content_manager(), backend_responder.clone()); Ok(()) } MaximaLibRequest::CancelInstallRequest(offer) => { diff --git a/maxima-ui/src/util/markdown.rs b/maxima-ui/src/util/markdown.rs index 7c6c01c6..29dd9346 100644 --- a/maxima-ui/src/util/markdown.rs +++ b/maxima-ui/src/util/markdown.rs @@ -4,6 +4,8 @@ pub fn html_to_easymark(html: &str) -> String { .replace("\n", "") .replace("*", "\\*") .replace("
", "\n") + .replace("

", "") + .replace("
", "\n") .replace("", "*") .replace("", "*") .replace("", "") // WE DO NOT CARE 🗣️ From 5e3058545bc6af7128e07a8d353cf620f4e28655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:57:22 +0200 Subject: [PATCH 20/57] Update rtm.proto --- maxima-lib/src/rtm/proto/rtm.proto | 77 ++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/maxima-lib/src/rtm/proto/rtm.proto b/maxima-lib/src/rtm/proto/rtm.proto index b798fdab..e327f986 100644 --- a/maxima-lib/src/rtm/proto/rtm.proto +++ b/maxima-lib/src/rtm/proto/rtm.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package eadp.rtm; +package com.ea.eadp.antelope.rtm.protocol; import "common.proto"; import "requests.proto"; @@ -7,26 +7,65 @@ import "responses.proto"; import "chat.proto"; message Communication { + oneof body { CommunicationV1 v1 = 1; + } } message CommunicationV1 { - string requestId = 1; - - oneof body { - PresenceSubscribeV1 presenceSubscribe = 2; - PresenceUpdateV1 presenceUpdate = 5; - PresenceV1 presence = 7; - PlayerContextV1 playerContext = 9; - ConversationChannelV1 conversationChannel = 11; - ConversationHistoryRequestV1 historyRequest = 12; // outer msg, not oneof - ErrorV1 error = 13; - ReconnectRequestV1 reconnectRequest = 14; - SuccessV1 success = 15; - HeartbeatV1 heartbeat = 20; - TypingEventV1 typingEvent = 43; - string unknown44 = 44; // unsure yet - LoginRequestV3 loginRequestV3 = 46; - SessionCleanupV1 sessionCleanupV1 = 49; - } + oneof body { + PresenceSubscribeV1 presenceSubscribe = 2; + PresenceUnsubscribeV1 presenceUnsubscribe = 3; + PresenceSubscriptionErrorV1 presenceSubscriptionError = 4; + PresenceUpdateV1 presenceUpdate = 5; + PresenceUpdateErrorV1 presenceUpdateError = 6; + PresenceV1 presence = 7; + NotificationV1 notification = 8; + ChatInitiateV1 chatInitiate = 9; + ChatLeaveV1 chatLeave = 10; + ChatMembersRequestV1 chatMembersRequest = 11; + ChatMembersV1 chatMembers = 12; + ErrorV1 error = 13; + ReconnectRequestV1 reconnectRequest = 14; + SuccessV1 success = 15; + PointToPointMessageV1 pointToPointMessage = 16; + ChatChannelsRequestV1 chatChannelsRequest = 17; + ChatChannelsV1 chatChannels = 18; + LoginRequestV1 loginRequest = 19; + HeartbeatV1 heartbeat = 20; + WorldChatAssignV1 worldChatAssign = 21; + WorldChatResponseV1 worldChatResponse = 22; + MuteUserV1 muteUser = 23; + UnmuteUserV1 unmuteUser = 24; + UserMembershipChangeV1 userMembershipChange = 25; + WorldChatConfigurationRequestV1 worldChatConfigurationRequestV1 = 26; + WorldChatConfigurationResponseV1 worldChatConfigurationResponse = 27; + WorldChatChannelsRequestV1 worldChatChannelsRequestV1 = 28; + WorldChatChannelsResponseV1 worldChatChannelsResponse = 29; + PromoteStickyMessageRequestV1 promoteStickyMessageRequest = 30; + RemoveStickyMessageRequestV1 removeStickyMessageRequest = 31; + FetchStickyMessagesRequestV1 fetchStickyMessagesRequest = 32; + StickyMessageResponseV1 stickyMessageResponse = 33; + GetRolesRequest rolesRequest = 34; + GetRolesResponse rolesResponse = 35; + UpdatePreferenceRequestV1 preferenceRequest = 36; + GetPreferenceRequestV1 getPreferenceRequest = 37; + GetPreferenceResponseV1 getPreferenceResponse = 38; + LoginRequestV2 loginRequestV2 = 39; + SessionNotificationV1 sessionNotification = 40; + ChatTypingEventRequestV1 chatTypingEventRequest = 41; + ChatChannelsRequestV2 chatChannelsRequestV2 = 42; + ChatChannelUpdateV1 chatChannelUpdate = 43; + ServerConfigRequestV1 serverConfigRequest = 44; + ServerConfigV1 serverConfig = 45; + LoginRequestV3 loginRequestV3 = 46; + SessionRequestV1 sessionRequestV1 = 47; + SessionResponseV1 sessionResponseV1 = 48; + SessionCleanupV1 sessionCleanupV1 = 49; + WorldChatNotificationV1 worldChatNotificationV1 = 50; + PresenceSubscribeAllFriendsV1 presenceSubscribeAllFriendsV1 = 51; + PresenceSubscriptionAllFriendsErrorV1 presenceSubscriptionAllFriendsErrorV1 = 52; + } + string requestId = 1; } + From c8425cfc37d71fc522f2377c845961c63824b2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:04:21 +0200 Subject: [PATCH 21/57] Update requests.proto --- maxima-lib/src/rtm/proto/requests.proto | 44 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/maxima-lib/src/rtm/proto/requests.proto b/maxima-lib/src/rtm/proto/requests.proto index 77741052..f721de70 100644 --- a/maxima-lib/src/rtm/proto/requests.proto +++ b/maxima-lib/src/rtm/proto/requests.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package eadp.rtm; +package com.ea.eadp.antelope.rtm.protocol; import "common.proto"; @@ -7,23 +7,35 @@ message PresenceSubscribeV1 { repeated Player players = 2; } -message LoginRequestV3 { - string token = 1; - - bool reconnect = 2; - bool heartbeat = 3; - - UserType userType = 4; - string productId = 5; - PlatformV1 platform = 6; - string clientVersion = 7; - - optional string sessionKey = 8; - optional string forceDisconnectSessionKey = 9; +message ReconnectRequestV1 { + string sessionKey = 1; } -message ReconnectRequestV1 { - string sessionKey = 1; +message LoginRequestV1 { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; +} + +message LoginRequestV2 { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; + UserType userType = 4; + string productId = 5; + bool singleSessionForceLogout = 6; +} + +message LoginRequestV3 { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; + UserType userType = 4; + string productId = 5; + PlatformV1 platform = 6; + string clientVersion = 7; + string sessionKey = 8; + string forceDisconnectSessionKey = 9; } message PresenceUpdateV1 { From 65c0ebed690167fa79e573ee684f195125c9ac7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:15:47 +0200 Subject: [PATCH 22/57] Update common.proto --- maxima-lib/src/rtm/proto/common.proto | 112 +++++++++++++++++--------- 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/maxima-lib/src/rtm/proto/common.proto b/maxima-lib/src/rtm/proto/common.proto index d49cc049..914ff4d0 100644 --- a/maxima-lib/src/rtm/proto/common.proto +++ b/maxima-lib/src/rtm/proto/common.proto @@ -1,57 +1,93 @@ syntax = "proto3"; -package eadp.rtm; +package com.ea.eadp.antelope.rtm.protocol; enum PlatformV1 { - NONE_PLATFORM = 0; - UNKNOWN_PLATFORM = 1; - MOBILE = 2; - PC = 3; - WEB = 4; - XBOXLIVE = 5; - PSN = 6; - SWITCH = 7; - STEAM = 8; - XBSK = 9; - PS5 = 10; + NONE_PLATFORM = 1; + UNKNOWN_PLATFORM = 1; + MOBILE = 2; + PC = 3; + WEB = 4; + XBOXLIVE = 5; + PSN = 6; + SWITCH = 7; + STEAM = 8; + XBSK = 9; + PS5 = 10; } enum UserType { - NONE_USER_TYPE = 0; - PERSONA = 1; - NUCLEUS = 2; + NONE_USERTYPE = 0; + PERSONA = 1; + NUCLEUS = 2; } enum BasicPresenceType { - NONE_PRESENCE = 0; - UNKNOWN_PRESENCE = 1; - OFFLINE = 2; - ONLINE = 3; - DND = 4; - AWAY = 5; - INVISIBLE = 6; - CUSTOM = 7; + NONE_PRESENCE = 0; + UNKNOWN_PRESENCE = 1; + OFFLINE = 2; + ONLINE = 3; + DND = 4; + AWAY = 5; + INVISIBLE = 6; + CUSTOM = 7; } enum RichPresenceType { - NONE_RICH_PRESENCE = 0; - UNKNOWN_RICH_PRESENCE = 1; - LFG = 2; - BROADCASTING = 3; + NONE_RICH_PRESENCE = 0; + UNKNOWN_RICH_PRESENCE = 1; + LFG = 2; + BROADCASTING = 3; +} + +message PresenceV1 { + string personaId = 1; + string status = 2; + string timestamp = 3; + Player player = 4; + BasicPresenceType basicPresenceType = 5; + string userDefinedPresence = 6; + RichPresenceV1 richPresence = 7; + string sessionKey = 8; + string clientVersion = 9; + PlatformV1 platform = 10; +} + +message PresenceUpdateErrorV1 { + string errorCode = 1; + string errorMessage = 2; +} + +message PresenceSubscriptionErrorV1 { + string personaId = 1; + string errorCode = 2; + string errorMessage = 3; + Player player = 4; +} + +message PresenceSubscriptionAllFriendsErrorV1 { + string errorCode = 1; + string errorMessage = 2; } message RichPresenceV1 { - string game = 1; - PlatformV1 platform = 2; - string gameModeType = 3; - string gameMode = 4; - string gameSessionData = 5; - RichPresenceType richPresenceType = 6; - string startTimestamp = 7; - string endTimestamp = 8; - string customRichPresenceData = 9; + string game = 1; + PlatformV1 platform = 2; + string gameModeType = 3; + string gameMode = 4; + string gameSessionData = 5; + RichPresenceType richPresenceType = 6; + string startTimestamp = 7; + string endTimestamp = 8; + string customRichPresenceData = 9; } message Player { - string playerId = 1; - string productId = 2; + string playerId = 1; + string productId = 2; +} + +message PlayerInfo { + Player player = 1; + string displayName = 2; + string nickName = 3; } From 855dc030a7fc42e79ff027c09b608651b1abf344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:16:25 +0200 Subject: [PATCH 23/57] Update chat.proto --- maxima-lib/src/rtm/proto/chat.proto | 247 ++++++++++++++++++++++++++-- 1 file changed, 229 insertions(+), 18 deletions(-) diff --git a/maxima-lib/src/rtm/proto/chat.proto b/maxima-lib/src/rtm/proto/chat.proto index b3d75f47..e383c05e 100644 --- a/maxima-lib/src/rtm/proto/chat.proto +++ b/maxima-lib/src/rtm/proto/chat.proto @@ -1,30 +1,241 @@ syntax = "proto3"; -package eadp.rtm; +package com.ea.eadp.antelope.rtm.protocol; -// incomplete, wip +enum TypingEventType { + COMPOSING = 1; + CUSTOM_EVENT = 2; +} + +enum InviteChannelScope { + IN_GAME = 1; + CROSS_GAME = 2; + ALL = 3; +} + +enum ChannelMembershipChangeType { + ADDED = 1; + REMOVED = 2; +} + +enum StickyMessageChangeType { + STICKY_MESSAGE_ADDED = 1; + STICKY_MESSAGE_REMOVED = 2; +} + +message ChatInitiateV1 { + repeated string personaIds = 1; + repeated Player players = 2; + bool crossGame = 3; +} + +message ChatLeaveV1 { + string channelId = 1; +} + +message ChatMembersRequestV1 { + repeated string channelId = 1; + ChannelType channelType = 2; + int32 fromIndex = 3; + int32 pageSize = 4; +} + +message ChatChannelsRequestV1 { + bool includeUnreadMessageCount = 1; + bool includeLastMessageInfo = 2; + InviteChannelScope inviteChannelScope = 3; + repeated ChannelType channelTypes = 4; +} + +message ChatChannelsRequestV2 { + bool includeLastMessageInfo = 1; + bool includeUnreadMessageInfo = 2; + repeated ChannelMessageType unreadMessageTypes = 3; + bool includeCustomProperties = 4; + InviteChannelScope inviteChannelScope = 5; + repeated ChannelType channelTypes = 6; +} + +message WorldChatConfigurationRequestV1 { +} + +message WorldChatChannelsRequestV1 { + string worldName = 1; + int32 fromIndex = 2; + int32 pageSize = 3; +} + +message WorldChatAssignV1 { + string worldName = 1; + int32 shardIndex = 2; +} + +message MuteUserV1 { + string channelId = 1; + string personaId = 2; + Player player = 3; +} + +message UnmuteUserV1 { + string channelId = 1; + string personaId = 2; + Player player = 3; +} + +message PromoteStickyMessageRequestV1 { + string channelId = 1; + string messageId = 2; + string timestamp = 3; + int32 index = 4; +} + +message RemoveStickyMessageRequestV1 { + string channelId = 1; + int32 index = 2; +} + +message FetchStickyMessagesRequestV1 { + string channelId = 1; +} + +message ChatTypingEventRequestV1 { + string channelId = 1; + TypingEventType event = 2; + string customTypingEvent = 3; +} + +message ChatMembersV1 { + string channelId = 1; + repeated PersonaV1 users = 2; + int32 totalMemberCount = 3; + repeated PlayerInfo players = 4; +} + +message ChatChannelsV1 { + repeated ChannelV1 channels = 1; + repeated ChannelMuteListV1 muteList = 2; + string lastOnlineTimestamp = 3; +} + +message WorldChatResponseV1 { + string channelId = 1; + int32 remainingDailyShardSwitchCount = 2; + int32 shardIndex = 3; + string worldName = 4; + string previousChannelId = 5; + int32 previousShardIndex = 6; + repeated ChannelMuteListV1 muteList = 7; + string channelName = 8; +} + +message WorldChatConfigurationResponseV1 { + int32 remainingDailyShardSwitchCount = 1; + int32 maxShards = 2; + int32 maxShardSize = 3; + int32 maxWorldNamesPerGame = 4; + int32 maxDailyShardSwitchCount = 5; +} + +message WorldChatChannelsResponseV1 { + string worldName = 1; + repeated WorldChatShard shard = 2; + int32 totalShardCount = 3; +} + +message WorldChatShard { + int32 index = 1; + int32 size = 2; +} + +message StickyMessageResponseV1 { + string channelId = 1; + repeated StickyMessageV1 message = 2; +} + +message ChatConnectedV1 { + PersonaV1 user = 1; + PlayerInfo players = 2; +} + +message ChatDisconnectedV1 { + PersonaV1 user = 1; + PlayerInfo players = 2; +} + +message ChatLeftV1 { + PersonaV1 user = 1; + PlayerInfo players = 2; +} + +message ChatUserMutedV1 { + PersonaV1 mutedUser = 1; + PersonaV1 mutedByUser = 2; + PlayerInfo mutedPlayer = 3; + PlayerInfo mutedByPlayer = 4; +} + +message ChatUserUnmutedV1 { + PersonaV1 unmutedUser = 1; + PersonaV1 unmutedByUser = 2; + PlayerInfo unmutedPlayer = 3; + PlayerInfo unmutedByPlayer = 4; +} + +message ChannelMembershipChangeV1 { + ChannelMembershipChangeType type = 1; + repeated PersonaV1 users = 2; + repeated PlayerInfo players = 3; +} + +message StickyMessageChangedV1 { + StickyMessageChangeType stickyMessageChangeType = 1; + StickyMessageV1 stickyMessage = 2; + int32 index = 3; +} + +message ChatTypingEventV1 { + TypingEventType event = 1; + string customTypingEvent = 2; + Player fromPlayer = 3; + string fromDisplayName = 4; + string fromNickName = 5; + string fromPlatformDisplayName = 6; + string fromPlatformNickName = 7; +} -message ConversationChannelV1 { - string channel_id = 1; // e.g. "--CrossGame" +message WorldChatMembershipChangeV1 { + ChannelMembershipChangeType type = 1; + PlayerInfo playerInfo = 2; } -message ConversationHistoryRequestV1 { - string channel_id = 1; - string timestamp = 2; - int32 limit = 3; - string message_type = 4; // "TEXT_MESSAGE" +message GroupMemberAttributeChangeV1 { + Player targetPlayer = 1; + string targetDisplayName = 2; + string targetNickName = 3; + string updatedTargetAttributes = 4; + Player initiatorPlayer = 5; + string initiatorDisplayName = 6; + string initiatorNickName = 7; + string initiatorAttributes = 8; } -message TypingEventV1 { - string channel_id = 1; - string timestamp = 2; +message GroupAttributeChangeV1 { + PlayerInfo initiatorPlayerInfo = 1; + string attributeName = 2; + string attributeDetail = 3; + string oldValue = 4; + string newValue = 5; + string timestamp = 6; } -message PlayerContextV1 { - PlayerRef player = 2; - int32 unknown3 = 3; +message GroupMemberRoleChangeV1 { + PlayerInfo initiatorPlayerInfo = 1; + PlayerInfo targetPlayerInfo = 2; + string newRole = 3; + string timestamp = 4; } -message PlayerRef { - string player_id = 1; - string product_id = 2; // "origin" +message ChatChannelUpdateV1 { + string channelId = 1; + string lastReadTimestamp = 2; + string customProperties = 3; } From e99270a5be99c76d790bfead94120edf9170b1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:17:43 +0200 Subject: [PATCH 24/57] Update chat.proto --- maxima-lib/src/rtm/proto/chat.proto | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/maxima-lib/src/rtm/proto/chat.proto b/maxima-lib/src/rtm/proto/chat.proto index e383c05e..1c99ffb4 100644 --- a/maxima-lib/src/rtm/proto/chat.proto +++ b/maxima-lib/src/rtm/proto/chat.proto @@ -2,22 +2,26 @@ syntax = "proto3"; package com.ea.eadp.antelope.rtm.protocol; enum TypingEventType { + NO_EVENT = 1: COMPOSING = 1; CUSTOM_EVENT = 2; } enum InviteChannelScope { + NO_INVITE_SCOPE = 0; IN_GAME = 1; CROSS_GAME = 2; ALL = 3; } enum ChannelMembershipChangeType { + NOTHING = 0; ADDED = 1; REMOVED = 2; } enum StickyMessageChangeType { + NO_STICKY = 0; STICKY_MESSAGE_ADDED = 1; STICKY_MESSAGE_REMOVED = 2; } From e8d2bf8e4b92ababe5e92bbac4f16ed7dc4584f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:18:39 +0200 Subject: [PATCH 25/57] Update README.md --- maxima-lib/src/rtm/proto/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maxima-lib/src/rtm/proto/README.md b/maxima-lib/src/rtm/proto/README.md index db23b0cc..db4f6be2 100644 --- a/maxima-lib/src/rtm/proto/README.md +++ b/maxima-lib/src/rtm/proto/README.md @@ -1,6 +1,6 @@ # RTM Protocol -These are scraped from the `igoMain-settings-social` webpack chunk of EA Desktop's frontend using Qt's remote debugger. +These are scraped from the webpack chunks of EA Desktop's frontend using Qt's remote debugger. A useful way of testing these protos and figuring out new ones is passing a captured message into protoc's `--decode_raw` mode. -EADP stands for EA Digital Platform. +EADP stands for EA Digital Platform. Antelope is the name of the RTM protocol. From 2a80188568d0763ca9866eb5ba7c95c59c1a80c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:22:18 +0200 Subject: [PATCH 26/57] Update requests.proto --- maxima-lib/src/rtm/proto/requests.proto | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/maxima-lib/src/rtm/proto/requests.proto b/maxima-lib/src/rtm/proto/requests.proto index f721de70..982634cf 100644 --- a/maxima-lib/src/rtm/proto/requests.proto +++ b/maxima-lib/src/rtm/proto/requests.proto @@ -11,12 +11,21 @@ message ReconnectRequestV1 { string sessionKey = 1; } +message LoginRequest { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; +} + message LoginRequestV1 { string token = 1; bool reconnect = 2; bool heartbeat = 3; } +message LogoutRequest { +} + message LoginRequestV2 { string token = 1; bool reconnect = 2; @@ -45,6 +54,39 @@ message PresenceUpdateV1 { RichPresenceV1 richPresence = 4; } +message PublishTextRequest { + string channelId = 1; + string text = 2; + com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 3; + int32 index = 4; + string metadata = 5; + com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 6; + com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 7; + repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatProperties = 8; +} + +message PublishBinaryRequest { + string channelId = 1; + bytes blob = 2; + string metadata = 3; +} + +message SubscribeRequest { + string channelId = 1; +} + +message UnsubscribeRequest { + string channelId = 1; +} + +message HistoryRequest { + string channelId = 1; + string timestamp = 2; + int32 recordCount = 3; + string primaryFilterType = 4; + string secondaryFilterType = 5; +} + message HeartbeatV1 {} message SessionCleanupV1 { From 12cd2be0281be47928d086c549848957969f0ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:22:46 +0200 Subject: [PATCH 27/57] Update responses.proto --- maxima-lib/src/rtm/proto/responses.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/maxima-lib/src/rtm/proto/responses.proto b/maxima-lib/src/rtm/proto/responses.proto index f1e24c90..ab4feb88 100644 --- a/maxima-lib/src/rtm/proto/responses.proto +++ b/maxima-lib/src/rtm/proto/responses.proto @@ -50,3 +50,4 @@ message PresenceV1 { optional string clientVersion = 9; optional PlatformV1 platform = 10; } + From e827e0898ad14ccec645e33db9ff9b21de168468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:24:00 +0200 Subject: [PATCH 28/57] Add CommunicationType and ChannelMessageType enums --- maxima-lib/src/rtm/proto/common.proto | 105 ++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/maxima-lib/src/rtm/proto/common.proto b/maxima-lib/src/rtm/proto/common.proto index 914ff4d0..a8fc13f3 100644 --- a/maxima-lib/src/rtm/proto/common.proto +++ b/maxima-lib/src/rtm/proto/common.proto @@ -39,6 +39,43 @@ enum RichPresenceType { BROADCASTING = 3; } +enum CommunicationType { + NO_COMMUNICATION_TYPE = 0; + LOGIN_REQUEST = 1; + LOGIN_RESPONSE = 2; + PUBLISH_TEXT_REQUEST = 3; + PUBLISH_BINARY_REQUEST = 4; + PUBLISH_RESPONSE = 5; + CHANNEL_MESSAGE = 6; + SUBSCRIBE_REQUEST = 7; + SUBSCRIBE_RESPONSE = 8; + UNSUBSCRIBE_REQUEST = 9; + UNSUBSCRIBE_RESPONSE = 10; + HISTORY_REQUEST = 11; + HISTORY_RESPONSE = 12; + LOGOUT_REQUEST = 13; +} + +enum ChannelMessageType { + NO_CHANNEL_MESSAGE_TYPE = 0; + TEXT_MESSAGE = 1; + BINARY_MESSAGE = 2; + GROUP_MEMBERSHIP_CHANGE = 3; + CHAT_CONNECTED = 4; + CHAT_DISCONNECTED = 5; + CHAT_LEFT = 6; + CUSTOM_MESSAGE = 7; + CHAT_USER_MUTED = 8; + CHAT_USER_UNMUTED = 9; + CHANNEL_MEMBERSHIP_CHANGE = 10; + STICKY_MESSAGE_CHANGED = 11; + CHAT_TYPING_EVENT = 12; + WORLD_CHAT_MEMBERSHIP_CHANGE = 13; + GROUP_MEMBER_ATTRIBUTE_CHANGE = 14; + GROUP_ATTRIBUTE_CHANGE = 15; + GROUP_MEMBER_ROLE_CHANGE = 16; +} + message PresenceV1 { string personaId = 1; string status = 2; @@ -81,13 +118,71 @@ message RichPresenceV1 { string customRichPresenceData = 9; } +message Header { + CommunicationType type = 1; + string requestId = 2; +} + message Player { string playerId = 1; - string productId = 2; + string productId = 4; +} + +message Channel { + string channelId = 1; + string channelName = 2; + com.ea.eadp.antelope.rtm.protocol.ChannelType type = 3; + bool crossGame = 4; +} + +message ChannelMessage { + oneof content { + TextMessage textMessage = 5; + BinaryMessage binaryMessage = 6; + GroupMembershipChange membershipChange = 7; + com.ea.eadp.antelope.rtm.protocol.ChatConnectedV1 chatConnected = 8; + com.ea.eadp.antelope.rtm.protocol.ChatDisconnectedV1 chatDisconnected = 9; + com.ea.eadp.antelope.rtm.protocol.ChatLeftV1 chatLeft = 10; + com.ea.eadp.antelope.rtm.protocol.CustomMessage customMessage = 11; + com.ea.eadp.antelope.rtm.protocol.ChatUserMutedV1 chatUserMuted = 12; + com.ea.eadp.antelope.rtm.protocol.ChatUserUnmutedV1 chatUserUnmuted = 13; + com.ea.eadp.antelope.rtm.protocol.ChannelMembershipChangeV1 channelMembershipChange = 14; + com.ea.eadp.antelope.rtm.protocol.StickyMessageChangedV1 stickyMessageChanged = 15; + com.ea.eadp.antelope.rtm.protocol.ChatTypingEventV1 chatTypingEvent = 16; + com.ea.eadp.antelope.rtm.protocol.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; + com.ea.eadp.antelope.rtm.protocol.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; + com.ea.eadp.antelope.rtm.protocol.GroupAttributeChangeV1 groupAttributeChange = 19; + com.ea.eadp.antelope.rtm.protocol.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; + } + string messageId = 1; + string channelId = 2; + string timestamp = 3; + ChannelMessageType type = 4; +} + +message TextMessage { + string fromPersonaId = 1; + string fromDisplayName = 2; + string text = 3; + string originalText = 4; + Player fromPlayer = 5; + string fromNickName = 6; + string fromPlatformDisplayName = 7; + string fromPlatformNickName = 8; + string metadata = 9; + com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 10; + com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 11; + com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 12; + repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatPropertyBag = 13; } -message PlayerInfo { - Player player = 1; - string displayName = 2; - string nickName = 3; +message BinaryMessage { + string fromPersonaId = 1; + string fromDisplayName = 2; + bytes blob = 3; + Player fromPlayer = 4; + string fromNickName = 5; + string fromPlatformDisplayName = 6; + string fromPlatformNickName = 7; + string metadata = 8; } From 3ff815a82db37b53b422bcb0cba0c6c2fb94743c Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:30:57 +0200 Subject: [PATCH 29/57] switch to Antelope protocol based on last RTM proto findings, keep dependencies updates --- Cargo.lock | 65 ++-- Cargo.toml | 2 +- maxima-lib/Cargo.toml | 36 +-- maxima-lib/src/content/zlib.rs | 2 +- maxima-lib/src/core/service_layer.rs | 1 + maxima-lib/src/lsx/connection.rs | 4 +- maxima-lib/src/rtm/proto/chat.proto | 1 + maxima-lib/src/rtm/proto/common.proto | 304 +++++++++--------- maxima-lib/src/rtm/proto/config.proto | 15 + maxima-lib/src/rtm/proto/enum.proto | 23 ++ maxima-lib/src/rtm/proto/error.proto | 41 +++ maxima-lib/src/rtm/proto/moderation.proto | 16 + maxima-lib/src/rtm/proto/notification.proto | 36 +++ maxima-lib/src/rtm/proto/player.proto | 14 + maxima-lib/src/rtm/proto/point_to_point.proto | 28 ++ maxima-lib/src/rtm/proto/preference.proto | 21 ++ maxima-lib/src/rtm/proto/presence.proto | 97 ++++++ maxima-lib/src/rtm/proto/requests.proto | 94 ------ maxima-lib/src/rtm/proto/responses.proto | 53 --- maxima-lib/src/rtm/proto/rtm.proto | 36 ++- maxima-lib/src/rtm/proto/social.proto | 252 +++++++++++++++ maxima-tui/Cargo.toml | 2 +- maxima-ui/src/main.rs | 2 +- maxima-ui/src/views/game_view.rs | 26 +- 24 files changed, 792 insertions(+), 379 deletions(-) create mode 100644 maxima-lib/src/rtm/proto/config.proto create mode 100644 maxima-lib/src/rtm/proto/enum.proto create mode 100644 maxima-lib/src/rtm/proto/error.proto create mode 100644 maxima-lib/src/rtm/proto/moderation.proto create mode 100644 maxima-lib/src/rtm/proto/notification.proto create mode 100644 maxima-lib/src/rtm/proto/player.proto create mode 100644 maxima-lib/src/rtm/proto/point_to_point.proto create mode 100644 maxima-lib/src/rtm/proto/preference.proto create mode 100644 maxima-lib/src/rtm/proto/presence.proto delete mode 100644 maxima-lib/src/rtm/proto/requests.proto delete mode 100644 maxima-lib/src/rtm/proto/responses.proto create mode 100644 maxima-lib/src/rtm/proto/social.proto diff --git a/Cargo.lock b/Cargo.lock index 10a51165..e34c3f87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,9 +442,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -1560,13 +1560,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3206,7 +3206,7 @@ dependencies = [ "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.9.0", + "redox_syscall 0.9.1", ] [[package]] @@ -3474,7 +3474,7 @@ dependencies = [ "tokio-rustls", "tokio-stream", "tokio-util", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "ureq", "url", "uuid", @@ -5311,9 +5311,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e" dependencies = [ "bitflags 2.13.1", ] @@ -6057,34 +6057,35 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sval" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d" +checksum = "ba3a370f3cd0422964fd9a19b6516f048527738e6ec50b1b0ff79b460b468390" [[package]] name = "sval_buffer" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a" +checksum = "111e6e2dab25782acb41b281263f5f60d6f56a2ba0b3b076187ad23a1552544d" dependencies = [ "sval", "sval_ref", + "zerocopy", ] [[package]] name = "sval_dynamic" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6" +checksum = "590ac4ebd299740eac453162302a494e6d5b3be243feab8bf07d0d4331b6b2b3" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a" +checksum = "4b964d6d917267355d7f343c515b908586e5b4aeeada328738f703452f9412d6" dependencies = [ "itoa", "ryu", @@ -6093,9 +6094,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12" +checksum = "dea35e4d6b997acc7dc4786ab782f6a6c5000efe4ae93a2db89cf350775fe5fe" dependencies = [ "itoa", "ryu", @@ -6104,9 +6105,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291" +checksum = "a73195ebd4b3e5866db2e1b75f3f383657ad5abc600c646d69b4f064fee89154" dependencies = [ "sval", "sval_buffer", @@ -6115,18 +6116,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb" +checksum = "c0ef28df9d586bfd15115286651337cb5645f8c203160d22d2d1a20cf13c428c" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5" +checksum = "d72fbe6537e88878f10237bde71ae4a1b65b2bf54bdf274abc566fc696334c92" dependencies = [ "serde_core", "sval", @@ -6546,9 +6547,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -6582,9 +6583,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] @@ -7224,15 +7225,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +checksum = "ef62a3d5f7b2411119a11b6f62570dbff91d7105e011a20fb83fbf8f5761c40f" dependencies = [ - "core-foundation 0.10.1", "jni", "log", "ndk-context", "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "url", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index 9da97797..f491d1a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ [workspace.dependencies] # Common Workspace Dependencies for all Maxima libs -tokio = { version = "1.53.0", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } +tokio = { version = "1.53.1", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 1ab63758..3ae53a5e 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -31,16 +31,16 @@ enable-ansi-support = "0.3.1" chrono = "0.4.45" regex = { workspace = true } directories = { workspace = true } -open = "5.3.6" -ureq = { version = "3.3.0", features = ["_tls"] } -sysinfo = "0.39.5" +open = "5.4.0" +ureq = { version = "3.3.0", features = ["rustls"] } +sysinfo = "0.39.6" strum_macros = { workspace = true } sha2 = "0.11.0" tar = "0.4.46" miniz_oxide = { version = "0.9.1", features = ["serde"] } xz2 = "0.1.7" zstd = "0.13.3" -toml = "1.1.2" +toml = "1.1.4" bytebuffer = "2.3.0" derive-getters = "0.5.0" derive_builder = "0.20.2" @@ -51,28 +51,28 @@ ring = "0.17.14" futures = { workspace = true } prost = "0.14.4" encoding_rs = "0.8.35" -async-compression = { version = "0.4.42", features = ["tokio", "deflate"] } -tokio-util = { version = "0.7.18", features = ["io", "compat"] } -uuid = "1.23.4" +async-compression = { version = "0.4.43", features = ["tokio", "deflate"] } +tokio-util = { version = "0.7.19", features = ["io", "compat"] } +uuid = "1.24.0" tokio-rustls = "0.26.4" -rustls = "0.23.41" -webpki-roots = "1.0.8" -aes = "0.9.1" +rustls = "0.23.42" +webpki-roots = "1.0.9" +aes = "0.9.2" ecb = { version = "0.2.0", features = ["alloc", "block-padding", "zeroize"] } cbc = { version = "0.2.1", features = ["alloc", "block-padding", "zeroize"] } -glob = "0.3.3" -md5 = "0.8.0" -bytes = "1.12.0" -async-trait = "0.1.89" +glob = "0.3.4" +md5 = "0.8.1" +bytes = "1.12.1" +async-trait = "0.1.91" crc32fast = "1.5.0" pelite = "0.10.0" gethostname = "1.1.0" thiserror = { workspace = true } url = { workspace = true } http = "1.4.2" -generic-array = "1.4.3" -rustls-pki-types = "1.15.0" -tokio-stream = "0.1.18" +generic-array = "1.4.4" +rustls-pki-types = "1.15.1" +tokio-stream = "0.1.19" sha1 = "0.11.0" libz-sys = "1.1.29" @@ -102,7 +102,7 @@ widestring = "1.2.1" wmi = "0.18.4" [target.'cfg(target_os = "macos")'.dependencies] -smbios-lib = "0.9.2" +smbios-lib = "0.9.4" [build-dependencies] prost-build = "0.14.4" diff --git a/maxima-lib/src/content/zlib.rs b/maxima-lib/src/content/zlib.rs index bc4fcd24..7e342fe4 100644 --- a/maxima-lib/src/content/zlib.rs +++ b/maxima-lib/src/content/zlib.rs @@ -3,7 +3,7 @@ use log::error; use miniz_oxide::inflate::stream::InflateState; use std::{mem::size_of, ptr}; -const Z_MAGIC: u32 = u32::from_be_bytes(*b"ZSTB"); // new format, not zlib's internal layout +const Z_MAGIC: u32 = u32::from_be_bytes(*b"ZSTB"); /// Serialize an InflateState snapshot into a buffer, /// prefixed with a magic number and the length of the serialized data. diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index 28ad301e..ce8b0f21 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -533,6 +533,7 @@ service_layer_enum!(OwnershipMethod, { SteamSubscription, Epic, EpicVault, + // Might even have Amazon Luna as "Luna" but since i've never seen any request like it yet... }); service_layer_enum!(OwnershipStatus, { diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index d7a55dc9..4a3d5719 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -78,8 +78,8 @@ pub enum LSXConnectionError { const CORE_SENDER: &str = "EALS"; const CHALLENGE_BUILD: &str = "release"; -//const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; // Need to figure out how to generate this -const CHALLENGE_VERSION: &str = "10,5,64,37936"; +//const CHALLENGE_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; +const CHALLENGE_VERSION: &str = "10,5,64,37936"; // is it Origin-related (this is the last version) or EA Desktop (would be way above, needs checkings) static LSX_PATTERN: LazyLock = LazyLock::new(|| Regex::new(r".*?").expect("LSX pattern regex should be valid")); diff --git a/maxima-lib/src/rtm/proto/chat.proto b/maxima-lib/src/rtm/proto/chat.proto index 1c99ffb4..879d726f 100644 --- a/maxima-lib/src/rtm/proto/chat.proto +++ b/maxima-lib/src/rtm/proto/chat.proto @@ -1,4 +1,5 @@ syntax = "proto3"; + package com.ea.eadp.antelope.rtm.protocol; enum TypingEventType { diff --git a/maxima-lib/src/rtm/proto/common.proto b/maxima-lib/src/rtm/proto/common.proto index a8fc13f3..ae1b74ac 100644 --- a/maxima-lib/src/rtm/proto/common.proto +++ b/maxima-lib/src/rtm/proto/common.proto @@ -1,18 +1,33 @@ syntax = "proto3"; + package com.ea.eadp.antelope.rtm.protocol; -enum PlatformV1 { - NONE_PLATFORM = 1; - UNKNOWN_PLATFORM = 1; - MOBILE = 2; - PC = 3; - WEB = 4; - XBOXLIVE = 5; - PSN = 6; - SWITCH = 7; - STEAM = 8; - XBSK = 9; - PS5 = 10; +enum ChannelType { + NO_CHANNEL_TYPE = 0; + EADP_GROUP_SERVICE = 1; + INVITE_CHANNEL = 2; + WORLD_CHAT = 3; + GAME = 4; +} + +enum MutedByV1 { + NO_MUTE = 0; + ADMIN_MUTED = 1; + USER_MUTED = 2; +} + +enum ContentTypeValue { + NO_CONTENT = 0; + GIFT = 1; + GAME_INVITE = 2; + OTHER = 3; +} + +enum MessageType { + NO_MESSAGE = 0; + REGULAR_MESSAGE = 1; + STICKY_MESSAGE = 2; + QUICK_MESSAGE = 3; } enum UserType { @@ -21,168 +36,145 @@ enum UserType { NUCLEUS = 2; } -enum BasicPresenceType { - NONE_PRESENCE = 0; - UNKNOWN_PRESENCE = 1; - OFFLINE = 2; - ONLINE = 3; - DND = 4; - AWAY = 5; - INVISIBLE = 6; - CUSTOM = 7; -} - -enum RichPresenceType { - NONE_RICH_PRESENCE = 0; - UNKNOWN_RICH_PRESENCE = 1; - LFG = 2; - BROADCASTING = 3; -} - -enum CommunicationType { - NO_COMMUNICATION_TYPE = 0; - LOGIN_REQUEST = 1; - LOGIN_RESPONSE = 2; - PUBLISH_TEXT_REQUEST = 3; - PUBLISH_BINARY_REQUEST = 4; - PUBLISH_RESPONSE = 5; - CHANNEL_MESSAGE = 6; - SUBSCRIBE_REQUEST = 7; - SUBSCRIBE_RESPONSE = 8; - UNSUBSCRIBE_REQUEST = 9; - UNSUBSCRIBE_RESPONSE = 10; - HISTORY_REQUEST = 11; - HISTORY_RESPONSE = 12; - LOGOUT_REQUEST = 13; -} - -enum ChannelMessageType { - NO_CHANNEL_MESSAGE_TYPE = 0; - TEXT_MESSAGE = 1; - BINARY_MESSAGE = 2; - GROUP_MEMBERSHIP_CHANGE = 3; - CHAT_CONNECTED = 4; - CHAT_DISCONNECTED = 5; - CHAT_LEFT = 6; - CUSTOM_MESSAGE = 7; - CHAT_USER_MUTED = 8; - CHAT_USER_UNMUTED = 9; - CHANNEL_MEMBERSHIP_CHANGE = 10; - STICKY_MESSAGE_CHANGED = 11; - CHAT_TYPING_EVENT = 12; - WORLD_CHAT_MEMBERSHIP_CHANGE = 13; - GROUP_MEMBER_ATTRIBUTE_CHANGE = 14; - GROUP_ATTRIBUTE_CHANGE = 15; - GROUP_MEMBER_ROLE_CHANGE = 16; -} - -message PresenceV1 { +message PersonaV1 { string personaId = 1; - string status = 2; - string timestamp = 3; - Player player = 4; - BasicPresenceType basicPresenceType = 5; - string userDefinedPresence = 6; - RichPresenceV1 richPresence = 7; - string sessionKey = 8; - string clientVersion = 9; - PlatformV1 platform = 10; + string displayName = 2; + string nickName = 3; } -message PresenceUpdateErrorV1 { - string errorCode = 1; - string errorMessage = 2; +message SuccessV1 { + oneof body { + LoginV2Success loginV2Success = 1; + ChatInitiateSuccessV1 chatInitiateSuccess = 2; + LoginV3Response loginV3Response = 3; + } } -message PresenceSubscriptionErrorV1 { - string personaId = 1; - string errorCode = 2; - string errorMessage = 3; - Player player = 4; +message LoginV2Success { + string sessionKey = 1; } -message PresenceSubscriptionAllFriendsErrorV1 { - string errorCode = 1; - string errorMessage = 2; +message ChatInitiateSuccessV1 { + ChannelV1 channel = 1; } -message RichPresenceV1 { - string game = 1; - PlatformV1 platform = 2; - string gameModeType = 3; - string gameMode = 4; - string gameSessionData = 5; - RichPresenceType richPresenceType = 6; - string startTimestamp = 7; - string endTimestamp = 8; - string customRichPresenceData = 9; +message MutedSetV1 { + repeated MutedByV1 mutedBy = 1; } -message Header { - CommunicationType type = 1; - string requestId = 2; +message ChannelV1 { + string channelId = 1; + string channelName = 2; + ChannelType type = 3; + int32 unreadMessageCount = 4; + TextMessageV1 lastMessageInfo = 5; + string lastReadTimestamp = 6; + string customProperties = 7; + bool crossGame = 8; } -message Player { - string playerId = 1; - string productId = 4; +message ChannelMuteListV1 { + string channelId = 1; + repeated PersonaV1 mutedUsers = 2; + repeated MutedSetV1 mutedBy = 3; + repeated Player mutedPlayers = 4; } -message Channel { - string channelId = 1; - string channelName = 2; - com.ea.eadp.antelope.rtm.protocol.ChannelType type = 3; - bool crossGame = 4; -} - -message ChannelMessage { - oneof content { - TextMessage textMessage = 5; - BinaryMessage binaryMessage = 6; - GroupMembershipChange membershipChange = 7; - com.ea.eadp.antelope.rtm.protocol.ChatConnectedV1 chatConnected = 8; - com.ea.eadp.antelope.rtm.protocol.ChatDisconnectedV1 chatDisconnected = 9; - com.ea.eadp.antelope.rtm.protocol.ChatLeftV1 chatLeft = 10; - com.ea.eadp.antelope.rtm.protocol.CustomMessage customMessage = 11; - com.ea.eadp.antelope.rtm.protocol.ChatUserMutedV1 chatUserMuted = 12; - com.ea.eadp.antelope.rtm.protocol.ChatUserUnmutedV1 chatUserUnmuted = 13; - com.ea.eadp.antelope.rtm.protocol.ChannelMembershipChangeV1 channelMembershipChange = 14; - com.ea.eadp.antelope.rtm.protocol.StickyMessageChangedV1 stickyMessageChanged = 15; - com.ea.eadp.antelope.rtm.protocol.ChatTypingEventV1 chatTypingEvent = 16; - com.ea.eadp.antelope.rtm.protocol.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; - com.ea.eadp.antelope.rtm.protocol.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; - com.ea.eadp.antelope.rtm.protocol.GroupAttributeChangeV1 groupAttributeChange = 19; - com.ea.eadp.antelope.rtm.protocol.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; - } - string messageId = 1; - string channelId = 2; +message HeartbeatV1 { +} + +message StickyMessageV1 { + string stickyAuthorPersonaId = 1; + string stickyAuthorDisplayName = 2; string timestamp = 3; - ChannelMessageType type = 4; + TextMessageV1 textMessage = 4; + string messageId = 5; + string stickyAuthorNickName = 6; + string stickyAuthorPlatformDisplayName = 7; + string stickyAuthorPlatformNickName = 8; } -message TextMessage { +message TextMessageV1 { string fromPersonaId = 1; string fromDisplayName = 2; - string text = 3; - string originalText = 4; - Player fromPlayer = 5; - string fromNickName = 6; - string fromPlatformDisplayName = 7; - string fromPlatformNickName = 8; - string metadata = 9; - com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 10; - com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 11; - com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 12; - repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatPropertyBag = 13; -} - -message BinaryMessage { - string fromPersonaId = 1; - string fromDisplayName = 2; - bytes blob = 3; - Player fromPlayer = 4; - string fromNickName = 5; - string fromPlatformDisplayName = 6; - string fromPlatformNickName = 7; - string metadata = 8; + string timestamp = 3; + string text = 4; + string originalText = 5; + Player player = 6; + string fromNickName = 7; + string fromPlatformDisplayName = 8; + string fromPlatformNickName = 9; + string metadata = 10; + MessageType messageType = 11; + StringId textMessageId = 12; + MessageContext messageContext = 13; + repeated ChatPropertyBagEntry chatPropertyBag = 14; +} + +message ChatPropertyBagEntry { + string key = 1; + ChatPropertyValue value = 2; +} + +message ChatPropertyValue { + oneof value { + string stringValue = 1; + int64 integerValue = 2; + double floatValue = 3; + ChatPropertyTimestamp chatPropertyTimestampValue = 4; + ChatPropertyDuration chatPropertyDurationValue = 5; + } +} + +message ChatPropertyTimestamp { + int64 seconds = 1; + int32 nanos = 2; +} + +message ChatPropertyDuration { + int64 seconds = 1; + int32 nanos = 2; +} + +message MessageContext { + eadp.common.v1.PlayerNetworkId senderPlayerNetworkId = 1; + eadp.common.v1.DevicePlatformId senderDevicePlatformId = 2; + eadp.common.v1.ProductId senderProductId = 3; + eadp.common.v1.PlayerNetworkId receiverPlayerNetworkId = 4; + eadp.common.v1.DevicePlatformId receiverDevicePlatformId = 5; + ContentType contentType = 6; +} + +message ContentType { + oneof body { + ContentTypeValue type = 1; + string customContentType = 2; + } +} + +message LoginV3Response { + string sessionKey = 1; + repeated SessionV1 connectedSessions = 2; +} + +message SessionV1 { + string sessionKey = 1; + PlatformV1 platform = 2; + string clientVersion = 3; + string timestamp = 4; +} + +message SessionRequestV1 { } + +message SessionResponseV1 { + repeated SessionV1 connectedSessions = 1; +} + +message SessionCleanupV1 { + string sessionKey = 1; +} + +message StringId { + string id = 1; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/config.proto b/maxima-lib/src/rtm/proto/config.proto new file mode 100644 index 00000000..2280e8b7 --- /dev/null +++ b/maxima-lib/src/rtm/proto/config.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +message ServerConfigRequestV1 { +} + +message ServerConfigV1 { + RateLimitConfigV1 rateLimitConfig = 1; +} + +message RateLimitConfigV1 { + double channelUpdateLimit = 1; + double typingEventLimit = 2; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/enum.proto b/maxima-lib/src/rtm/proto/enum.proto new file mode 100644 index 00000000..7e75932f --- /dev/null +++ b/maxima-lib/src/rtm/proto/enum.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +enum ChannelMessageType { + NO_CHANNEL_MESSAGE_TYPE = 0; + TEXT_MESSAGE = 1; + BINARY_MESSAGE = 2; + GROUP_MEMBERSHIP_CHANGE = 3; + CHAT_CONNECTED = 4; + CHAT_DISCONNECTED = 5; + CHAT_LEFT = 6; + CUSTOM_MESSAGE = 7; + CHAT_USER_MUTED = 8; + CHAT_USER_UNMUTED = 9; + CHANNEL_MEMBERSHIP_CHANGE = 10; + STICKY_MESSAGE_CHANGED = 11; + CHAT_TYPING_EVENT = 12; + WORLD_CHAT_MEMBERSHIP_CHANGE = 13; + GROUP_MEMBER_ATTRIBUTE_CHANGE = 14; + GROUP_ATTRIBUTE_CHANGE = 15; + GROUP_MEMBER_ROLE_CHANGE = 16; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/error.proto b/maxima-lib/src/rtm/proto/error.proto new file mode 100644 index 00000000..c97fa7e3 --- /dev/null +++ b/maxima-lib/src/rtm/proto/error.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +message ErrorV1 { + oneof body { + ChatMembersRequestErrorV1 chatMembersRequestError = 3; + ValidationErrorV1 validationError = 4; + AssignErrorV1 assignError = 5; + ChatTypingEventRequestErrorV1 chatTypingEventRequestError = 6; + LoginErrorV1 loginError = 7; + } + string errorCode = 1; + string errorMessage = 2; +} + +message ChatMembersRequestErrorV1 { + string channelId = 1; +} + +message ValidationErrorV1 { + repeated FieldViolationV1 fieldViolation = 1; +} + +message FieldViolationV1 { + string fieldName = 1; + string violationMessage = 2; +} + +message AssignErrorV1 { + string worldName = 1; + int32 shardIndex = 2; +} + +message ChatTypingEventRequestErrorV1 { + string channelId = 1; +} + +message LoginErrorV1 { + repeated SessionV1 connectedSessions = 1; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/moderation.proto b/maxima-lib/src/rtm/proto/moderation.proto new file mode 100644 index 00000000..9f7b3078 --- /dev/null +++ b/maxima-lib/src/rtm/proto/moderation.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +enum ModerationRoleType { + NO_MODERATION_ROLE = 0; + MODERATION_ROLE_VOLUNTEER = 1; + MODERATION_ROLE_EMPLOYEE = 2; +} + +message GetRolesRequest { +} + +message GetRolesResponse { + repeated ModerationRoleType roles = 1; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/notification.proto b/maxima-lib/src/rtm/proto/notification.proto new file mode 100644 index 00000000..9a066189 --- /dev/null +++ b/maxima-lib/src/rtm/proto/notification.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +enum SessionNotificationTypeV1 { + NOTHING = 0; + DEFAULT = 1; + FORCE_DISCONNECT = 2; + USER_LOGGED_IN = 3; + USER_LOGGED_OUT = 4; + CHAT_CHANNEL_UPDATE = 5; +} + +message NotificationV1 { + string type = 1; + string payload = 2; +} + +message UserMembershipChangeV1 { + ChannelMembershipChangeType type = 1; + ChannelV1 channel = 2; +} + +message SessionNotificationV1 { + SessionNotificationTypeV1 type = 1; + string message = 2; + PlatformV1 platform = 3; + int32 sessionLimitRemaining = 4; + SessionV1 connectedSessions = 5; + ChatChannelUpdateV1 chatChannelUpdate = 6; +} + +message WorldChatNotificationV1 { + string sessionKey = 1; + string worldName = 2; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/player.proto b/maxima-lib/src/rtm/proto/player.proto new file mode 100644 index 00000000..a5660cbf --- /dev/null +++ b/maxima-lib/src/rtm/proto/player.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +message Player { + string playerId = 1; + string productId = 2; +} + +message PlayerInfo { + Player player = 1; + string displayName = 2; + string nickName = 3; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/point_to_point.proto b/maxima-lib/src/rtm/proto/point_to_point.proto new file mode 100644 index 00000000..02c3ade9 --- /dev/null +++ b/maxima-lib/src/rtm/proto/point_to_point.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +enum Domain { + NO_DOMAIN = 0; + USER = 1; +} + +message AddressV1 { + Domain domain = 1; + string id = 2; + Player player = 3; +} + +message CustomMessage { + string type = 1; + bytes payload = 2; + string payloadType = 3; +} + +message PointToPointMessageV1 { + oneof messageContent { + CustomMessage customMessage = 3; + } + AddressV1 from = 1; + AddressV1 to = 2; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/preference.proto b/maxima-lib/src/rtm/proto/preference.proto new file mode 100644 index 00000000..7d989562 --- /dev/null +++ b/maxima-lib/src/rtm/proto/preference.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +message UpdatePreferenceRequestV1 { + oneof body { + TranslationPreferenceV1 translation = 1; + } +} + +message GetPreferenceRequestV1 { +} + +message GetPreferenceResponseV1 { + TranslationPreferenceV1 translationPreference = 1; +} + +message TranslationPreferenceV1 { + bool autoTranslate = 1; + string language = 2; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/presence.proto b/maxima-lib/src/rtm/proto/presence.proto new file mode 100644 index 00000000..2f01ce43 --- /dev/null +++ b/maxima-lib/src/rtm/proto/presence.proto @@ -0,0 +1,97 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.rtm.protocol; + +enum BasicPresenceType { + NONE_PRESENCE = 0; + UNKNOWN_PRESENCE = 1; + OFFLINE = 2; + ONLINE = 3; + DND = 4; + AWAY = 5; + INVISIBLE = 6; + CUSTOM = 7; +} + +enum RichPresenceType { + NONE_RICH_PRESENCE = 0; + UNKNOWN_RICH_PRESENCE = 1; + LFG = 2; + BROADCASTING = 3; +} + +enum PlatformV1 { + NONE_PLATFORM = 0; + UNKNOWN_PLATFORM = 1; + MOBILE = 2; + PC = 3; + WEB = 4; + XBOXLIVE = 5; + PSN = 6; + SWITCH = 7; + STEAM = 8; + XBSK = 9; + PS5 = 10; +} + +message PresenceSubscribeV1 { + repeated string personaId = 1; + repeated Player players = 2; +} + +message PresenceSubscribeAllFriendsV1 { +} + +message PresenceUnsubscribeV1 { + repeated string personaId = 1; + repeated Player players = 2; +} + +message PresenceUpdateV1 { + string status = 1; + BasicPresenceType basicPresenceType = 2; + string userDefinedPresence = 3; + RichPresenceV1 richPresence = 4; +} + +message PresenceV1 { + string personaId = 1; + string status = 2; + string timestamp = 3; + Player player = 4; + BasicPresenceType basicPresenceType = 5; + string userDefinedPresence = 6; + RichPresenceV1 richPresence = 7; + string sessionKey = 8; + string clientVersion = 9; + PlatformV1 platform = 10; +} + +message PresenceUpdateErrorV1 { + string errorCode = 1; + string errorMessage = 2; +} + +message PresenceSubscriptionErrorV1 { + string personaId = 1; + string errorCode = 2; + string errorMessage = 3; + Player player = 4; +} + +message PresenceSubscriptionAllFriendsErrorV1 { + string errorCode = 1; + string errorMessage = 2; +} + +message RichPresenceV1 { + string game = 1; + PlatformV1 platform = 2; + string gameModeType = 3; + string gameMode = 4; + string gameSessionData = 5; + RichPresenceType richPresenceType = 6; + string startTimestamp = 7; + string endTimestamp = 8; + string customRichPresenceData = 9; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/requests.proto b/maxima-lib/src/rtm/proto/requests.proto deleted file mode 100644 index 982634cf..00000000 --- a/maxima-lib/src/rtm/proto/requests.proto +++ /dev/null @@ -1,94 +0,0 @@ -syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; - -import "common.proto"; - -message PresenceSubscribeV1 { - repeated Player players = 2; -} - -message ReconnectRequestV1 { - string sessionKey = 1; -} - -message LoginRequest { - string token = 1; - bool reconnect = 2; - bool heartbeat = 3; -} - -message LoginRequestV1 { - string token = 1; - bool reconnect = 2; - bool heartbeat = 3; -} - -message LogoutRequest { -} - -message LoginRequestV2 { - string token = 1; - bool reconnect = 2; - bool heartbeat = 3; - UserType userType = 4; - string productId = 5; - bool singleSessionForceLogout = 6; -} - -message LoginRequestV3 { - string token = 1; - bool reconnect = 2; - bool heartbeat = 3; - UserType userType = 4; - string productId = 5; - PlatformV1 platform = 6; - string clientVersion = 7; - string sessionKey = 8; - string forceDisconnectSessionKey = 9; -} - -message PresenceUpdateV1 { - string status = 1; - BasicPresenceType basicPresenceType = 2; - string userDefinedPresence = 3; - RichPresenceV1 richPresence = 4; -} - -message PublishTextRequest { - string channelId = 1; - string text = 2; - com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 3; - int32 index = 4; - string metadata = 5; - com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 6; - com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 7; - repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatProperties = 8; -} - -message PublishBinaryRequest { - string channelId = 1; - bytes blob = 2; - string metadata = 3; -} - -message SubscribeRequest { - string channelId = 1; -} - -message UnsubscribeRequest { - string channelId = 1; -} - -message HistoryRequest { - string channelId = 1; - string timestamp = 2; - int32 recordCount = 3; - string primaryFilterType = 4; - string secondaryFilterType = 5; -} - -message HeartbeatV1 {} - -message SessionCleanupV1 { - string sessionKey = 1; -} diff --git a/maxima-lib/src/rtm/proto/responses.proto b/maxima-lib/src/rtm/proto/responses.proto deleted file mode 100644 index ab4feb88..00000000 --- a/maxima-lib/src/rtm/proto/responses.proto +++ /dev/null @@ -1,53 +0,0 @@ -syntax = "proto3"; -package eadp.rtm; - -import "common.proto"; - -message SessionV1 { - string sessionKey = 1; - PlatformV1 platform = 2; - string clientVersion = 3; - string timestamp = 4; -} - -message LoginV3Response { - string sessionKey = 1; - repeated SessionV1 connectedSessions = 2; -} - -message SuccessV1 { - oneof body { - LoginV3Response loginV3Response = 3; - } -} - -message ErrorV1 { - string errorCode = 1; - string errorMessage = 2; - - oneof body { - LoginErrorV1 loginError = 7; - } -} - -message LoginErrorV1 { - repeated SessionV1 connectedSessions = 1; -} - -message PresenceV1 { - string personaId = 1; - - optional string status = 2; - optional string timestamp = 3; - - Player player = 4; - BasicPresenceType basicPresenceType = 5; - - optional string userDefinedPresence = 6; - optional RichPresenceV1 richPresence = 7; - - optional string sessionKey = 8; - optional string clientVersion = 9; - optional PlatformV1 platform = 10; -} - diff --git a/maxima-lib/src/rtm/proto/rtm.proto b/maxima-lib/src/rtm/proto/rtm.proto index e327f986..0ae809d5 100644 --- a/maxima-lib/src/rtm/proto/rtm.proto +++ b/maxima-lib/src/rtm/proto/rtm.proto @@ -1,10 +1,6 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; -import "common.proto"; -import "requests.proto"; -import "responses.proto"; -import "chat.proto"; +package com.ea.eadp.antelope.rtm.protocol; message Communication { oneof body { @@ -69,3 +65,33 @@ message CommunicationV1 { string requestId = 1; } +message ReconnectRequestV1 { + string sessionKey = 1; +} + +message LoginRequestV1 { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; +} + +message LoginRequestV2 { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; + UserType userType = 4; + string productId = 5; + bool singleSessionForceLogout = 6; +} + +message LoginRequestV3 { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; + UserType userType = 4; + string productId = 5; + PlatformV1 platform = 6; + string clientVersion = 7; + string sessionKey = 8; + string forceDisconnectSessionKey = 9; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/social.proto b/maxima-lib/src/rtm/proto/social.proto new file mode 100644 index 00000000..2095443b --- /dev/null +++ b/maxima-lib/src/rtm/proto/social.proto @@ -0,0 +1,252 @@ +syntax = "proto3"; + +package com.ea.eadp.antelope.protocol; + +enum CommunicationType { + NONE_COMMUNICATION_TYPE = 0; + LOGIN_REQUEST = 1; + LOGIN_RESPONSE = 2; + PUBLISH_TEXT_REQUEST = 3; + PUBLISH_BINARY_REQUEST = 4; + PUBLISH_RESPONSE = 5; + CHANNEL_MESSAGE = 6; + SUBSCRIBE_REQUEST = 7; + SUBSCRIBE_RESPONSE = 8; + UNSUBSCRIBE_REQUEST = 9; + UNSUBSCRIBE_RESPONSE = 10; + HISTORY_REQUEST = 11; + HISTORY_RESPONSE = 12; + LOGOUT_REQUEST = 13; +} + +enum ChannelMessageType { + NONE_TEXT_MESSAGE = 0; + TEXT_MESSAGE = 1; + BINARY_MESSAGE = 2; + GROUP_MEMBERSHIP_CHANGE = 3; + CHAT_CONNECTED = 4; + CHAT_DISCONNECTED = 5; + CHAT_LEFT = 6; + CUSTOM_MESSAGE = 7; + CHAT_USER_MUTED = 8; + CHAT_USER_UNMUTED = 9; + CHANNEL_MEMBERSHIP_CHANGE = 10; + STICKY_MESSAGE_CHANGED = 11; + CHAT_TYPING_EVENT = 12; + WORLD_CHAT_MEMBERSHIP_CHANGE = 13; + GROUP_MEMBER_ATTRIBUTE_CHANGE = 14; + GROUP_ATTRIBUTE_CHANGE = 15; + GROUP_MEMBER_ROLE_CHANGE = 16; +} + +enum GroupMembershipChangeType { + NO_CHANGE = 0; + LEAVE = 1; + JOIN = 2; + KICKED = 3; +} + +enum MutedBy { + NO_MUTE = 0; + ADMIN = 1; + USER = 2; +} + +message Communication { + oneof body { + LoginRequest loginRequest = 2; + LoginResponse loginResponse = 3; + PublishTextRequest publishTextRequest = 4; + PublishBinaryRequest publishBinaryRequest = 5; + PublishResponse publishResponse = 6; + ChannelMessage channelMessage = 7; + SubscribeRequest subscribeRequest = 8; + SubscribeResponse subscribeResponse = 9; + UnsubscribeRequest unsubscribeRequest = 10; + UnsubscribeResponse unsubscribeResponse = 11; + HistoryRequest historyRequest = 12; + HistoryResponse historyResponse = 13; + LogoutRequest logoutRequest = 14; + } + Header header = 1; +} + +message Header { + CommunicationType type = 1; + string requestId = 2; +} + +message Player { + string playerId = 1; + string productId = 4; +} + +message Channel { + string channelId = 1; + string channelName = 2; + com.ea.eadp.antelope.rtm.protocol.ChannelType type = 3; + bool crossGame = 4; +} + +message ChannelMessage { + oneof content { + TextMessage textMessage = 5; + BinaryMessage binaryMessage = 6; + GroupMembershipChange membershipChange = 7; + com.ea.eadp.antelope.rtm.protocol.ChatConnectedV1 chatConnected = 8; + com.ea.eadp.antelope.rtm.protocol.ChatDisconnectedV1 chatDisconnected = 9; + com.ea.eadp.antelope.rtm.protocol.ChatLeftV1 chatLeft = 10; + com.ea.eadp.antelope.rtm.protocol.CustomMessage customMessage = 11; + com.ea.eadp.antelope.rtm.protocol.ChatUserMutedV1 chatUserMuted = 12; + com.ea.eadp.antelope.rtm.protocol.ChatUserUnmutedV1 chatUserUnmuted = 13; + com.ea.eadp.antelope.rtm.protocol.ChannelMembershipChangeV1 channelMembershipChange = 14; + com.ea.eadp.antelope.rtm.protocol.StickyMessageChangedV1 stickyMessageChanged = 15; + com.ea.eadp.antelope.rtm.protocol.ChatTypingEventV1 chatTypingEvent = 16; + com.ea.eadp.antelope.rtm.protocol.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; + com.ea.eadp.antelope.rtm.protocol.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; + com.ea.eadp.antelope.rtm.protocol.GroupAttributeChangeV1 groupAttributeChange = 19; + com.ea.eadp.antelope.rtm.protocol.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; + } + string messageId = 1; + string channelId = 2; + string timestamp = 3; + ChannelMessageType type = 4; +} + +message TextMessage { + string fromPersonaId = 1; + string fromDisplayName = 2; + string text = 3; + string originalText = 4; + Player fromPlayer = 5; + string fromNickName = 6; + string fromPlatformDisplayName = 7; + string fromPlatformNickName = 8; + string metadata = 9; + com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 10; + com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 11; + com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 12; + repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatPropertyBag = 13; +} + +message BinaryMessage { + string fromPersonaId = 1; + string fromDisplayName = 2; + bytes blob = 3; + Player fromPlayer = 4; + string fromNickName = 5; + string fromPlatformDisplayName = 6; + string fromPlatformNickName = 7; + string metadata = 8; +} + +message GroupMembershipChange { + string targetPersonaId = 1; + string targetDisplayName = 2; + string initiatorPersonaId = 3; + string initiatorDisplayName = 4; + GroupMembershipChangeType type = 5; + Player targetPlayer = 6; + Player initiatingPLayer = 7; + string targetNickName = 8; + string initiatorNickName = 9; + string targetAttributes = 10; + string initiatorAttributes = 11; +} + +message MutedUser { + string personaId = 1; + repeated MutedBy mutedBy = 2; +} + +message ChannelMuteList { + string channelId = 1; + repeated MutedUser mutedUsers = 2; +} + +message MutedUserV1 { + string personaId = 1; + repeated com.ea.eadp.antelope.rtm.protocol.MutedByV1 mutedBy = 2; + Player player = 3; +} + +message LoginRequest { + string token = 1; + bool reconnect = 2; + bool heartbeat = 3; +} + +message LogoutRequest { +} + +message PublishTextRequest { + string channelId = 1; + string text = 2; + com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 3; + int32 index = 4; + string metadata = 5; + com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 6; + com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 7; + repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatProperties = 8; +} + +message PublishBinaryRequest { + string channelId = 1; + bytes blob = 2; + string metadata = 3; +} + +message SubscribeRequest { + string channelId = 1; +} + +message UnsubscribeRequest { + string channelId = 1; +} + +message HistoryRequest { + string channelId = 1; + string timestamp = 2; + int32 recordCount = 3; + string primaryFilterType = 4; + string secondaryFilterType = 5; +} + +message LoginResponse { + bool success = 1; + repeated Channel channels = 2; + repeated ChannelMuteList muteList = 3; + string errorCode = 4; + string errorMessage = 5; +} + +message PublishResponse { + bool success = 1; + string errorCode = 2; + string errorMessage = 3; + string messageId = 4; + string timestamp = 5; +} + +message SubscribeResponse { + string channelId = 1; + bool success = 2; + string errorCode = 3; + string errorMessage = 4; + repeated MutedUserV1 mutedUsers = 5; +} + +message UnsubscribeResponse { + string channelId = 1; + bool success = 2; + string errorCode = 3; + string errorMessage = 4; +} + +message HistoryResponse { + string channelId = 1; + bool success = 2; + repeated ChannelMessage messages = 3; + string errorCode = 4; + string errorMessage = 5; +} \ No newline at end of file diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index 12eaf9e3..59492b58 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -18,7 +18,7 @@ inquire = { workspace = true } ratatui = "0.30.2" strum = { workspace = true } color-eyre = "0.6.5" -tokio-stream = "0.1.18" +tokio-stream = "0.1.19" [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true } diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index 05135a27..c8c977de 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -540,7 +540,7 @@ fn custom_window_frame( }); // senpai ? could you draw all over my panel uwu - egui::CentralPanel::default().frame(egui::Frame::NONE).show_inside(ui, |ui| { + egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| { if !enabled { let warning_rect = Rect { min: ui.max_rect().min, diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index e67f88fb..9e94173e 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -292,26 +292,22 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { } if hero_vis_frac < 1.0 - /* && hero.is_some() */ { - // TODO: find a better solution - let mut mesh = Mesh::default(); - - let hero_tint = if ui.is_enabled() { + let full_tint = if ui.is_enabled() { Color32::from_black_alpha(20) } else { Color32::from_black_alpha(174) }; - mesh.colored_vertex( - hero_rect.left_bottom() - vec2(0.0, hero_rect.height() * hero_vis_frac), - hero_tint, - ); - mesh.colored_vertex( - hero_rect.right_bottom() - vec2(0.0, hero_rect.height() * hero_vis_frac), - hero_tint, - ); - mesh.colored_vertex(hero_rect.right_top(), hero_tint); - mesh.colored_vertex(hero_rect.left_top(), hero_tint); + let fade_tint = Color32::from_black_alpha(0); + + let bottom_y = hero_rect.left_bottom() - vec2(0.0, hero_rect.height() * hero_vis_frac); + + let mut mesh = Mesh::default(); + // bottom edge fades to transparent — reads as the hero "dissolving" + mesh.colored_vertex(bottom_y, fade_tint); + mesh.colored_vertex(bottom_y + vec2(hero_rect.width(), 0.0), fade_tint); + mesh.colored_vertex(hero_rect.right_top(), full_tint); + mesh.colored_vertex(hero_rect.left_top(), full_tint); mesh.add_triangle(0, 1, 2); mesh.add_triangle(0, 2, 3); ui.painter().add(Shape::mesh(mesh)); From dfece6c3682310f590bd6b1d900fcff7cec99444 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:06:39 +0200 Subject: [PATCH 30/57] switched to Antelope protocol properly, working base RTM --- Cargo.lock | 64 +++++++++---------- maxima-cli/src/main.rs | 6 +- maxima-lib/build.rs | 15 ++++- maxima-lib/src/rtm/client.rs | 59 +++++++++-------- maxima-lib/src/rtm/connection.rs | 34 ++++++---- maxima-lib/src/rtm/mod.rs | 2 +- .../src/rtm/proto/antelope_common.proto | 15 +++++ maxima-lib/src/rtm/proto/chat.proto | 8 ++- maxima-lib/src/rtm/proto/common.proto | 17 +++-- maxima-lib/src/rtm/proto/config.proto | 2 +- maxima-lib/src/rtm/proto/enum.proto | 2 +- maxima-lib/src/rtm/proto/error.proto | 4 +- maxima-lib/src/rtm/proto/moderation.proto | 2 +- maxima-lib/src/rtm/proto/notification.proto | 8 ++- maxima-lib/src/rtm/proto/player.proto | 2 +- maxima-lib/src/rtm/proto/point_to_point.proto | 4 +- maxima-lib/src/rtm/proto/preference.proto | 2 +- maxima-lib/src/rtm/proto/presence.proto | 4 +- maxima-lib/src/rtm/proto/rtm.proto | 12 +++- maxima-lib/src/rtm/proto/social.proto | 50 ++++++++------- maxima-ui/src/event_thread.rs | 6 +- 21 files changed, 199 insertions(+), 119 deletions(-) create mode 100644 maxima-lib/src/rtm/proto/antelope_common.proto diff --git a/Cargo.lock b/Cargo.lock index e34c3f87..238d6c5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -940,9 +940,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.4" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -950,9 +950,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream", "anstyle", @@ -2579,9 +2579,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2594,7 +2594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -2605,7 +2605,7 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", - "http 1.4.2", + "http 1.5.0", "http-body", "pin-project-lite", ] @@ -2633,9 +2633,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", "zeroize", @@ -2651,7 +2651,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.4.2", + "http 1.5.0", "http-body", "httparse", "itoa", @@ -2667,7 +2667,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.2", + "http 1.5.0", "hyper", "hyper-util", "rustls", @@ -2686,7 +2686,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body", "hyper", "ipnet", @@ -3439,7 +3439,7 @@ dependencies = [ "gethostname", "glob", "hex", - "http 1.4.2", + "http 1.5.0", "is_elevated", "libz-sys", "log", @@ -5375,7 +5375,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body", "http-body-util", "hyper", @@ -5512,9 +5512,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -6391,9 +6391,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -6491,13 +6491,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6622,7 +6622,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body", "http-body-util", "pin-project-lite", @@ -6801,7 +6801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ "base64 0.22.1", - "http 1.4.2", + "http 1.5.0", "httparse", "log", ] @@ -6896,9 +6896,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef73bfbaf3216cb59c205d7176bee1194e0d84348979da31f4a71fefe3c2054e" +checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -6906,9 +6906,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b92170db3db8a6354f12a5b7f13a5453928433e08fc46aee51eedfa8f7a28a1" +checksum = "417d6197dd0ee696783d6be4276ac6ea74b985e00024c85ccfb37aff4f2bed82" dependencies = [ "erased-serde", "serde_core", @@ -6917,9 +6917,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9832097ca044466ae3f1aa43a943d4e450b7c3b8cdf65363875df07da79a0" +checksum = "c61f7251ecde2c9ed431bbe0659853e7991753447447bbf1ae59d8b31c578d4e" dependencies = [ "sval", "sval_buffer", @@ -7782,9 +7782,9 @@ dependencies = [ [[package]] name = "xcursor" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" [[package]] name = "xkbcommon-dl" diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 8a6cf37d..4cdfd609 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -678,7 +678,7 @@ async fn test_rtm_connection(maxima_arc: LockedMaxima) -> Result<()> { let players: Vec = friends.iter().map(|f| f.id().to_owned()).collect(); info!("Subscribed to {} players", players.len()); - rtm.subscribe(&players).await?; + rtm.subscribe_all().await?; drop(maxima); loop { @@ -704,7 +704,7 @@ async fn test_rtm_connection(maxima_arc: LockedMaxima) -> Result<()> { drop(maxima); - tokio::time::sleep(std::time::Duration::from_millis(500)).await; + tokio::time::sleep(std::time::Duration::from_secs(30)).await; } } @@ -814,7 +814,7 @@ async fn start_game( let players: Vec = friends.iter().map(|f| f.id().to_owned()).collect(); info!("Subscribed to {} players", players.len()); - maxima.rtm().subscribe(&players).await?; + maxima.rtm().subscribe_all().await?; } } diff --git a/maxima-lib/build.rs b/maxima-lib/build.rs index ea64d751..43ba4c80 100644 --- a/maxima-lib/build.rs +++ b/maxima-lib/build.rs @@ -1,3 +1,14 @@ fn main() -> std::io::Result<()> { - prost_build::compile_protos(&["src/rtm/proto/rtm.proto"], &["src/rtm/proto/"]) -} + prost_build::compile_protos( + &[ + "src/rtm/proto/rtm.proto", + "src/rtm/proto/common.proto", + "src/rtm/proto/presence.proto", + "src/rtm/proto/chat.proto", + "src/rtm/proto/config.proto", + "src/rtm/proto/player.proto", + "src/rtm/proto/antelope_common.proto", + ], + &["src/rtm/proto/"], + ) +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/client.rs b/maxima-lib/src/rtm/client.rs index a2c0cb2d..3aa657be 100644 --- a/maxima-lib/src/rtm/client.rs +++ b/maxima-lib/src/rtm/client.rs @@ -12,7 +12,7 @@ use super::{ RtmError, connection::RtmConnectionManager, proto::{ - BasicPresenceType, HeartbeatV1, LoginV3Response, Player, PresenceUpdateV1, + BasicPresenceType, HeartbeatV1, LoginV3Response, Player, PresenceUpdateV1, PresenceSubscribeAllFriendsV1, RichPresenceType, RichPresenceV1, SessionCleanupV1, communication_v1, success_v1, }, }; @@ -36,15 +36,12 @@ macro_rules! send_rtm_request { async move { match fut.await? { communication_v1::Body::Success(success) => { - if let Some(body) = success.body { - if matches!(body, success_v1::Body::$response_body_name(_)) { - let success_v1::Body::$response_body_name(data) = body; - Ok(data) - } else { - Err(RtmError::InvalidResponse(body)) - } - } else { - Err(RtmError::NoBody) + match success.body { + Some(body) => match body { + success_v1::Body::$response_body_name(data) => Ok(data), + body => Err(RtmError::InvalidResponse(body)), + }, + None => Err(RtmError::NoBody), } } communication_v1::Body::Error(err) => Err(RtmError::V1(err)), @@ -52,7 +49,12 @@ macro_rules! send_rtm_request { } } } - _rtm_transform($connection_manager.send_request(communication_v1::Body::$request_body_name($comm_name $comm_initializer))) + + _rtm_transform( + $connection_manager.send_request( + communication_v1::Body::$request_body_name($comm_name $comm_initializer) + ) + ) } }; } @@ -166,15 +168,12 @@ impl RtmClient { ) -> Result<(), RtmError> { match body { communication_v1::Body::Presence(presence) => { - if presence.client_version.is_none() { + if presence.client_version.is_empty() { return Ok(()); } let res: ClientVersion = serde_json::from_str( - presence - .client_version - .as_ref() - .ok_or(RtmError::InvalidClientVersion)?, + &presence.client_version, )?; if res.client_type != "Client" && res.client_type != "LegacyClient" { @@ -229,8 +228,8 @@ impl RtmClient { product_id: "origin".to_owned(), platform: PlatformV1::Pc as i32, client_version: serde_json::to_string(&client_version)?, - session_key: None, - force_disconnect_session_key: None, + session_key: String::new(), + force_disconnect_session_key: String::new(), }).await?; for ele in res.connected_sessions { @@ -250,11 +249,9 @@ impl RtmClient { pub async fn set_presence( &mut self, basic_presence: BasicPresence, - status: &str, - offer_id: &str, + game_name: &str, // e.g. "Apex Legends" + offer_id: &str, // e.g. "Origin.OFR.50.0002148" ) -> Result<(), RtmError> { - info!("Updating RTM presence to '{}'", status); - let rpc_data = CustomRichPresenceData { game_product_id: offer_id.to_owned(), version: 1, @@ -268,12 +265,17 @@ impl RtmClient { BasicPresence::Online => BasicPresenceType::Online, }; + // status is the JSON availability string, not a label + let status_json = serde_json::to_string(&serde_json::json!({ + "presenceavailability": basic_presence_type.as_str_name().to_lowercase() + }))?; + send_and_forget_rtm_request!(self.conn_man, PresenceUpdate, PresenceUpdateV1, { - status: "".to_owned(), + status: status_json, basic_presence_type: basic_presence_type as i32, user_defined_presence: "".to_owned(), rich_presence: Some(RichPresenceV1 { - game: status.to_owned(), + game: game_name.to_owned(), // game name, not status text platform: PlatformV1::Pc as i32, game_mode_type: "".to_owned(), game_mode: "".to_owned(), @@ -288,13 +290,20 @@ impl RtmClient { } /// Subscribe to a list of user IDs' presences - pub async fn subscribe(&mut self, players: &[String]) -> Result<(), RtmError> { + pub async fn subscribe(&mut self, persona: Vec, players: &[String]) -> Result<(), RtmError> { send_and_forget_rtm_request!(self.conn_man, PresenceSubscribe, PresenceSubscribeV1, { + persona_id: vec![], players: players.iter().map(|id| Player{ player_id: id.to_owned(), product_id: String::from("origin"), }).collect() }) .await } + + pub async fn subscribe_all(&mut self) -> Result<(), RtmError> { + send_and_forget_rtm_request!(self.conn_man, PresenceSubscribeAllFriendsV1, PresenceSubscribeAllFriendsV1, {}) + .await + } + pub async fn session_cleanup(&mut self, session_key: &str) -> Result<(), RtmError> { send_and_forget_rtm_request!(self.conn_man, SessionCleanupV1, SessionCleanupV1, { session_key: session_key.to_owned() diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index b7e6de02..5f1ff126 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -139,13 +139,18 @@ impl RtmConnectionManager { let buf = bytes.split_to(expected_size as usize).freeze(); let msg = Communication::decode(buf)?; - let id = &msg.v1.as_ref().ok_or(RtmError::NoBody)?.request_id; + let id = match msg.body.as_ref() { + Some(crate::rtm::proto::communication::Body::V1(v1)) => &v1.request_id, + _ => return Err(Box::new(RtmError::NoBody)), + }; if let Some(tx) = pending_responses.remove(id) { let _ = tx.send(msg); - } else if id.is_empty() && let Some(body) = &msg.v1.as_ref().ok_or(RtmError::NoBody)?.body { + } else if id.is_empty() + && let Some(crate::rtm::proto::communication::Body::V1(v1)) = &msg.body + && let Some(body) = &v1.body { update_presence_tx.send(body.clone()).await?; } @@ -161,10 +166,12 @@ impl RtmConnectionManager { request = request_rx.recv(), if expected_size == -1 => { if let Some(request) = request { let communication = Communication { - v1: Some(CommunicationV1 { - request_id: request.id.to_owned(), - body: Some(request.payload), - }), + body: Some(crate::rtm::proto::communication::Body::V1( + CommunicationV1 { + request_id: request.id.to_owned(), + body: Some(request.payload), + } + )), }; let mut buf = BytesMut::new(); @@ -201,12 +208,15 @@ impl RtmConnectionManager { .await?; match response_rx.await { - Ok(response) => Ok(response - .v1 - .ok_or(RtmError::NoBody)? - .body - .ok_or(RtmError::NoBody)?), - Err(_) => Err(RtmError::Io(io::Error::other("Failed to receive response"))), + Ok(response) => match response.body { + Some(crate::rtm::proto::communication::Body::V1(v1)) => { + v1.body.ok_or(RtmError::NoBody) + } + _ => Err(RtmError::NoBody), + }, + Err(_) => Err(RtmError::Io(io::Error::other( + "Failed to receive response", + ))), } } diff --git a/maxima-lib/src/rtm/mod.rs b/maxima-lib/src/rtm/mod.rs index 42bb13bf..6d1dd393 100644 --- a/maxima-lib/src/rtm/mod.rs +++ b/maxima-lib/src/rtm/mod.rs @@ -1,5 +1,5 @@ pub mod proto { - include!(concat!(env!("OUT_DIR"), "/eadp.rtm.rs")); + include!(concat!(env!("OUT_DIR"), "/antelope.rtm.rs")); } pub mod client; diff --git a/maxima-lib/src/rtm/proto/antelope_common.proto b/maxima-lib/src/rtm/proto/antelope_common.proto new file mode 100644 index 00000000..5700e79f --- /dev/null +++ b/maxima-lib/src/rtm/proto/antelope_common.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package antelope.rtm; + +message ProductId { + string id = 1; +} + +message PlayerNetworkId { + string id = 1; +} + +message DevicePlatformId { + string id = 1; +} \ No newline at end of file diff --git a/maxima-lib/src/rtm/proto/chat.proto b/maxima-lib/src/rtm/proto/chat.proto index 879d726f..c8579a2a 100644 --- a/maxima-lib/src/rtm/proto/chat.proto +++ b/maxima-lib/src/rtm/proto/chat.proto @@ -1,9 +1,13 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "player.proto"; +import "common.proto"; +import "enum.proto"; + +package antelope.rtm; enum TypingEventType { - NO_EVENT = 1: + NO_EVENT = 0; COMPOSING = 1; CUSTOM_EVENT = 2; } diff --git a/maxima-lib/src/rtm/proto/common.proto b/maxima-lib/src/rtm/proto/common.proto index ae1b74ac..a854cab8 100644 --- a/maxima-lib/src/rtm/proto/common.proto +++ b/maxima-lib/src/rtm/proto/common.proto @@ -1,6 +1,11 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "config.proto"; +import "player.proto"; +import "presence.proto"; +import "antelope_common.proto"; + +package antelope.rtm; enum ChannelType { NO_CHANNEL_TYPE = 0; @@ -137,11 +142,11 @@ message ChatPropertyDuration { } message MessageContext { - eadp.common.v1.PlayerNetworkId senderPlayerNetworkId = 1; - eadp.common.v1.DevicePlatformId senderDevicePlatformId = 2; - eadp.common.v1.ProductId senderProductId = 3; - eadp.common.v1.PlayerNetworkId receiverPlayerNetworkId = 4; - eadp.common.v1.DevicePlatformId receiverDevicePlatformId = 5; + .antelope.rtm.PlayerNetworkId senderPlayerNetworkId = 1; + .antelope.rtm.DevicePlatformId senderDevicePlatformId = 2; + .antelope.rtm.ProductId senderProductId = 3; + .antelope.rtm.PlayerNetworkId receiverPlayerNetworkId = 4; + .antelope.rtm.DevicePlatformId receiverDevicePlatformId = 5; ContentType contentType = 6; } diff --git a/maxima-lib/src/rtm/proto/config.proto b/maxima-lib/src/rtm/proto/config.proto index 2280e8b7..edd87431 100644 --- a/maxima-lib/src/rtm/proto/config.proto +++ b/maxima-lib/src/rtm/proto/config.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +package antelope.rtm; message ServerConfigRequestV1 { } diff --git a/maxima-lib/src/rtm/proto/enum.proto b/maxima-lib/src/rtm/proto/enum.proto index 7e75932f..2a07e4d2 100644 --- a/maxima-lib/src/rtm/proto/enum.proto +++ b/maxima-lib/src/rtm/proto/enum.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +package antelope.rtm; enum ChannelMessageType { NO_CHANNEL_MESSAGE_TYPE = 0; diff --git a/maxima-lib/src/rtm/proto/error.proto b/maxima-lib/src/rtm/proto/error.proto index c97fa7e3..865458b4 100644 --- a/maxima-lib/src/rtm/proto/error.proto +++ b/maxima-lib/src/rtm/proto/error.proto @@ -1,6 +1,8 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "common.proto"; + +package antelope.rtm; message ErrorV1 { oneof body { diff --git a/maxima-lib/src/rtm/proto/moderation.proto b/maxima-lib/src/rtm/proto/moderation.proto index 9f7b3078..c5ecdb9a 100644 --- a/maxima-lib/src/rtm/proto/moderation.proto +++ b/maxima-lib/src/rtm/proto/moderation.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +package antelope.rtm; enum ModerationRoleType { NO_MODERATION_ROLE = 0; diff --git a/maxima-lib/src/rtm/proto/notification.proto b/maxima-lib/src/rtm/proto/notification.proto index 9a066189..c613f841 100644 --- a/maxima-lib/src/rtm/proto/notification.proto +++ b/maxima-lib/src/rtm/proto/notification.proto @@ -1,9 +1,13 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "chat.proto"; +import "common.proto"; +import "presence.proto"; + +package antelope.rtm; enum SessionNotificationTypeV1 { - NOTHING = 0; + NO_SESSION_NOTIFICATION = 0; DEFAULT = 1; FORCE_DISCONNECT = 2; USER_LOGGED_IN = 3; diff --git a/maxima-lib/src/rtm/proto/player.proto b/maxima-lib/src/rtm/proto/player.proto index a5660cbf..dd5a3e95 100644 --- a/maxima-lib/src/rtm/proto/player.proto +++ b/maxima-lib/src/rtm/proto/player.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +package antelope.rtm; message Player { string playerId = 1; diff --git a/maxima-lib/src/rtm/proto/point_to_point.proto b/maxima-lib/src/rtm/proto/point_to_point.proto index 02c3ade9..cccba60e 100644 --- a/maxima-lib/src/rtm/proto/point_to_point.proto +++ b/maxima-lib/src/rtm/proto/point_to_point.proto @@ -1,6 +1,8 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "player.proto"; + +package antelope.rtm; enum Domain { NO_DOMAIN = 0; diff --git a/maxima-lib/src/rtm/proto/preference.proto b/maxima-lib/src/rtm/proto/preference.proto index 7d989562..1ed36ae5 100644 --- a/maxima-lib/src/rtm/proto/preference.proto +++ b/maxima-lib/src/rtm/proto/preference.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +package antelope.rtm; message UpdatePreferenceRequestV1 { oneof body { diff --git a/maxima-lib/src/rtm/proto/presence.proto b/maxima-lib/src/rtm/proto/presence.proto index 2f01ce43..bf9c9ffa 100644 --- a/maxima-lib/src/rtm/proto/presence.proto +++ b/maxima-lib/src/rtm/proto/presence.proto @@ -1,6 +1,8 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "player.proto"; + +package antelope.rtm; enum BasicPresenceType { NONE_PRESENCE = 0; diff --git a/maxima-lib/src/rtm/proto/rtm.proto b/maxima-lib/src/rtm/proto/rtm.proto index 0ae809d5..af748fb8 100644 --- a/maxima-lib/src/rtm/proto/rtm.proto +++ b/maxima-lib/src/rtm/proto/rtm.proto @@ -1,6 +1,16 @@ syntax = "proto3"; -package com.ea.eadp.antelope.rtm.protocol; +import "presence.proto"; +import "common.proto"; +import "chat.proto"; +import "notification.proto"; +import "error.proto"; +import "point_to_point.proto"; +import "moderation.proto"; +import "preference.proto"; +import "config.proto"; + +package antelope.rtm; message Communication { oneof body { diff --git a/maxima-lib/src/rtm/proto/social.proto b/maxima-lib/src/rtm/proto/social.proto index 2095443b..44f49ac8 100644 --- a/maxima-lib/src/rtm/proto/social.proto +++ b/maxima-lib/src/rtm/proto/social.proto @@ -1,6 +1,8 @@ syntax = "proto3"; -package com.ea.eadp.antelope.protocol; +import "common.proto"; + +package antelope.rtm; enum CommunicationType { NONE_COMMUNICATION_TYPE = 0; @@ -84,7 +86,7 @@ message Player { message Channel { string channelId = 1; string channelName = 2; - com.ea.eadp.antelope.rtm.protocol.ChannelType type = 3; + ea.antelope.ChannelType type = 3; bool crossGame = 4; } @@ -93,19 +95,19 @@ message ChannelMessage { TextMessage textMessage = 5; BinaryMessage binaryMessage = 6; GroupMembershipChange membershipChange = 7; - com.ea.eadp.antelope.rtm.protocol.ChatConnectedV1 chatConnected = 8; - com.ea.eadp.antelope.rtm.protocol.ChatDisconnectedV1 chatDisconnected = 9; - com.ea.eadp.antelope.rtm.protocol.ChatLeftV1 chatLeft = 10; - com.ea.eadp.antelope.rtm.protocol.CustomMessage customMessage = 11; - com.ea.eadp.antelope.rtm.protocol.ChatUserMutedV1 chatUserMuted = 12; - com.ea.eadp.antelope.rtm.protocol.ChatUserUnmutedV1 chatUserUnmuted = 13; - com.ea.eadp.antelope.rtm.protocol.ChannelMembershipChangeV1 channelMembershipChange = 14; - com.ea.eadp.antelope.rtm.protocol.StickyMessageChangedV1 stickyMessageChanged = 15; - com.ea.eadp.antelope.rtm.protocol.ChatTypingEventV1 chatTypingEvent = 16; - com.ea.eadp.antelope.rtm.protocol.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; - com.ea.eadp.antelope.rtm.protocol.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; - com.ea.eadp.antelope.rtm.protocol.GroupAttributeChangeV1 groupAttributeChange = 19; - com.ea.eadp.antelope.rtm.protocol.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; + ea.antelope.ChatConnectedV1 chatConnected = 8; + ea.antelope.ChatDisconnectedV1 chatDisconnected = 9; + ea.antelope.ChatLeftV1 chatLeft = 10; + ea.antelope.CustomMessage customMessage = 11; + ea.antelope.ChatUserMutedV1 chatUserMuted = 12; + ea.antelope.ChatUserUnmutedV1 chatUserUnmuted = 13; + ea.antelope.ChannelMembershipChangeV1 channelMembershipChange = 14; + ea.antelope.StickyMessageChangedV1 stickyMessageChanged = 15; + ea.antelope.ChatTypingEventV1 chatTypingEvent = 16; + ea.antelope.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; + ea.antelope.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; + ea.antelope.GroupAttributeChangeV1 groupAttributeChange = 19; + ea.antelope.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; } string messageId = 1; string channelId = 2; @@ -123,10 +125,10 @@ message TextMessage { string fromPlatformDisplayName = 7; string fromPlatformNickName = 8; string metadata = 9; - com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 10; - com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 11; - com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 12; - repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatPropertyBag = 13; + ea.antelope.MessageType messageType = 10; + ea.antelope.StringId textMessageId = 11; + ea.antelope.MessageContext messageContext = 12; + repeated ea.antelope.ChatPropertyBagEntry chatPropertyBag = 13; } message BinaryMessage { @@ -166,7 +168,7 @@ message ChannelMuteList { message MutedUserV1 { string personaId = 1; - repeated com.ea.eadp.antelope.rtm.protocol.MutedByV1 mutedBy = 2; + repeated ea.antelope.MutedByV1 mutedBy = 2; Player player = 3; } @@ -182,12 +184,12 @@ message LogoutRequest { message PublishTextRequest { string channelId = 1; string text = 2; - com.ea.eadp.antelope.rtm.protocol.MessageType messageType = 3; + ea.antelope.MessageType messageType = 3; int32 index = 4; string metadata = 5; - com.ea.eadp.antelope.rtm.protocol.StringId textMessageId = 6; - com.ea.eadp.antelope.rtm.protocol.MessageContext messageContext = 7; - repeated com.ea.eadp.antelope.rtm.protocol.ChatPropertyBagEntry chatProperties = 8; + ea.antelope.StringId textMessageId = 6; + ea.antelope.MessageContext messageContext = 7; + repeated ea.antelope.ChatPropertyBagEntry chatProperties = 8; } message PublishBinaryRequest { diff --git a/maxima-ui/src/event_thread.rs b/maxima-ui/src/event_thread.rs index 0abd3b8d..9338351b 100644 --- a/maxima-ui/src/event_thread.rs +++ b/maxima-ui/src/event_thread.rs @@ -57,6 +57,10 @@ impl EventThread { ) -> Result<(), BackendError> { let mut maxima = maxima_arc.lock().await; + let user = maxima.local_user().await?; + let player = user.player().as_ref().unwrap(); + let persona_id: Vec = vec![player.psd().to_string()]; + let friends: ServiceFriends = maxima .service_layer() .request( @@ -77,7 +81,7 @@ impl EventThread { friends.friends().items().iter().map(|f| f.id().to_owned()).collect(); info!("Subscribed to {} players", players.len()); - rtm.subscribe(&players).await?; + rtm.subscribe(persona_id, &players).await?; drop(maxima); let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(30)); From f63525bd20b1a533b595a990e2601e84451dc2e5 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:10:23 +0200 Subject: [PATCH 31/57] properly rewrite social links --- maxima-lib/src/rtm/proto/social.proto | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/maxima-lib/src/rtm/proto/social.proto b/maxima-lib/src/rtm/proto/social.proto index 44f49ac8..af631794 100644 --- a/maxima-lib/src/rtm/proto/social.proto +++ b/maxima-lib/src/rtm/proto/social.proto @@ -86,7 +86,7 @@ message Player { message Channel { string channelId = 1; string channelName = 2; - ea.antelope.ChannelType type = 3; + antelope.rtm.ChannelType type = 3; bool crossGame = 4; } @@ -95,19 +95,19 @@ message ChannelMessage { TextMessage textMessage = 5; BinaryMessage binaryMessage = 6; GroupMembershipChange membershipChange = 7; - ea.antelope.ChatConnectedV1 chatConnected = 8; - ea.antelope.ChatDisconnectedV1 chatDisconnected = 9; - ea.antelope.ChatLeftV1 chatLeft = 10; - ea.antelope.CustomMessage customMessage = 11; - ea.antelope.ChatUserMutedV1 chatUserMuted = 12; - ea.antelope.ChatUserUnmutedV1 chatUserUnmuted = 13; - ea.antelope.ChannelMembershipChangeV1 channelMembershipChange = 14; - ea.antelope.StickyMessageChangedV1 stickyMessageChanged = 15; - ea.antelope.ChatTypingEventV1 chatTypingEvent = 16; - ea.antelope.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; - ea.antelope.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; - ea.antelope.GroupAttributeChangeV1 groupAttributeChange = 19; - ea.antelope.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; + antelope.rtm.ChatConnectedV1 chatConnected = 8; + antelope.rtm.ChatDisconnectedV1 chatDisconnected = 9; + antelope.rtm.ChatLeftV1 chatLeft = 10; + antelope.rtm.CustomMessage customMessage = 11; + antelope.rtm.ChatUserMutedV1 chatUserMuted = 12; + antelope.rtm.ChatUserUnmutedV1 chatUserUnmuted = 13; + antelope.rtm.ChannelMembershipChangeV1 channelMembershipChange = 14; + antelope.rtm.StickyMessageChangedV1 stickyMessageChanged = 15; + antelope.rtm.ChatTypingEventV1 chatTypingEvent = 16; + antelope.rtm.WorldChatMembershipChangeV1 worldChatMembershipChange = 17; + antelope.rtm.GroupMemberAttributeChangeV1 groupMemberAttributeChange = 18; + antelope.rtm.GroupAttributeChangeV1 groupAttributeChange = 19; + antelope.rtm.GroupMemberRoleChangeV1 groupMemberRoleChange = 20; } string messageId = 1; string channelId = 2; @@ -125,10 +125,10 @@ message TextMessage { string fromPlatformDisplayName = 7; string fromPlatformNickName = 8; string metadata = 9; - ea.antelope.MessageType messageType = 10; - ea.antelope.StringId textMessageId = 11; - ea.antelope.MessageContext messageContext = 12; - repeated ea.antelope.ChatPropertyBagEntry chatPropertyBag = 13; + antelope.rtm.MessageType messageType = 10; + antelope.rtm.StringId textMessageId = 11; + antelope.rtm.MessageContext messageContext = 12; + repeated antelope.rtm.ChatPropertyBagEntry chatPropertyBag = 13; } message BinaryMessage { @@ -168,7 +168,7 @@ message ChannelMuteList { message MutedUserV1 { string personaId = 1; - repeated ea.antelope.MutedByV1 mutedBy = 2; + repeated antelope.rtm.MutedByV1 mutedBy = 2; Player player = 3; } @@ -184,12 +184,12 @@ message LogoutRequest { message PublishTextRequest { string channelId = 1; string text = 2; - ea.antelope.MessageType messageType = 3; + antelope.rtm.MessageType messageType = 3; int32 index = 4; string metadata = 5; - ea.antelope.StringId textMessageId = 6; - ea.antelope.MessageContext messageContext = 7; - repeated ea.antelope.ChatPropertyBagEntry chatProperties = 8; + antelope.rtm.StringId textMessageId = 6; + antelope.rtm.MessageContext messageContext = 7; + repeated antelope.rtm.ChatPropertyBagEntry chatProperties = 8; } message PublishBinaryRequest { From a4ce8118ef0376da7e883045470638b429ebed45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:18:17 +0200 Subject: [PATCH 32/57] Update README.md --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cf8aabc2..e971c6d1 100644 --- a/README.md +++ b/README.md @@ -24,20 +24,21 @@ Maxima itself is a library (`maxima-lib`), with provided CLI (`maxima-cli`), TUI - Locating games (aka. game importing) - Running games under [proton](https://github.com/GloriousEggroll/proton-ge-custom) on Linux/SteamDeck - `proton-ge` is automatically installed together with [umu](https://github.com/Open-Wine-Components/umu-launcher). - -**In-Dev:** - - macOS support - - Support for launching Maxima through Epic/Steam + +**Developer Roadmap:** + - macOS support (sjp761) + - Chat system + - Friend Adding/Removing/Inviting + - Support for launching Maxima through Epic/Steam (Link2EA protocol) + - Cleaner/Stabler downloader implementation **Planned:** - Library documentation/examples - Support for installing DLCs - Full EA Desktop interoperability. Games installed with EA Desktop already appear on Maxima, but to take it a step further we'd like the ability to, for example, start a download on EA Desktop and continue it on Maxima. - - Cleaner/Stabler downloader implementation - Progressive/Selective installs - Some games are able to start without being fully installed, and some games contain language-specific files. - Support for the store (buying games) - - Friend Adding/Removing/Inviting - Status setting; locked to "online" at the moment - Refactoring Maxima to new architecture allowing multiple frontends to co-exist From 5901d4700d488713df63a209efd2cd41a9567491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:21:25 +0200 Subject: [PATCH 33/57] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e971c6d1..7fa0f3d8 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Maxima itself is a library (`maxima-lib`), with provided CLI (`maxima-cli`), TUI ![UI](images/UI.png) +**Current developing stack:** +Rust 1.90.1 | UI : egui / glow | Requests : reqwest | Async support & work : tokio | Windows layer : windows-sys | Linux layer : homemade | macOS layer : homemade and many under the hood plugins that make this program possible. + **Features:** - EA Authentication - Downloading/Updating games From 3ab3d38086e17e252f25c1df8d2bd6fb6673666d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20M=2E?= <30985701+BellezaEmporium@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:21:47 +0200 Subject: [PATCH 34/57] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7fa0f3d8..baadf0c1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Maxima itself is a library (`maxima-lib`), with provided CLI (`maxima-cli`), TUI ![UI](images/UI.png) **Current developing stack:** + Rust 1.90.1 | UI : egui / glow | Requests : reqwest | Async support & work : tokio | Windows layer : windows-sys | Linux layer : homemade | macOS layer : homemade and many under the hood plugins that make this program possible. **Features:** From af33516e532b9650df59690d4864fdc578cba7bf Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:42:53 +0200 Subject: [PATCH 35/57] Implement touchup service into maxima-service, elevates request directly --- Cargo.lock | 102 +- Cargo.toml | 4 + maxima-cli/Cargo.toml | 1 + maxima-cli/src/main.rs | 148 +-- maxima-lib/Cargo.toml | 6 +- maxima-lib/src/content/downloader.rs | 1045 ++++------------- maxima-lib/src/content/manager.rs | 294 ++--- maxima-lib/src/content/mod.rs | 34 +- maxima-lib/src/content/zip.rs | 35 +- maxima-lib/src/content/zlib.rs | 191 ++- maxima-lib/src/core/background_service_win.rs | 5 + maxima-service/src/service/error.rs | 2 + maxima-service/src/service/mod.rs | 19 +- maxima-ui/src/bridge_thread.rs | 5 +- 14 files changed, 758 insertions(+), 1133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 238d6c5e..6a99dcc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -442,9 +442,8 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +version = "0.4.42" +source = "git+https://github.com/sjp761/async-compression.git?rev=a226c72f754f942ad187cc65cb1f03df5ae26b7b#a226c72f754f942ad187cc65cb1f03df5ae26b7b" dependencies = [ "compression-codecs", "compression-core", @@ -1080,8 +1079,7 @@ dependencies = [ [[package]] name = "compression-codecs" version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +source = "git+https://github.com/sjp761/async-compression.git?rev=a226c72f754f942ad187cc65cb1f03df5ae26b7b#a226c72f754f942ad187cc65cb1f03df5ae26b7b" dependencies = [ "compression-core", "flate2", @@ -1091,8 +1089,7 @@ dependencies = [ [[package]] name = "compression-core" version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +source = "git+https://github.com/sjp761/async-compression.git?rev=a226c72f754f942ad187cc65cb1f03df5ae26b7b#a226c72f754f942ad187cc65cb1f03df5ae26b7b" [[package]] name = "concurrent-queue" @@ -1345,12 +1342,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core 0.24.0", + "darling_macro 0.24.0", ] [[package]] @@ -1369,15 +1366,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1393,13 +1390,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" dependencies = [ - "darling_core 0.23.0", + "darling_core 0.24.0", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1530,6 +1527,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1965,12 +1971,6 @@ dependencies = [ "regex", ] -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - [[package]] name = "fastrand" version = "2.5.0" @@ -2045,13 +2045,12 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +version = "1.1.10" +source = "git+https://github.com/sjp761/flate2-rs.git?rev=45fc9f6a56a97513b285029fee14213a2d0fe2e5#45fc9f6a56a97513b285029fee14213a2d0fe2e5" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", ] [[package]] @@ -2935,15 +2934,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ - "darling 0.23.0", + "darling 0.24.0", "indoc", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -2969,9 +2968,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is-docker" @@ -3199,9 +3198,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "bitflags 2.13.1", "libc", @@ -3223,9 +3222,9 @@ dependencies = [ [[package]] name = "line-clipping" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ "bitflags 2.13.1", ] @@ -3405,6 +3404,7 @@ dependencies = [ "maxima-lib", "maxima-resources", "regex", + "reqwest", "serde", "serde_json", "tokio", @@ -3417,6 +3417,7 @@ name = "maxima-lib" version = "0.1.0" dependencies = [ "aes", + "anyhow", "async-compression", "async-trait", "base62", @@ -3429,6 +3430,7 @@ dependencies = [ "derive-getters", "derive_builder", "directories", + "dirs", "ecb", "enable-ansi-support", "encoding_rs", @@ -3662,6 +3664,7 @@ checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "serde", + "simd-adler32", ] [[package]] @@ -4416,21 +4419,21 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "palette" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" dependencies = [ "approx", - "fast-srgb8", "libm", "palette_derive", + "palette_math", ] [[package]] name = "palette_derive" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" dependencies = [ "by_address", "proc-macro2", @@ -4438,6 +4441,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + [[package]] name = "parking_lot" version = "0.12.5" diff --git a/Cargo.toml b/Cargo.toml index f491d1a6..56efde8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,10 @@ winreg = "0.56.0" windows-service = "0.8.1" is_elevated = "0.1.2" +[patch.crates-io] +async-compression = { git = "https://github.com/sjp761/async-compression.git", rev = "a226c72f754f942ad187cc65cb1f03df5ae26b7b", version = "0.4.42" } +flate2 = { git = "https://github.com/sjp761/flate2-rs.git", rev = "45fc9f6a56a97513b285029fee14213a2d0fe2e5", version = "1.1.10" } + [workspace.package] version = "0.1.0" edition = "2024" diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index 5fb9ad7e..cee2bc6e 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -16,6 +16,7 @@ log = { workspace = true } regex = { workspace = true } inquire = { workspace = true } futures = { workspace = true } +reqwest = { workspace = true } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true } diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 4cdfd609..c8e4faf9 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -1,13 +1,14 @@ use clap::{Parser, Subcommand}; -use anyhow::{Result, bail}; +use anyhow::{bail, Result}; use inquire::{Select, Text}; use log::{debug, error, info, warn}; use regex::Regex; +use reqwest; use std::{ path::PathBuf, - sync::{Arc, LazyLock}, + sync::LazyLock, time::Instant, }; @@ -16,22 +17,18 @@ use is_elevated::is_elevated; #[cfg(windows)] use maxima::{ - core::background_service::request_registry_setup, + core::background_service::{request_registry_setup, BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, util::service::{is_service_running, is_service_valid, register_service_user, start_service}, }; use maxima::{ content::{ - ContentService, - downloader::ZipDownloader, - manager::{QueuedGame, QueuedGameBuilder}, + ContentService, downloader::ZipDownloader, manager::QueuedGameBuilder, }, core::{ LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, auth::{ TokenResponse, context::AuthContext, login::{begin_oauth_login_flow, manual_login}, nucleus_auth_exchange, nucleus_token_exchange, - }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH}, service_layer::{ - SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, - ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, - ServiceLegacyOffer, ServicePlayer, + }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ + SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceLegacyOffer, ServicePlayer, }, }, ooa, rtm::client::BasicPresence, util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, }; @@ -108,18 +105,15 @@ struct Args { #[command(subcommand)] mode: Option, - #[arg(long)] - #[clap(global = true)] + #[arg(long, global = true)] login: Option, } #[tokio::main] async fn main() { - let result = startup().await; - - if let Some(e) = result.err() { + if let Err(e) = startup().await { match std::env::var("RUST_BACKTRACE") { - Ok(_) => error!("{}:\n{}", e, e.backtrace().to_string()), + Ok(_) => error!("{}:\n{}", e, e.backtrace()), Err(_) => error!("{}: {}", e, e.root_cause()), } } @@ -164,16 +158,12 @@ pub async fn login_flow(login_override: Option) -> Result let mut auth_context = AuthContext::new()?; if let Some(access_token) = &login_override { - let access_token = if let Some(captures) = MANUAL_LOGIN_PATTERN.captures(&access_token) { + let access_token = if let Some(captures) = MANUAL_LOGIN_PATTERN.captures(access_token) { let persona = &captures[1]; let password = &captures[2]; - let login_result = manual_login(persona, password).await; - if login_result.is_err() { - bail!("Login failed: {}", login_result.err().unwrap().to_string()); - } - - login_result.unwrap() + let login_result = manual_login(persona, password).await?; + login_result } else { access_token.to_owned() }; @@ -193,12 +183,8 @@ pub async fn login_flow(login_override: Option) -> Result info!("Received login..."); } - let token_res = nucleus_token_exchange(&auth_context).await; - if token_res.is_err() { - bail!("Login failed: {}", token_res.err().unwrap().to_string()); - } - - Ok(token_res?) + let token_res = nucleus_token_exchange(&auth_context).await?; + Ok(token_res) } async fn startup() -> Result<()> { @@ -271,17 +257,10 @@ async fn startup() -> Result<()> { let offer_id = if login.is_none() { let mut maxima = maxima_arc.lock().await; let offer = maxima.mut_library().game_by_base_slug(&slug).await; - // TODO: ideally this function should return an Error type, but this frontend makes that complicated - if let Err(err) = offer { - bail!("Error fetching offer for slug `{}`: {}", slug, err); - } else { - let offer = offer.unwrap(); - // TODO: could do a match here as well, same problem as above - if offer.is_some() { - offer.unwrap().offer_id().to_owned() - } else { - bail!("No owned offer found for '{}'", slug); - } + match offer { + Ok(Some(offer)) => offer.offer_id().to_owned(), + Ok(None) => bail!("No owned offer found for '{}'", slug), + Err(err) => bail!("Error fetching offer for slug `{}`: {}", slug, err), } } else { slug @@ -352,7 +331,6 @@ async fn interactive_start_game(maxima_arc: LockedMaxima) -> Result<()> { if !game.base_offer().is_installed().await { continue; } - owned_games.push(game); } @@ -380,7 +358,6 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { if game.base_offer().is_installed().await { continue; } - owned_games.push(game); } @@ -432,16 +409,16 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let mut maxima = maxima_arc.lock().await; for event in maxima.consume_pending_events() { - match event { - MaximaEvent::ReceivedLSXRequest(_pid, _request) => (), - _ => {} - } + let MaximaEvent::ReceivedLSXRequest(_, _) = event else { + continue; + }; + debug!("Received LSX request event"); } maxima.update().await; if let Some(downloader) = maxima.content_manager().current() { - info!("Downloading: {}%/100%", downloader.percentage_done()); + info!("Downloading: {:.1}%/100%", downloader.percentage_done()); } else { break; } @@ -450,11 +427,9 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } - let end_time = Instant::now(); - let elapsed_time = end_time - start_time; - + let elapsed_time = Instant::now() - start_time; info!( - "Download took {}.{}", + "Download took {}.{}s", elapsed_time.as_secs(), elapsed_time.subsec_millis() ); @@ -472,7 +447,7 @@ async fn download_specific_file( let content_service = ContentService::new(maxima.auth_storage().clone()); let builds = content_service.available_builds(offer).await?; - let build = builds.build(build_id); + let build = builds.builds.iter().find(|b| b.build_id() == build_id); if build.is_none() { bail!("Couldn't find the game build {}", build_id); } @@ -486,7 +461,7 @@ async fn download_specific_file( debug!("URL: {}", url.url()); - let downloader = ZipDownloader::new("test-game", &url.url(), std::path::Path::new("C:/DownloadTest")).await?; + let downloader = ZipDownloader::new(url.url()).await?; let num_of_entries = downloader.manifest().entries().len(); info!("Entries: {}", num_of_entries); @@ -500,7 +475,8 @@ async fn download_specific_file( } let ele = entry.unwrap(); - downloader.download_single_file(ele, None).await.unwrap(); + let output_dir = std::env::current_dir()?.join("downloads"); + downloader.download_single_file(ele, &output_dir).await?; info!( "Downloaded file {} from game build {}", @@ -598,12 +574,7 @@ async fn juno_token_refresh(maxima_arc: LockedMaxima) -> Result<()> { bail!("Login failed!"); } - let token_res = nucleus_token_exchange(&context).await; - if token_res.is_err() { - bail!("Login failed: {}", token_res.err().unwrap().to_string()); - } - - let token_res = token_res.unwrap(); + let token_res = nucleus_token_exchange(&context).await?; info!("Access Token: {}", token_res.access_token()); info!("Refresh Token: {:?}", token_res.refresh_token()); info!("Token Type: {}", token_res.token_type()); @@ -650,19 +621,12 @@ async fn get_user_by_id(maxima_arc: LockedMaxima, user_id: &str) -> Result<()> { .await?; info!("Name: {}", player.display_name()); - dbg!(player); Ok(()) } -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()), - // Err(err) => error!("{}", err), - // }; - +async fn get_game_by_slug(_maxima_arc: LockedMaxima, _slug: &str) -> Result<()> { + // TODO: implement Ok(()) } @@ -687,17 +651,18 @@ async fn test_rtm_connection(maxima_arc: LockedMaxima) -> Result<()> { { let store = maxima.rtm().presence_store().lock().await; - for entry in store.iter() { + for (id, presence) in store.iter() { + let name = friends + .iter() + .find(|x| *x.id() == *id) + .map(|f| f.display_name()) + .map_or("unknown", |v| v); info!( "{}/{} is {:?}: In {}", - friends - .iter() - .find(|x| x.id().to_owned() == *entry.0) - .unwrap() - .display_name(), - entry.0, - entry.1.basic(), - entry.1.status() + name, + id, + presence.basic(), + presence.status() ); } } @@ -757,9 +722,18 @@ 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) -> Result<()> { let path = PathBuf::from(path); let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; + let client = reqwest::Client::new(); + let resp = client + .post(format!("http://127.0.0.1:{}/touchup", BACKGROUND_SERVICE_PORT)) + .json(&ServiceTouchupRequest { + output_dir: path.to_string_lossy().into_owned(), + }) + .send() + .await?; + info!("Installation finished!"); manifest.run_touchup(&path).await?; info!("Installed!"); Ok(()) @@ -837,7 +811,11 @@ async fn start_game( launch::start_game( maxima_arc.clone(), - LaunchMode::OnlineOffline(offer_id.to_owned(), persona.to_owned(), password.to_owned()), + LaunchMode::OnlineOffline( + offer_id.to_owned(), + persona.to_owned(), + password.to_owned(), + ), launch_options, ) .await?; @@ -847,10 +825,10 @@ async fn start_game( let mut maxima = maxima_arc.lock().await; for event in maxima.consume_pending_events() { - match event { - MaximaEvent::ReceivedLSXRequest(_pid, _request) => (), - _ => {} - } + let MaximaEvent::ReceivedLSXRequest(_, _) = event else { + continue; + }; + debug!("Received LSX request event"); } maxima.update().await; @@ -863,4 +841,4 @@ async fn start_game( } Ok(()) -} +} \ No newline at end of file diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 3ae53a5e..10b8783e 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -14,7 +14,6 @@ rust-version = { workspace = true } [dependencies] postcard = { version = "1.1.3", features = ["use-std"] } -flate2 = { version = "1.1.9", features = ["zlib"] } scopeguard = "1.2.0" tokio = { workspace = true } moka = { version = "0.12.15", features = ["sync"] } @@ -51,7 +50,6 @@ ring = "0.17.14" futures = { workspace = true } prost = "0.14.4" encoding_rs = "0.8.35" -async-compression = { version = "0.4.43", features = ["tokio", "deflate"] } tokio-util = { version = "0.7.19", features = ["io", "compat"] } uuid = "1.24.0" tokio-rustls = "0.26.4" @@ -75,6 +73,10 @@ rustls-pki-types = "1.15.1" tokio-stream = "0.1.19" sha1 = "0.11.0" libz-sys = "1.1.29" +anyhow = { workspace = true } +dirs = "6.0.0" +async-compression = { version = "0.4.42", features = ["tokio", "deflate"] } +flate2 = { version = "1.1.10", features = ["zlib"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 6fb2cb6e..07d6bc38 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -1,901 +1,354 @@ use std::{ - fs::{self, File, OpenOptions}, io::{self, BufReader, BufWriter, Read, Write}, path::{Path, PathBuf}, sync::Arc, time::Duration, + io, + path::{Path, PathBuf}, + pin::Pin, + task::{self, Poll}, }; -use miniz_oxide::{ - DataFormat, MZError, MZFlush, MZStatus, - inflate::stream::{InflateState, inflate}, +use anyhow::{bail, Result}; +use async_compression::tokio::write::DeflateDecoder; +use bytes::{Bytes, BytesMut}; +use futures::{Stream, StreamExt, TryStreamExt}; +use log::{debug, error, info}; +use reqwest::{Client, StatusCode}; +use strum_macros::Display; +use tokio::{ + fs::{OpenOptions, create_dir, create_dir_all}, io::{AsyncWrite, AsyncWriteExt, BufReader}, }; -use reqwest::{ - StatusCode, - blocking::{Client, Response}, - header::{ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_RANGE, ETAG, IF_RANGE, RANGE}, -}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use thiserror::Error; -use url::Url; -use crate::content::zip::{CompressionType, ZipError, ZipFile, ZipFileEntry}; - -const CHECKPOINT_MAGIC: [u8; 4] = *b"MDL1"; -const CHECKPOINT_VERSION: u16 = 1; -const BUFFER_SIZE: usize = 64 * 1024; -const REPLAY_BUFFER_SIZE: usize = 64 * 1024; -const INFLATE_CHUNK: usize = 64 * 1024; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum CompressionFormat { - /// A normal zlib stream: RFC 1950 + RFC 1951. - Zlib, - - /// A bare DEFLATE stream: RFC 1951. - RawDeflate, -} +use tokio_util::compat::FuturesAsyncReadCompatExt; -impl CompressionFormat { - fn miniz_format(self) -> DataFormat { - match self { - Self::Zlib => DataFormat::Zlib, - Self::RawDeflate => DataFormat::Raw, - } - } -} +use crate::content::{ + zip::CompressionType, + zlib::{restore_zlib_state, write_zlib_state}, +}; -#[derive(Debug, Clone)] -pub struct DownloadPaths { - /// The final decompressed payload. - pub output: PathBuf, +use super::zip::{ZipFile, ZipFileEntry}; - /// A durable copy of every compressed byte already processed. - /// - /// This permits replay if a serialized miniz_oxide checkpoint becomes - /// incompatible after a dependency upgrade. - pub compressed_spool: PathBuf, +/// How often we persist the zlib state, in bytes of *compressed* input. +const STATE_SAVE_INTERVAL: usize = 1_048_576; +const MAX_RETRIES: u32 = 3; - /// The atomic checkpoint containing decoder state and offsets. - pub checkpoint: PathBuf, +#[derive(Debug)] +enum DecoderRestoreError { + CacheEmpty, } -impl DownloadPaths { - pub fn from_output(output: impl Into) -> Self { - let output = output.into(); - - Self { - compressed_spool: append_suffix(&output, ".compressed.part"), - checkpoint: append_suffix(&output, ".checkpoint"), - output, - } - } - - pub fn remove_partial_files(&self) -> io::Result<()> { - remove_if_exists(&self.output)?; - remove_if_exists(&self.compressed_spool)?; - remove_if_exists(&self.checkpoint)?; - Ok(()) - } +trait RestorableDecoder { + fn save_state(&mut self); + fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError>; } -#[derive(Debug, Clone)] -pub struct DownloadRequest { - pub url: Url, - pub compression: CompressionFormat, - pub paths: DownloadPaths, - - /// A stable identity supplied by the caller, for example an expected - /// SHA-256 digest of the final payload or a manifest asset ID. - /// - /// It prevents accidentally applying an old checkpoint to another URL. - pub resource_id: [u8; 32], - - /// Number of compressed network bytes between checkpoint writes. - pub checkpoint_interval: u64, +struct RestorableDeflateDecoder { + inner: DeflateDecoder, + file_name: String, + bytes_written: usize, + bytes_since_last: usize, + should_save: bool, } -impl DownloadRequest { - pub fn new(url: Url, compression: CompressionFormat, output: impl Into) -> Self { - let resource_id = sha256(url.as_str().as_bytes()); - +impl RestorableDeflateDecoder { + fn new(decoder: DeflateDecoder, file_name: String) -> Self { Self { - url, - compression, - paths: DownloadPaths::from_output(output), - resource_id, - checkpoint_interval: 4 * 1024 * 1024, + inner: decoder, + file_name, + bytes_written: 0, + bytes_since_last: 0, + should_save: false, } } -} - -#[derive(Debug, Clone)] -pub struct DownloadResult { - pub compressed_bytes: u64, - pub decompressed_bytes: u64, - pub resumed: bool, -} - -#[derive(Debug, Error)] -pub enum DownloadError { - #[error("I/O error: {0}")] - Io(#[from] io::Error), - - #[error("HTTP request failed: {0}")] - Http(#[from] reqwest::Error), - - #[error("HTTP server returned {status} for {url}")] - HttpStatus { status: StatusCode, url: Url }, - - #[error("server returned Content-Encoding={encoding:?}; expected identity")] - UnexpectedContentEncoding { encoding: Option }, - - #[error("server ignored the resume Range request")] - RangeIgnored, - - #[error("server returned an invalid Content-Range header")] - InvalidContentRange, - - #[error("checkpoint is malformed")] - InvalidCheckpoint, - - #[error("checkpoint format version {found} is unsupported")] - UnsupportedCheckpointVersion { found: u16 }, - - #[error("checkpoint was created for a different URL or resource identity")] - CheckpointResourceMismatch, - - #[error("checkpoint compression format does not match this request")] - CheckpointCompressionMismatch, - - #[error("compressed spool is shorter than the checkpoint offset")] - CompressedSpoolTooShort, - #[error("decompressed output is shorter than the checkpoint offset")] - DecompressedOutputTooShort, - - #[error("miniz_oxide stream error: {0:?}")] - Inflate(MZError), - - #[error("inflate made no progress")] - InflateNoProgress, - - #[error("received bytes after the end of the DEFLATE stream")] - TrailingCompressedData, - - #[error("the HTTP response ended before the DEFLATE stream completed")] - IncompleteDeflateStream, - - #[error("replayed stream produced {actual} bytes; checkpoint expected {expected}")] - ReplayOutputMismatch { expected: u64, actual: u64 }, - - #[error(transparent)] - Zip(#[from] ZipError), - - #[error("server returned {0} for a ranged request")] - UnexpectedStatus(StatusCode), - - #[error("entry `{0}` decompressed to an unexpected size")] - SizeMismatch(String), -} - -#[derive(Debug, Serialize, Deserialize)] -struct Checkpoint { - magic: [u8; 4], - version: u16, - - resource_id: [u8; 32], - compression: CompressionFormat, - - /// Bytes durably written to `compressed_spool`. - compressed_offset: u64, - - /// Bytes durably written to `output`. - decompressed_offset: u64, - - /// HTTP ETag captured when the checkpoint was made. - /// - /// On resume, this is sent in If-Range. If the representation changed, - /// the server should return 200 instead of serving a stale byte range. - etag: Option, -} - -impl Checkpoint { - fn new( - request: &DownloadRequest, - compressed_offset: u64, - decompressed_offset: u64, - etag: Option, - ) -> Self { - Self { - magic: CHECKPOINT_MAGIC, - version: CHECKPOINT_VERSION, - resource_id: request.resource_id, - compression: request.compression, - compressed_offset, - decompressed_offset, - etag, - } - } - - fn validate(&self, request: &DownloadRequest) -> Result<(), DownloadError> { - if self.magic != CHECKPOINT_MAGIC { - return Err(DownloadError::InvalidCheckpoint); - } - - if self.version != CHECKPOINT_VERSION { - return Err(DownloadError::UnsupportedCheckpointVersion { - found: self.version, - }); - } - - if self.resource_id != request.resource_id { - return Err(DownloadError::CheckpointResourceMismatch); - } - - if self.compression != request.compression { - return Err(DownloadError::CheckpointCompressionMismatch); - } - - Ok(()) + fn update_bytes_written(&mut self, bytes: usize) { + self.bytes_written += bytes; + self.bytes_since_last += bytes; + self.should_save = self.bytes_since_last >= STATE_SAVE_INTERVAL; } -} -struct ActiveDownload { - inflate: Box, - compressed_offset: u64, - decompressed_offset: u64, - etag: Option, - stream_finished: bool, - resumed: bool, -} - -impl ActiveDownload { - fn fresh(format: CompressionFormat, etag: Option) -> Self { - Self { - inflate: InflateState::new_boxed(format.miniz_format()), - compressed_offset: 0, - decompressed_offset: 0, - etag, - stream_finished: false, - resumed: false, - } + fn state_path(&self) -> Result { + let dir = dirs::cache_dir() + .ok_or_else(|| anyhow::anyhow!("no cache dir"))? + .join("Maxima"); + std::fs::create_dir_all(&dir)?; + + let safe_name = self.file_name.replace(['/', '\\'], "_"); + Ok(dir.join(format!("{}.state", safe_name))) } - fn from_checkpoint( - checkpoint: Checkpoint, - request: &DownloadRequest, - ) -> Result { - // Rebuild a fresh decoder and fast-forward it by replaying the - // already-downloaded compressed bytes from the spool file, since - // InflateState cannot be persisted directly. - let inflate = InflateState::new_boxed(request.compression.miniz_format()); - let mut spool = BufReader::new(File::open(&request.paths.compressed_spool)?); - let sink = io::sink(); - let mut input = [0_u8; REPLAY_BUFFER_SIZE]; - let compressed_offset = 0_u64; - let decompressed_offset = 0_u64; - let stream_finished = false; - - while compressed_offset < checkpoint.compressed_offset { - let count = spool.read(&mut input)?; - if count == 0 { - break; - } - let to_process = &input[..count]; - } - - if compressed_offset != checkpoint.compressed_offset - || decompressed_offset != checkpoint.decompressed_offset - { - return Err(DownloadError::InvalidCheckpoint); - } - - Ok(Self { - inflate, - compressed_offset, - decompressed_offset, - etag: checkpoint.etag, - stream_finished, - resumed: true, - }) + fn zstream(&mut self) -> &mut libz_sys::z_stream { + self.inner + .inner_mut() + .decoder_mut() + .inner + .decompress + .get_raw() } - fn checkpoint(&self, request: &DownloadRequest) -> Checkpoint { - Checkpoint::new( - request, - self.compressed_offset, - self.decompressed_offset, - self.etag.clone(), - ) + pub fn get_mut(&mut self) -> &mut W { + self.inner.get_mut() } } -/// A synchronous, resumable downloader. -/// -/// The remote representation must be stable and range-addressable. This -/// downloader sends `Accept-Encoding: identity`; configure the origin so that -/// the payload itself is zlib or raw-DEFLATE if decompression is required. -pub struct Downloader { - client: Client, -} - -impl Downloader { - pub fn new() -> Result { - let client = Client::builder() - .connect_timeout(Duration::from_secs(30)) - .timeout(Duration::from_secs(300)) - .build()?; - - Ok(Self { client }) - } - - pub fn download(&self, request: &DownloadRequest) -> Result { - ensure_parent_exists(&request.paths.output)?; - ensure_parent_exists(&request.paths.compressed_spool)?; - ensure_parent_exists(&request.paths.checkpoint)?; - - let mut active = self.restore_or_create(request)?; - let response = self.open_response(request, &mut active)?; - - let mut spool = open_append(&request.paths.compressed_spool)?; - let mut output = open_append(&request.paths.output)?; - - self.consume_response(request, &mut active, response, &mut spool, &mut output)?; - - spool.flush()?; - spool.get_ref().sync_all()?; - - output.flush()?; - output.get_ref().sync_all()?; - - if !active.stream_finished { - self.write_checkpoint(request, &active)?; - return Err(DownloadError::IncompleteDeflateStream); - } - - remove_if_exists(&request.paths.checkpoint)?; - remove_if_exists(&request.paths.compressed_spool)?; - - Ok(DownloadResult { - compressed_bytes: active.compressed_offset, - decompressed_bytes: active.decompressed_offset, - resumed: active.resumed, - }) - } - - fn restore_or_create( - &self, - request: &DownloadRequest, - ) -> Result { - let checkpoint = match read_checkpoint(&request.paths.checkpoint) { - Ok(checkpoint) => checkpoint, - Err(DownloadError::Io(error)) if error.kind() == io::ErrorKind::NotFound => { - truncate_or_create(&request.paths.output)?; - truncate_or_create(&request.paths.compressed_spool)?; - return Ok(ActiveDownload::fresh(request.compression, None)); - } - Err(error) => { - return self.replay_or_restart(request, Some(error)); +impl RestorableDecoder for RestorableDeflateDecoder { + fn save_state(&mut self) { + let mut buf = BytesMut::new(); + write_zlib_state(&mut buf, self.zstream()); + + match self.state_path() { + Ok(path) => { + if let Err(e) = std::fs::write(&path, buf) { + error!("Failed to write state {}: {e}", path.display()); + } else { + self.bytes_since_last = 0; + debug!("Serialized zlib state"); + } } - }; - - if checkpoint.validate(request).is_err() { - return self.replay_or_restart(request, None); - } - - if verify_file_len( - &request.paths.compressed_spool, - checkpoint.compressed_offset, - ) - .is_err() - || verify_file_len(&request.paths.output, checkpoint.decompressed_offset).is_err() - { - return self.replay_or_restart(request, None); + Err(e) => error!("Failed to resolve state path: {e}"), } - - ActiveDownload::from_checkpoint(checkpoint, request) } - fn replay_or_restart( - &self, - request: &DownloadRequest, - decode_error: Option, - ) -> Result { - let spool_len = match fs::metadata(&request.paths.compressed_spool) { - Ok(metadata) => metadata.len(), - Err(error) if error.kind() == io::ErrorKind::NotFound => { - truncate_or_create(&request.paths.output)?; - truncate_or_create(&request.paths.compressed_spool)?; - remove_if_exists(&request.paths.checkpoint)?; - return Ok(ActiveDownload::fresh(request.compression, None)); - } - Err(error) => return Err(DownloadError::Io(error)), - }; + fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError> { + let path = self + .state_path() + .map_err(|_| DecoderRestoreError::CacheEmpty)?; - if spool_len == 0 { - truncate_or_create(&request.paths.output)?; - remove_if_exists(&request.paths.checkpoint)?; - return Ok(ActiveDownload::fresh(request.compression, None)); + if !path.exists() { + debug!("No cache available."); + return Err(DecoderRestoreError::CacheEmpty); } - let mut active = ActiveDownload::fresh(request.compression, None); - let mut spool = BufReader::new(File::open(&request.paths.compressed_spool)?); - let mut sink = io::sink(); - let mut input = [0_u8; REPLAY_BUFFER_SIZE]; + info!("Got some cache!"); + let mut bytes = Bytes::from(std::fs::read(path).unwrap()); - loop { - let count = spool.read(&mut input)?; + let decompress = &mut self.inner.inner_mut().decoder_mut().inner.decompress; + decompress.reset(false); - if count == 0 { - break; - } - - self.inflate_bytes(&mut active, &input[..count], &mut sink)?; - } - - let actual_output_len = fs::metadata(&request.paths.output)?.len(); - - if actual_output_len != active.decompressed_offset { - if let Some(original_error) = decode_error { - return Err(original_error); - } - - return Err(DownloadError::ReplayOutputMismatch { - expected: actual_output_len, - actual: active.decompressed_offset, - }); + { + let zstream = decompress.get_raw(); + restore_zlib_state(&mut bytes, zstream); } + debug!("reset and restored zlib state"); - active.resumed = true; - self.write_checkpoint(request, &active)?; - Ok(active) + Ok((decompress.total_in(), decompress.total_out())) } +} - fn open_response( - &self, - request: &DownloadRequest, - active: &mut ActiveDownload, - ) -> Result { - let mut builder = self - .client - .get(request.url.clone()) - .header(ACCEPT_ENCODING, "identity"); - - if active.compressed_offset != 0 { - builder = builder.header(RANGE, format!("bytes={}-", active.compressed_offset)); - - if let Some(etag) = &active.etag { - builder = builder.header(IF_RANGE, etag); - } - } - - let response = builder.send()?; - let status = response.status(); - - self.ensure_identity_encoding(&response)?; - - match (active.compressed_offset, status) { - (0, StatusCode::OK) => { - active.etag = response_etag(&response); - Ok(response) - } - - (offset, StatusCode::PARTIAL_CONTENT) if offset != 0 => { - validate_content_range(&response, offset)?; - Ok(response) - } - - (offset, StatusCode::OK) if offset != 0 => { - // If If-Range failed, or the server ignores range requests, - // its full response cannot be appended to our old stream. - Err(DownloadError::RangeIgnored) - } +impl AsyncWrite for RestorableDeflateDecoder { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let poll_result = Pin::new(&mut self.inner).poll_write(cx, buf); - (_, status) => Err(DownloadError::HttpStatus { - status, - url: request.url.clone(), - }), + if let Poll::Ready(Ok(n)) = &poll_result { + self.update_bytes_written(*n); } - } - fn consume_response( - &self, - request: &DownloadRequest, - active: &mut ActiveDownload, - mut response: Response, - spool: &mut BufWriter, - output: &mut BufWriter, - ) -> Result<(), DownloadError> { - let mut input = [0_u8; BUFFER_SIZE]; - let mut checkpoint_after = active - .compressed_offset - .saturating_add(request.checkpoint_interval.max(1)); - - loop { - let read = response.read(&mut input)?; - - if read == 0 { - break; - } - - // Transaction order: - // 1. Make source bytes durable in the replay spool. - // 2. Advance the safe inflater state. - // 3. Make decoded output durable. - // 4. Atomically replace the state checkpoint. - spool.write_all(&input[..read])?; - - self.inflate_bytes(active, &input[..read], output)?; - - if active.compressed_offset >= checkpoint_after { - spool.flush()?; - spool.get_ref().sync_data()?; - - output.flush()?; - output.get_ref().sync_data()?; - - self.write_checkpoint(request, active)?; - - checkpoint_after = active - .compressed_offset - .saturating_add(request.checkpoint_interval.max(1)); + if self.should_save { + if let Poll::Ready(Ok(())) = Pin::new(&mut self.inner).poll_flush(cx) { + debug!("save interval reached, serializing state..."); + self.save_state(); } } - Ok(()) + poll_result } - fn inflate_bytes( - &self, - active: &mut ActiveDownload, - mut source: &[u8], - output: &mut impl Write, - ) -> Result<(), DownloadError> { - let mut destination = [0_u8; BUFFER_SIZE]; - - while !source.is_empty() { - if active.stream_finished { - return Err(DownloadError::TrailingCompressedData); - } - - let result = inflate(&mut active.inflate, source, &mut destination, MZFlush::None); - - let consumed = result.bytes_consumed; - let written = result.bytes_written; - - if written != 0 { - output.write_all(&destination[..written])?; - active.decompressed_offset += written as u64; - } - - active.compressed_offset += consumed as u64; - source = &source[consumed..]; - - match result.status { - Ok(MZStatus::Ok) => { - if consumed == 0 && written == 0 { - return Err(DownloadError::InflateNoProgress); - } - } - - Ok(MZStatus::StreamEnd) => { - active.stream_finished = true; - - if !source.is_empty() { - return Err(DownloadError::TrailingCompressedData); - } - } - - Ok(other) => { - return Err(DownloadError::Inflate(MZError::Stream)); - } - - Err(error) => return Err(DownloadError::Inflate(error)), - } - } - - Ok(()) + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) } - fn ensure_identity_encoding(&self, response: &Response) -> Result<(), DownloadError> { - let encoding = response - .headers() - .get(CONTENT_ENCODING) - .and_then(|value| value.to_str().ok()) - .map(ToOwned::to_owned); - - match encoding.as_deref() { - None | Some("identity") => Ok(()), - _ => Err(DownloadError::UnexpectedContentEncoding { encoding }), - } - } - - fn write_checkpoint( - &self, - request: &DownloadRequest, - active: &ActiveDownload, - ) -> Result<(), DownloadError> { - let checkpoint = active.checkpoint(request); - - let bytes = - postcard::to_allocvec(&checkpoint).map_err(|_| DownloadError::InvalidCheckpoint)?; - - atomic_write(&request.paths.checkpoint, &bytes)?; - Ok(()) + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) } } -pub type ProgressCallback = Arc; - -/// Reads a remote ZIP's central directory once via HTTP Range requests -/// (see `ZipFile::fetch`), then allows downloading individual entries by -/// range without fetching the whole archive. pub struct ZipDownloader { - offer_id: String, url: String, - path: PathBuf, client: Client, - zip: ZipFile, + manifest: ZipFile, } impl ZipDownloader { - pub async fn new(offer_id: &str, url: &str, path: &Path) -> Result { - let zip = ZipFile::fetch(url).await?; - + fn clear_state(file_name: &str) { + if let Some(dir) = dirs::cache_dir() { + let safe_name = file_name.replace(['/', '\\'], "_"); + let _ = std::fs::remove_file(dir.join("Maxima").join(format!("{}.state", safe_name))); + } + } + + pub async fn new(url: &str) -> Result { + let manifest = ZipFile::fetch(url).await?; Ok(Self { - offer_id: offer_id.to_owned(), url: url.to_owned(), - path: path.to_owned(), - client: Client::new(), - zip, + client: Client::builder().build()?, + manifest, }) } pub fn manifest(&self) -> &ZipFile { - &self.zip - } - - pub fn path(&self) -> &Path { - &self.path + &self.manifest } - pub fn offer_id(&self) -> &str { - &self.offer_id - } - - /// Downloads and, if needed, decompresses a single central-directory - /// entry using an HTTP Range request scoped to `[data_offset, - /// data_offset + compressed_size)`. pub async fn download_single_file( &self, entry: &ZipFileEntry, - progress: Option, - ) -> Result<(), DownloadError> { - let start = *entry.data_offset(); - let end = start + *entry.compressed_size() - 1; - - let response = self - .client - .get(&self.url) - .header("range", format!("bytes={start}-{end}")) - .send()?; - - if !response.status().is_success() { - return Err(DownloadError::UnexpectedStatus(response.status())); - } + output_dir: &Path, + ) -> Result { + let file_path = output_dir.join(entry.name()); - let dest_path = self.path.join(entry.name()); - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent)?; + if let Some(parent) = file_path.parent().filter(|p| !p.exists()) { + debug!("Creating {}", parent.display()); + create_dir_all(parent).await?; } - let mut out = File::create(&dest_path)?; + let name = entry.name().replace('\\', "/"); + let file_path = output_dir.join(&name); - match entry.compression_type() { - CompressionType::None => { - self.write_stored(response, &mut out, progress)?; - } - CompressionType::Deflate => { - self.write_deflated(response, &mut out, entry, progress)?; + if name.ends_with('/') { + if !file_path.exists() { + create_dir_all(&file_path).await?; } + return Ok(0); } - out.flush()?; - Ok(()) - } - - fn write_stored( - &self, - mut response: Response, - out: &mut File, - progress: Option, - ) -> Result<(), DownloadError> { - let mut buffer = [0_u8; BUFFER_SIZE]; - loop { - let read = response.read(&mut buffer)?; - - if read == 0 { - break; - } - - out.write_all(&buffer[..read])?; - - if let Some(cb) = &progress { - cb(read); - } + if *entry.uncompressed_size() == 0 { + debug!("{} is empty", entry.name()); + return Ok(0); } - Ok(()) - } - - fn write_deflated( - &self, - mut response: Response, - out: &mut File, - entry: &ZipFileEntry, - progress: Option, - ) -> Result<(), DownloadError> { - let mut inflate_state = InflateState::new_boxed(DataFormat::Raw); - let mut written_total: i64 = 0; - let mut input = [0_u8; BUFFER_SIZE]; - - loop { - let read = response.read(&mut input)?; + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&file_path) + .await?; - if read == 0 { - break; + let mut compressed_offset = 0; + let writer = tokio::io::BufWriter::new(file); + let mut writer: Box = match entry.compression_type() { + CompressionType::None => { + compressed_offset = tokio::fs::metadata(&file_path) + .await + .map(|m| m.len()) + .unwrap_or(0); + Box::new(writer) } + CompressionType::Deflate => { + let decoder = DeflateDecoder::new(writer); + let mut decoder = RestorableDeflateDecoder::new(decoder, entry.name().into()); + + match decoder.restore_state() { + Ok((bytes_in, _bytes_out)) => { + // bytes_in = compressed bytes consumed (from zlib state) + compressed_offset = bytes_in; // Use this for HTTP range + // Note: we don't need bytes_out here, but we keep it for set_len + decoder.get_mut().get_mut().set_len(_bytes_out).await?; + } + Err(err) => { + decoder.get_mut().get_mut().set_len(0).await?; + debug!("Failed to restore state for {}: {:?}", entry.name(), err); + } + } + Box::new(decoder) + } + }; - let compressed_len = read; - let mut source: &[u8] = &input[..read]; - let mut destination = [0_u8; INFLATE_CHUNK]; - - while !source.is_empty() { - let result = inflate(&mut inflate_state, source, &mut destination, MZFlush::None); + let offset = entry.data_offset(); + let start_offset = offset + compressed_offset as i64; // Add compressed offset, not decompressed + let end_offset = offset + entry.compressed_size() - 1; - let consumed = result.bytes_consumed; - let produced = result.bytes_written; + debug!("Requesting range: {}-{} (entry compressed size: {})", start_offset, end_offset, entry.compressed_size()); - if produced != 0 { - out.write_all(&destination[..produced])?; - written_total += produced as i64; - } - - source = &source[consumed..]; + // Ensure we don't request past the end or invalid ranges + if compressed_offset >= *entry.compressed_size() as u64 { + debug!("{} already fully downloaded, skipping", entry.name()); + writer.shutdown().await?; + Self::clear_state(entry.name()); + return Ok(compressed_offset); + } + if start_offset > end_offset { + bail!("Invalid range calculation: start {} > end {}", start_offset, end_offset); + } - match result.status { - Ok(MZStatus::Ok) => { - if consumed == 0 && produced == 0 { - return Err(DownloadError::InflateNoProgress); - } + let range = format!("bytes={}-{}", start_offset, end_offset); + debug!("Type: {:?} | range: {}", entry.compression_type(), range); + + let mut attempt = 0; + let data = loop { + attempt += 1; + match self + .client + .get(&self.url) + .header("range", range.clone()) + .send() + .await + { + Ok(res) => { + // A resuming request that gets 200 instead of 206 would + // replay the whole stream and corrupt the output. + if compressed_offset > 0 && res.status() != StatusCode::PARTIAL_CONTENT { + bail!("server ignored Range request (status {})", res.status()); + } + if !res.status().is_success() { + bail!("download failed with status {}", res.status()); } - Ok(MZStatus::StreamEnd) => break, - Ok(_) => {} - Err(err) => return Err(DownloadError::Inflate(err)), + break res; } + Err(err) if attempt < MAX_RETRIES => { + error!("Failed to download ({}) attempt {attempt}: {err}", file_path.display()); + tokio::time::sleep(std::time::Duration::from_secs(1 << attempt)).await; + } + Err(err) => return Err(err.into()), } + }; - if let Some(cb) = &progress { - cb(compressed_len); - } - } + let stream = ByteCountingStream::new(data.bytes_stream()); + let mut stream_reader = BufReader::new(stream.into_async_read().compat()); - if written_total != *entry.uncompressed_size() { - return Err(DownloadError::SizeMismatch(entry.name().clone())); - } + tokio::io::copy(&mut stream_reader, &mut writer).await?; + writer.shutdown().await?; // flush decoder + BufWriter to disk - Ok(()) + Ok(compressed_offset) } } -fn read_checkpoint(path: &Path) -> Result { - let bytes = fs::read(path)?; - - postcard::from_bytes::(&bytes).map_err(|_| DownloadError::InvalidCheckpoint) +struct ByteCountingStream { + inner: S, + byte_count: usize, } -fn validate_content_range(response: &Response, expected_start: u64) -> Result<(), DownloadError> { - let value = response - .headers() - .get(CONTENT_RANGE) - .and_then(|value| value.to_str().ok()) - .ok_or(DownloadError::InvalidContentRange)?; - - let value = value - .strip_prefix("bytes ") - .ok_or(DownloadError::InvalidContentRange)?; - - let (range, _) = value - .split_once('/') - .ok_or(DownloadError::InvalidContentRange)?; - - let (start, _) = range - .split_once('-') - .ok_or(DownloadError::InvalidContentRange)?; - - let start = start - .parse::() - .map_err(|_| DownloadError::InvalidContentRange)?; - - if start != expected_start { - return Err(DownloadError::InvalidContentRange); +impl ByteCountingStream +where + S: Stream>, +{ + fn new(inner: S) -> Self { + Self { inner, byte_count: 0 } } - - Ok(()) -} - -fn response_etag(response: &Response) -> Option { - response - .headers() - .get(ETAG) - .and_then(|value| value.to_str().ok()) - .map(ToOwned::to_owned) -} - -fn open_append(path: &Path) -> Result, DownloadError> { - Ok(BufWriter::new( - OpenOptions::new() - .create(true) - .append(true) - .read(true) - .open(path)?, - )) } -fn truncate_or_create(path: &Path) -> Result<(), DownloadError> { - File::create(path)?.sync_all()?; - Ok(()) -} - -fn verify_file_len(path: &Path, expected_minimum: u64) -> Result<(), DownloadError> { - let actual = fs::metadata(path)?.len(); - - if actual < expected_minimum { - return Err(DownloadError::CompressedSpoolTooShort); - } - - Ok(()) +#[derive(Debug, Display)] +pub enum DownloadError { + DownloadFailed(usize), } -fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), DownloadError> { - let temporary = append_suffix(path, ".tmp"); +impl std::error::Error for DownloadError {} - { - let mut file = File::create(&temporary)?; - file.write_all(bytes)?; - file.sync_all()?; - } +impl Stream for ByteCountingStream +where + S: Stream> + Unpin, +{ + type Item = io::Result; - #[cfg(windows)] - { - if path.exists() { - fs::remove_file(path)?; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + match self.inner.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(chunk))) => { + self.byte_count += chunk.len(); + Poll::Ready(Some(Ok(chunk))) + } + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(io::Error::other( + DownloadError::DownloadFailed(self.byte_count), + )))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, } } - - fs::rename(temporary, path)?; - Ok(()) -} - -fn ensure_parent_exists(path: &Path) -> Result<(), DownloadError> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - Ok(()) -} - -fn append_suffix(path: &Path, suffix: &str) -> PathBuf { - let mut value = path.as_os_str().to_os_string(); - value.push(suffix); - PathBuf::from(value) -} - -fn remove_if_exists(path: &Path) -> io::Result<()> { - match fs::remove_file(path) { - Ok(()) => Ok(()), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error), - } -} - -fn sha256(bytes: &[u8]) -> [u8; 32] { - Sha256::digest(bytes).into() -} +} \ No newline at end of file diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f1976d70..0fac996d 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -1,11 +1,12 @@ use std::{ path::PathBuf, sync::{ - Arc, atomic::{AtomicUsize, Ordering}, + Arc, }, }; +use anyhow::bail; use derive_builder::Builder; use derive_getters::Getters; use futures::StreamExt; @@ -20,14 +21,12 @@ use crate::{ content::{ ContentService, downloader::{DownloadError, ZipDownloader}, zip::{CompressionType, ZipError, ZipFileEntry}, }, core::{ - MaximaEvent, - auth::storage::LockedAuthStorage, - manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, - service_layer::ServiceLayerError, + MaximaEvent, auth::storage::LockedAuthStorage, background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::ServiceLayerError, }, util::native::{NativeError, maxima_dir}, }; const QUEUE_FILE: &str = "download_queue.json"; +const MAX_CONCURRENT_DOWNLOADS: usize = 16; #[derive(Default, Builder, Getters, Clone, Serialize, Deserialize, PartialEq)] pub struct QueuedGame { @@ -40,7 +39,6 @@ pub struct QueuedGame { pub struct DownloadQueue { current: Option, paused: bool, - queued: Vec, completed: Vec, } @@ -58,8 +56,6 @@ pub enum ContentManagerError { #[error("download in progress, you must cancel it before starting a new one")] DownloadInProgress, - #[error("offer `{0}` is not in the download queue")] - NotInQueue(String), } #[derive(Error, Debug)] @@ -87,53 +83,65 @@ pub enum DownloaderError { EntrySize { requested: u64, entry: usize }, #[error("unsupported compression type `{0:?}`")] CompressionType(CompressionType), + #[error(transparent)] + Other(#[from] anyhow::Error), } impl DownloadQueue { - pub(crate) async fn load() -> Result { + pub(crate) async fn load() -> Result { let file = maxima_dir()?.join(QUEUE_FILE); if !file.exists() { return Ok(Self::default()); } - let data = fs::read_to_string(file).await?; - let result = serde_json::from_str(&data); - if result.is_err() { - return Ok(Self::default()); + let data = fs::read_to_string(&file).await?; + match serde_json::from_str(&data) { + Ok(queue) => Ok(queue), + Err(err) => { + // Keep the corrupt file for inspection instead of silently discarding it + let backup = file.with_extension("json.bak"); + error!("Corrupt download queue, backing up to {}: {err}", backup.display()); + let _ = fs::rename(&file, &backup).await; + Ok(Self::default()) + } } - - Ok(result?) } pub(crate) async fn save(&self) -> Result<(), ContentManagerError> { let file = maxima_dir()?.join(QUEUE_FILE); - fs::write(file, serde_json::to_string(&self)?).await?; + fs::write(file, serde_json::to_string(self)?).await?; Ok(()) } pub fn push_to_current(&mut self, game: QueuedGame) { - if let Some(current) = &self.current { - self.queued.push(current.clone()); + if let Some(current) = self.current.take() { + self.queued.push(current); } + self.current = Some(game); + } - self.current = Some(game.clone()); + /// FIFO: take the oldest queued game. + fn pop_next(&mut self) -> Option { + if self.queued.is_empty() { + None + } else { + Some(self.queued.remove(0)) + } } } +type ProgressCallback = Box; + pub struct GameDownloader { offer_id: String, - downloader: Arc, entries: Vec, - + output_dir: PathBuf, cancel_token: CancellationToken, completed_bytes: Arc, total_count: usize, total_bytes: usize, notify: Arc, - error: Arc>>, - finished: Arc, - paused: Arc, } impl GameDownloader { @@ -147,60 +155,48 @@ impl GameDownloader { debug!("URL: {}", url.url()); - let downloader = ZipDownloader::new(&game.offer_id, url.url(), &game.path).await?; - - let mut entries = Vec::new(); - for ele in downloader.manifest().entries() { - // TODO: Filtering - entries.push(ele.clone()); - } + let downloader = ZipDownloader::new(url.url()).await?; + let entries = downloader.manifest().entries().to_vec(); let total_count = entries.len(); let total_bytes = entries .iter() .map(|x| *x.compressed_size() as usize) - .sum::(); // mmmph a good amount of bytes, eat that Beton Brutal + .sum::() + + 1; // +1 accounts for the touchup step at the end Ok(GameDownloader { offer_id: game.offer_id.to_owned(), - downloader: Arc::new(downloader), entries, + output_dir: game.path.clone(), cancel_token: CancellationToken::new(), completed_bytes: Arc::new(AtomicUsize::new(0)), total_count, total_bytes, notify: Arc::new(Notify::new()), - error: Arc::new(std::sync::Mutex::new(None)), - paused: Arc::new(std::sync::atomic::AtomicBool::new(false)), - finished: Arc::new(std::sync::atomic::AtomicBool::new(false)), }) } pub fn download(&self) { - let (downloader_arc, entries, cancel_token, completed_bytes, notify, error) = + let (downloader_arc, entries, cancel_token, completed_bytes, notify, output_dir) = self.prepare_download_vars(); - let total_count = self.total_count; - let finished = self.finished.clone(); + let notify_done = notify.clone(); tokio::spawn(async move { - let result = GameDownloader::start_downloads( - total_count, + let dl = GameDownloader::start_downloads( downloader_arc, entries, cancel_token, completed_bytes, - finished.clone(), + notify, + output_dir, ) .await; - - if let Err(err) = result { - error!("Error when downloading!: `{:?}`", err); - *error.lock().unwrap() = Some(err); + if let Err(err) = dl { + error!("Error when downloading!: `{err:?}`"); } - - finished.fetch_or(true, Ordering::SeqCst); - notify.notify_one(); + notify_done.notify_one(); }); } @@ -212,7 +208,7 @@ impl GameDownloader { CancellationToken, Arc, Arc, - Arc>>, + PathBuf, ) { ( self.downloader.clone(), @@ -220,54 +216,78 @@ impl GameDownloader { self.cancel_token.clone(), self.completed_bytes.clone(), self.notify.clone(), - self.error.clone(), + self.output_dir.clone(), ) } async fn start_downloads( - total_count: usize, downloader_arc: Arc, entries: Vec, cancel_token: CancellationToken, completed_bytes: Arc, - finished: Arc, + notify: Arc, + output_dir: PathBuf, ) -> Result<(), DownloaderError> { - let mut handles = Vec::with_capacity(total_count); + let mut handles = Vec::with_capacity(entries.len()); - for ele in entries.iter().take(total_count) { + for ele in entries { let downloader = downloader_arc.clone(); - let ele = ele.clone(); + let output_dir = output_dir.clone(); let cancel_token = cancel_token.clone(); let completed_bytes = completed_bytes.clone(); + handles.push(async move { + if ele.name().contains("Cleanup") { + info!("Ele: {:?}", ele); + } + + let on_progress: ProgressCallback = Box::new(move |bytes| { + completed_bytes.fetch_add(bytes, Ordering::SeqCst); + }); + tokio::select! { - result = downloader.download_single_file(&ele, Some(Arc::new(move |bytes| { - completed_bytes.fetch_add(bytes, Ordering::SeqCst); - }))) => result.map(|_| ()).map_err(DownloaderError::from), + result = downloader.download_single_file(&ele, &output_dir) => { + if let Err(err) = result { + error!("File download failed: {}", err); + } + }, _ = cancel_token.cancelled() => { info!("Download of {} cancelled", ele.name()); - Ok(()) }, } }); } - let results: Vec> = futures::stream::iter(handles) - .buffer_unordered(16) - .collect() + let _results = futures::stream::iter(handles) + .buffer_unordered(MAX_CONCURRENT_DOWNLOADS) + .collect::>() .await; - for result in results { - result?; + // If we were cancelled mid-flight, don't run touchup or report completion + if cancel_token.is_cancelled() { + info!("Download cancelled, skipping touchup"); + notify.notify_one(); + return Ok(()); } - let path = downloader_arc.path(); info!("Files downloaded, running touchup..."); - let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - manifest.run_touchup(path).await?; + let client = reqwest::Client::new(); + let resp = client + .post(format!("http://127.0.0.1:{}/touchup", BACKGROUND_SERVICE_PORT)) + .json(&ServiceTouchupRequest { + output_dir: output_dir.to_string_lossy().into_owned(), + }) + .send() + .await?; + + if !resp.status().is_success() { + let text = resp.text().await.unwrap_or_default(); + return Err(DownloaderError::Other(anyhow::anyhow!("Touchup request failed: {}", text))); + } info!("Installation finished!"); - let _ = finished; + completed_bytes.fetch_add(1, Ordering::SeqCst); + notify.notify_one(); Ok(()) } @@ -297,17 +317,9 @@ impl GameDownloader { self.total_bytes } - pub fn offer_id(&self) -> &String { + pub fn offer_id(&self) -> &str { &self.offer_id } - - pub fn take_error(&self) -> Option { - self.error.lock().unwrap().take() - } - - pub fn is_finished(&self) -> bool { - self.finished.load(Ordering::SeqCst) - } } #[derive(Getters)] @@ -333,7 +345,6 @@ impl ContentManager { self.queue.queued.push(game); self.queue.save().await?; } - Ok(()) } @@ -348,7 +359,6 @@ impl ContentManager { self.install_direct(game).await?; return Ok(()); } - self.queue.queued.push(current.clone()); } @@ -356,13 +366,13 @@ impl ContentManager { Ok(()) } + // Starts installation of a game immediately, without checking the queue. If another + // download is in progress, it will be cancelled. async fn install_direct(&mut self, game: QueuedGame) -> Result<(), ContentManagerError> { if self.current.is_some() { return Err(ContentManagerError::DownloadInProgress); } - self.queue.queued.retain(|g| g.offer_id != game.offer_id); - self.queue.current = Some(game.clone()); self.queue.save().await?; @@ -372,99 +382,111 @@ impl ContentManager { Ok(()) } + // Cancels the current download and removes the game from the queue, if present. pub async fn cancel_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { + // Stop the in-flight download first so nothing keeps writing to disk. if let Some(current) = &self.current { if current.offer_id() == offer_id { current.cancel(); + current.wait().await; // let the task settle before mutating state self.current = None; - self.queue.current = None; - self.queue.save().await?; - return Ok(()); } } - if let Some(pos) = self + if self .queue - .queued - .iter() - .position(|g| g.offer_id == offer_id) + .current + .as_ref() + .is_some_and(|g| g.offer_id() == offer_id) { - self.queue.queued.remove(pos); - self.queue.save().await?; - return Ok(()); + self.queue.current = None; } - Err(ContentManagerError::NotInQueue(offer_id.to_owned())) + self.queue.queued.retain(|g| g.offer_id() != offer_id); + self.queue.completed.retain(|g| g.offer_id() != offer_id); + self.queue.paused = false; + + self.queue.save().await?; + Ok(()) } + /// Pauses the active download. Partial files and saved zlib states stay + /// on disk, so it can be resumed later via `install_now` with the same game. pub async fn pause_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { if let Some(current) = &self.current { if current.offer_id() == offer_id { current.cancel(); + current.wait().await; self.current = None; - - let game = self - .queue - .current - .take() - .ok_or_else(|| ContentManagerError::NotInQueue(offer_id.to_owned()))?; - - self.queue.queued.insert(0, game); + self.queue.paused = true; self.queue.save().await?; - return Ok(()); } } - - Err(ContentManagerError::NotInQueue(offer_id.to_owned())) + // If it's only queued (not downloading yet), there's nothing to pause. + Ok(()) } + /// Moves a queued install to the front, pausing whatever is currently + /// downloading and starting the requested game immediately. pub async fn move_install_to_top(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { - let pos = self + if self .queue - .queued - .iter() - .position(|g| g.offer_id == offer_id); - if let Some(pos) = pos { - let game = self.queue.queued.remove(pos); - self.queue.queued.insert(0, game); - self.queue.save().await?; - return Ok(()); + .current + .as_ref() + .is_some_and(|g| g.offer_id() == offer_id) + { + return Ok(()); // already at the top } - Err(ContentManagerError::NotInQueue(offer_id.to_owned())) - } + let Some(index) = self.queue.queued.iter().position(|g| g.offer_id() == offer_id) + else { + return Ok(()); // not in the queue at all + }; - pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { - if self.current.is_none() && self.queue.current.is_none() && !self.queue.queued.is_empty() { - let game = self.queue.queued.remove(0); - self.install_direct(game).await?; + let game = self.queue.queued.remove(index); + + // Pause the current download and requeue it at the front. + if let Some(current) = self.current.take() { + current.cancel(); + current.wait().await; + } + if let Some(current) = self.queue.current.take() { + self.queue.queued.insert(0, current); } + self.install_direct(game).await + } + + pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { let mut event = None; if let Some(current) = &self.current { - if let Some(err) = current.take_error() { - let offer_id = current.offer_id().to_owned(); - self.current = None; - self.queue.current = None; - if !self.queue.queued.is_empty() { - let game = self.queue.queued.remove(0); - self.install_now(game).await?; - } - self.queue.save().await?; - event = Some(MaximaEvent::InstallFailed(offer_id, err.to_string())); - } else if current.is_finished() { - event = Some(MaximaEvent::InstallFinished(current.offer_id().to_owned())); + if current.is_done() { + let finished = self + .queue + .current + .take() + .expect("queue.current out of sync with active download"); + self.queue.completed.push(finished); + event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); self.current = None; - self.queue.current = None; - if !self.queue.queued.is_empty() { - let game = self.queue.queued.remove(0); - self.install_now(game).await?; - } + self.queue.paused = false; // Reset pause flag when done self.queue.save().await?; } } + if self.current.is_none() && !self.queue.paused { + if let Some(game) = self.queue.pop_next() { + self.install_direct(game).await?; + } + } + + if self.current.is_none() && self.queue.current.is_some() && !self.queue.paused { + if let Some(game) = self.queue.current.take() { + self.install_direct(game).await?; + } + } + Ok(event) } } \ No newline at end of file diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 74260a21..07dd545d 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -1,13 +1,9 @@ use std::time::Duration; +use anyhow::Result; use crate::core::{ - auth::storage::LockedAuthStorage, - cache::DynamicCache, - service_layer::{ - SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, - ServiceAvailableBuilds, ServiceAvailableBuildsBuilder, - ServiceAvailableBuildsRequestBuilder, ServiceDownloadUrlMetadata, - ServiceDownloadUrlRequestBuilder, ServiceLayerClient, ServiceLayerError, + auth::storage::LockedAuthStorage, cache::DynamicCache, service_layer::{ + SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, ServiceAvailableBuildsBuilder, ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, ServiceDownloadUrlMetadata, ServiceDownloadUrlRequestBuilder, ServiceLayerClient, ServiceLayerError, }, }; @@ -16,6 +12,22 @@ pub mod manager; pub mod zip; pub mod zlib; +#[derive(Clone)] +pub struct ServiceAvailableBuilds { + pub builds: Vec, +} + +impl ServiceAvailableBuilds { + pub fn live_build(&self) -> Option<&ServiceAvailableBuild> { + self.builds.iter().find(|b| { + b.download_type() + .as_ref() + .unwrap_or(&ServiceDownloadType::None) + == &ServiceDownloadType::Live + }) + } +} + pub struct ContentService { service_layer: ServiceLayerClient, request_cache: DynamicCache, @@ -37,7 +49,7 @@ impl ContentService { pub async fn available_builds( &self, - offer_id: &str, + offer_id: &str ) -> Result { let cache_key = "builds_".to_owned() + offer_id; if let Some(cached) = self.request_cache.get(&cache_key) { @@ -54,9 +66,7 @@ impl ContentService { ) .await?; - let builds = ServiceAvailableBuildsBuilder::default() - .builds(builds) - .build()?; + let builds = ServiceAvailableBuilds { builds }; self.request_cache.insert(cache_key, builds.clone()); Ok(builds) } @@ -85,4 +95,4 @@ impl ContentService { self.request_cache.insert(cache_key, url.clone()); Ok(url) } -} +} \ No newline at end of file diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index 605bbc57..5702533c 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -1,11 +1,10 @@ use bytebuffer::{ByteBuffer, Endian}; use derive_getters::Getters; -use encoding_rs::WINDOWS_1252; +use encoding_rs::{WINDOWS_1252}; use log::{debug, warn}; -use reqwest::Client; use reqwest::header::ToStrError; +use reqwest::Client; use std::cmp; -use std::path::{Component, Path}; use std::string::FromUtf8Error; use thiserror::Error; @@ -46,8 +45,6 @@ pub enum EntryError { Decode, #[error("invalid signature `{0:#10x}` (expected `{sig:#10x}`)", sig = ZIP_FILE_HEADER_SIGNATURE)] Signature(u32), - #[error("path traversal detected: {0}")] - PathTraversal(String), } #[derive(Error, Debug)] @@ -98,19 +95,6 @@ fn signature_scan_rev(data: &[u8], signature: u32) -> Option { None } -fn validate_entry_name(name: &str) -> Result<(), EntryError> { - let p = Path::new(name); - if p.is_absolute() { - return Err(EntryError::PathTraversal(name.to_string())); - } - for component in p.components() { - if component == Component::ParentDir { - return Err(EntryError::PathTraversal(name.to_string())); - } - } - Ok(()) -} - #[derive(Default, Clone, Debug, PartialEq)] pub enum CompressionType { #[default] @@ -185,9 +169,8 @@ impl ZipFileEntry { if had_errors { return Err(EntryError::Decode); } - s.to_string() + s.into_owned() }; - validate_entry_name(&entry.name)?; entry.extra_field = data.read_bytes(extra_field_len as usize)?; if let Ok(data) = entry.extra_field(0x01) { @@ -265,7 +248,7 @@ impl EndOfCentralDirectory { } let signature = data.read_u32()?; - if signature != ZIP_EOCD_SIGNATURE { + if signature as u32 != ZIP_EOCD_SIGNATURE { return Err(EOCDError::Signature(signature)); } @@ -301,12 +284,12 @@ impl EndOfCentralDirectory { data.read_u16()?; data.read_u16()?; - self.disk_number = data.read_u32()?; - self.disk_number_with_cd = data.read_u32()?; + self.disk_number = data.read_u32()? as u32; + self.disk_number_with_cd = data.read_u32()? as u32; self.disk_entries = data.read_i64()? as u64; self.total_entries = data.read_i64()? as u64; self.cd_size = data.read_i64()? as u64; - self.cd_offset = data.read_i64()?; + self.cd_offset = data.read_i64()? as i64; self.comment_length = 0; Ok(()) @@ -436,7 +419,7 @@ impl ZipFile { idx: i, total: eocd.total_entries, err: format!("{:?}", err).to_string(), - }); + }) } Ok(e) => e, }; @@ -467,4 +450,4 @@ impl ZipFile { Ok(()) } -} +} \ No newline at end of file diff --git a/maxima-lib/src/content/zlib.rs b/maxima-lib/src/content/zlib.rs index 7e342fe4..0903295b 100644 --- a/maxima-lib/src/content/zlib.rs +++ b/maxima-lib/src/content/zlib.rs @@ -1,45 +1,178 @@ +use std::{ + os::raw::{c_char, c_int, c_uint, c_ulong, c_ushort}, + ptr, +}; + use bytes::{Buf, BufMut, Bytes, BytesMut}; -use log::error; -use miniz_oxide::inflate::stream::InflateState; -use std::{mem::size_of, ptr}; +use libz_sys::{gz_headerp, z_stream as mz_stream, z_streamp}; + +pub const Z_ENOUGH_LENS: usize = 852; +pub const Z_ENOUGH_DISTS: usize = 592; +pub const Z_ENOUGH: usize = Z_ENOUGH_LENS + Z_ENOUGH_DISTS; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct ZCode { + pub op: u8, + pub bits: u8, + pub val: c_ushort, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct ZInflateState { + pub strm: z_streamp, + pub inflate_mode: c_uint, + + pub last: c_int, + pub wrap: c_int, + + pub havedict: c_int, + pub flags: c_int, + + pub dmax: c_uint, + pub check: c_ulong, + pub total: c_ulong, + pub head: gz_headerp, + + pub wbits: c_uint, + pub wsize: c_uint, + pub whave: c_uint, + pub wnext: c_uint, + pub window: *mut c_char, -const Z_MAGIC: u32 = u32::from_be_bytes(*b"ZSTB"); + pub hold: c_ulong, + pub bits: c_uint, -/// Serialize an InflateState snapshot into a buffer, -/// prefixed with a magic number and the length of the serialized data. -pub(crate) fn write_zlib_state(buf: &mut BytesMut, state: &InflateState) { - buf.put_u32(Z_MAGIC); + pub length: c_uint, + pub offset: c_uint, + pub extra: c_uint, - let len = size_of::(); - let encoded = - unsafe { std::slice::from_raw_parts((state as *const InflateState).cast::(), len) }; + pub lencode: *mut ZCode, + pub distcode: *mut ZCode, - buf.put_u32(len as u32); - buf.put_slice(encoded); + pub lenbits: c_uint, + pub distbits: c_uint, + + pub ncode: c_uint, + pub nlen: c_uint, + pub ndist: c_uint, + pub have: c_uint, + pub next: *mut ZCode, + + pub lens: [c_ushort; 320], + pub work: [c_ushort; 288], + + pub codes: [ZCode; Z_ENOUGH], + + pub sane: c_int, + pub back: c_int, + pub was: c_uint, } -pub(crate) fn restore_zlib_state(buf: &mut Bytes) -> Option> { - if buf.get_u32() != Z_MAGIC { - error!("Invalid magic number while reading zlib state"); - return None; +pub fn write_zlib_state(buf: &mut BytesMut, stream: &mut mz_stream) { + // `as u64` casts: c_ulong is u32 on Windows, u64 on 64-bit Unix. + buf.put_u64(stream.total_in as u64); + buf.put_u64(stream.total_out as u64); + buf.put_i32(stream.data_type); + buf.put_u64(stream.adler as u64); + + let state = stream.state as *mut ZInflateState; + assert!(!state.is_null(), "zlib stream has no inflate state"); + let state_ref = unsafe { &*state }; + + let size = size_of::(); + let mut blob = vec![0u8; size]; + unsafe { + ptr::copy_nonoverlapping(state as *const u8, blob.as_mut_ptr(), size); } + buf.extend_from_slice(&blob); - let len = buf.get_u32() as usize; - if len != size_of::() { - error!("Invalid InflateState size while reading zlib state: {len}"); - return None; + if !state_ref.window.is_null() { + let window_size = 1usize << state_ref.wbits; + let window = + unsafe { std::slice::from_raw_parts(state_ref.window as *const u8, window_size) }; + buf.extend_from_slice(window); } - let encoded = buf.copy_to_bytes(len); - let mut state = Box::::new(unsafe { std::mem::zeroed() }); + let base = state_ref.codes.as_ptr(); + let lencode_index = unsafe { state_ref.lencode.offset_from(base) }; + let distcode_index = unsafe { state_ref.distcode.offset_from(base) }; + let next_index = unsafe { state_ref.next.offset_from(base) }; - unsafe { - ptr::copy_nonoverlapping( - encoded.as_ptr(), - (&mut *state as *mut InflateState).cast::(), - len, + for (name, idx) in [ + ("lencode", lencode_index), + ("distcode", distcode_index), + ("next", next_index), + ] { + assert!( + (0..Z_ENOUGH as isize).contains(&idx), + "Can't serialize this zlib state, {name} out of range \ + (lencode: {lencode_index}, distcode: {distcode_index}, next: {next_index})" ); } - Some(state) + buf.put_u32(lencode_index as u32); + buf.put_u32(distcode_index as u32); + buf.put_u32(next_index as u32); + + buf.put_u32(state_ref.lenbits); + buf.put_u32(state_ref.distbits); } + +pub fn restore_zlib_state(buf: &mut Bytes, stream: &mut mz_stream) { + stream.total_in = buf.get_u64() as _; + stream.total_out = buf.get_u64() as _; + stream.data_type = buf.get_i32(); + stream.adler = buf.get_u64() as _; + + let state = stream.state as *mut ZInflateState; + assert!(!state.is_null(), "zlib stream has no inflate state"); + + let size = size_of::(); + let blob = buf.copy_to_bytes(size); + unsafe { + ptr::copy_nonoverlapping(blob.as_ptr(), state as *mut u8, size); + (*state).strm = stream; + } + + let state_ref = unsafe { &mut *state }; + + if !state_ref.window.is_null() { + let window_size = 1usize << state_ref.wbits; + let window = buf.copy_to_bytes(window_size); + + // libz-sys wraps C function pointers in Option; + let zalloc = stream.zalloc; + state_ref.window = + unsafe { zalloc(stream.opaque, 1, window_size as c_uint) as *mut c_char }; + assert!(!state_ref.window.is_null(), "zalloc failed for inflate window"); + + unsafe { + ptr::copy_nonoverlapping( + window.as_ptr(), + state_ref.window as *mut u8, + window_size, + ); + } + } + + let lencode = buf.get_u32() as isize; + let distcode = buf.get_u32() as isize; + let nextcode = buf.get_u32() as isize; + + for (name, idx) in [("lencode", lencode), ("distcode", distcode), ("next", nextcode)] { + assert!( + (0..Z_ENOUGH as isize).contains(&idx), + "Can't deserialize this zlib state, {name} out of range" + ); + } + + let base = state_ref.codes.as_mut_ptr(); + state_ref.lencode = unsafe { base.offset(lencode) }; + state_ref.distcode = unsafe { base.offset(distcode) }; + state_ref.next = unsafe { base.offset(nextcode) }; + + state_ref.lenbits = buf.get_u32(); + state_ref.distbits = buf.get_u32(); +} \ No newline at end of file diff --git a/maxima-lib/src/core/background_service_win.rs b/maxima-lib/src/core/background_service_win.rs index 59832575..891239a4 100644 --- a/maxima-lib/src/core/background_service_win.rs +++ b/maxima-lib/src/core/background_service_win.rs @@ -16,6 +16,11 @@ pub struct ServiceLibraryInjectionRequest { pub path: String, } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ServiceTouchupRequest { + pub output_dir: String, +} + pub async fn request_library_injection( pid: u32, path: &str, diff --git a/maxima-service/src/service/error.rs b/maxima-service/src/service/error.rs index 7c713275..cc64301d 100644 --- a/maxima-service/src/service/error.rs +++ b/maxima-service/src/service/error.rs @@ -13,6 +13,8 @@ pub enum ServerError { Native(#[from] maxima::util::native::NativeError), #[error(transparent)] Service(#[from] windows_service::Error), + #[error(transparent)] + Touchup(#[from] maxima::core::manifest::ManifestError), #[error("attempted to inject into invalid process")] InvalidInjectionTarget, diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 07b234d3..cdf0a0db 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -8,7 +8,7 @@ use std::time::Duration; use actix_web::{get, post, web, HttpResponse, Responder}; use log::{error, info}; use maxima::core::background_service::{ - ServiceLibraryInjectionRequest, BACKGROUND_SERVICE_PORT, + BACKGROUND_SERVICE_PORT, ServiceLibraryInjectionRequest, ServiceTouchupRequest, }; use maxima::util::dll_injector::{DllInjector, InjectionError}; use maxima::util::native::SafeParent; @@ -284,6 +284,22 @@ async fn req_inject_library(body: web::Bytes) -> Result Result { + info!("Running touchup"); + + let request: ServiceTouchupRequest = serde_json::from_slice(&body)?; + let manifest = maxima::core::manifest::read( + Path::new(&request.output_dir).join(maxima::core::manifest::MANIFEST_RELATIVE_PATH), + ) + .await?; + manifest.run_touchup(Path::new(&request.output_dir)).await?; + + Ok(HttpResponse::Ok().body("Done")) +} + fn run_(_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<()>) { actix_web::rt::System::new().block_on(async move { use actix_web::{App, HttpServer}; @@ -292,6 +308,7 @@ fn run_(_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<()>) { App::new() .service(req_set_up_registry) .service(req_inject_library) + .service(req_touchup) }) .bind(("127.0.0.1", BACKGROUND_SERVICE_PORT)) { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 2377b3ed..a5d62c62 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,5 +1,6 @@ use egui::Context; use log::{error, info, warn}; +use thiserror::Error; use crate::{ GameDetails, GameInfo, GameSettings, @@ -111,7 +112,7 @@ pub struct BridgeThread { pub rtm_commander: UnboundedSender, } -#[derive(thiserror::Error, Debug)] +#[derive(Error, Debug)] pub enum BackendError { #[error(transparent)] Auth(#[from] AuthError), @@ -147,6 +148,8 @@ pub enum BackendError { ServiceLayer(#[from] ServiceLayerError), #[error(transparent)] Token(#[from] TokenError), + #[error(transparent)] + Other(#[from] anyhow::Error), #[error("backend-frontend communication channel disconnected")] ChannelDisconnected, From 920fbdada2b0a6d22ae13eddb181939465a3c53b 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 36/57] File Exclusion --- Cargo.lock | 24 +++++++++++++++ maxima-lib/Cargo.toml | 1 + maxima-lib/src/content/exclusion.rs | 46 +++++++++++++++++++++++++++++ maxima-lib/src/content/manager.rs | 15 +++++++++- maxima-lib/src/content/mod.rs | 1 + 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 maxima-lib/src/content/exclusion.rs diff --git a/Cargo.lock b/Cargo.lock index 6a99dcc1..6d52db78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,6 +718,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "serde_core", +] + [[package]] name = "built" version = "0.8.1" @@ -2368,6 +2378,19 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "glow" version = "0.17.0" @@ -3440,6 +3463,7 @@ dependencies = [ "generic-array 1.4.4", "gethostname", "glob", + "globset", "hex", "http 1.5.0", "is_elevated", diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 10b8783e..fbd23d77 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -68,6 +68,7 @@ gethostname = "1.1.0" thiserror = { workspace = true } url = { workspace = true } http = "1.4.2" +globset = "0.4" generic-array = "1.4.4" rustls-pki-types = "1.15.1" tokio-stream = "0.1.19" 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 0fac996d..12d34f80 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -1,3 +1,4 @@ +use crate::content::exclusion::get_exclusion_list; use std::{ path::PathBuf, sync::{ @@ -16,6 +17,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; +use globset::GlobSet; use crate::{ content::{ @@ -156,7 +158,18 @@ impl GameDownloader { debug!("URL: {}", url.url()); let downloader = ZipDownloader::new(url.url()).await?; - let entries = downloader.manifest().entries().to_vec(); + let exclusion_list = get_exclusion_list(game.offer_id.clone()); + let mut entries = Vec::new(); + 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()); + } let total_count = entries.len(); let total_bytes = entries diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 07dd545d..6c083418 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -11,6 +11,7 @@ pub mod downloader; pub mod manager; pub mod zip; pub mod zlib; +pub mod exclusion; #[derive(Clone)] pub struct ServiceAvailableBuilds { From 600a5f048b6e1d11cc45e48802acb9c0a2683ed4 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 37/57] Per game wine prefixes Fix all compile errors Fix winit compile error on linux Put rustls into virtual workspace Fix GUI not launching on Linux Run touchup only when needed, set CWD before game launch to fix crashing --- Cargo.lock | 385 +++++++++++++++++- Cargo.toml | 3 +- maxima-bootstrap/src/main.rs | 3 +- maxima-cli/Cargo.toml | 1 + maxima-cli/src/main.rs | 77 +++- maxima-lib/Cargo.toml | 2 +- maxima-lib/src/content/exclusion.rs | 48 +-- maxima-lib/src/content/manager.rs | 44 +- maxima-lib/src/content/mod.rs | 2 +- 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 | 18 +- maxima-lib/src/core/cloudsync.rs | 34 +- maxima-lib/src/core/launch.rs | 48 ++- maxima-lib/src/core/library.rs | 102 ++--- maxima-lib/src/core/manifest/dip.rs | 29 +- maxima-lib/src/core/manifest/mod.rs | 10 +- maxima-lib/src/core/manifest/pre_dip.rs | 31 +- maxima-lib/src/gameinfo/mod.rs | 88 ++++ maxima-lib/src/lib.rs | 1 + maxima-lib/src/lsx/connection.rs | 18 +- maxima-lib/src/lsx/request/license.rs | 3 +- maxima-lib/src/ooa/mod.rs | 32 +- maxima-lib/src/unix/wine.rs | 79 ++-- maxima-lib/src/util/registry.rs | 100 +++-- maxima-resources/Cargo.toml | 2 +- maxima-service/src/service/mod.rs | 2 +- maxima-ui/Cargo.toml | 5 +- maxima-ui/res/locale/en_us.json | 3 +- maxima-ui/src/bridge/get_games.rs | 9 +- maxima-ui/src/bridge_thread.rs | 26 +- maxima-ui/src/main.rs | 58 ++- maxima-ui/src/translation_manager.rs | 2 + maxima-ui/src/views/settings_view.rs | 17 + 34 files changed, 1005 insertions(+), 300 deletions(-) create mode 100644 maxima-lib/src/gameinfo/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6d52db78..ac6a5e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "accesskit" version = "0.24.1" @@ -229,6 +245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "serde", "version_check", @@ -431,6 +448,12 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "as-slice" version = "0.2.1" @@ -606,7 +629,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -615,6 +647,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bit_field" version = "0.10.3" @@ -720,9 +758,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "serde_core", @@ -847,6 +885,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "calloop-wayland-source" version = "0.4.1" @@ -1014,6 +1064,15 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "unicode-width", +] + [[package]] name = "color" version = "0.3.3" @@ -1657,6 +1716,7 @@ dependencies = [ "bytemuck", "document-features", "egui", + "egui-wgpu", "egui-winit", "egui_glow", "glow", @@ -1705,6 +1765,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "egui-wgpu" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e6cfac0725563555fa4f91e9f799b9d7c6c5dd831fca6abc8234afc64b7a34" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.19", + "type-map", + "web-time", + "wgpu", + "winit", +] + [[package]] name = "egui-winit" version = "0.35.0" @@ -1757,6 +1837,7 @@ dependencies = [ "log", "memoffset", "profiling", + "winit", ] [[package]] @@ -1977,7 +2058,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ - "bit-set", + "bit-set 0.5.3", "regex", ] @@ -2380,9 +2461,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -2414,6 +2495,7 @@ dependencies = [ "cgl", "dispatch2", "glutin_egl_sys", + "glutin_glx_sys", "glutin_wgl_sys", "libloading", "objc2 0.6.4", @@ -2422,7 +2504,9 @@ dependencies = [ "objc2-foundation 0.3.2", "once_cell", "raw-window-handle", + "wayland-sys", "windows-sys 0.52.0", + "x11-dl", ] [[package]] @@ -2447,6 +2531,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + [[package]] name = "glutin_wgl_sys" version = "0.6.1" @@ -2492,6 +2586,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", "zerocopy", ] @@ -2579,6 +2674,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "home" version = "0.5.12" @@ -3428,6 +3529,7 @@ dependencies = [ "maxima-resources", "regex", "reqwest", + "rustls", "serde", "serde_json", "tokio", @@ -3598,6 +3700,7 @@ dependencies = [ "thiserror 2.0.19", "tokio", "windows-sys 0.61.2", + "winit", ] [[package]] @@ -3736,6 +3839,31 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "thiserror 2.0.19", + "unicode-ident", +] + [[package]] name = "ndk" version = "0.9.0" @@ -3916,6 +4044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4435,6 +4564,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "owo-colors" version = "4.3.0" @@ -4759,6 +4897,15 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -4988,7 +5135,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2 0.6.5", "thiserror 2.0.19", @@ -5010,7 +5157,7 @@ dependencies = [ "rand 0.10.2", "rand_pcg", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -5400,6 +5547,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.13.4" @@ -5505,6 +5658,12 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -5665,6 +5824,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -5948,6 +6120,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + [[package]] name = "smithay-client-toolkit" version = "0.20.0" @@ -5956,7 +6153,7 @@ checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ "bitflags 2.13.1", "calloop 0.14.4", - "calloop-wayland-source", + "calloop-wayland-source 0.4.1", "cursor-icon", "libc", "log", @@ -5982,7 +6179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" dependencies = [ "libc", - "smithay-client-toolkit", + "smithay-client-toolkit 0.20.0", "wayland-backend", ] @@ -6742,6 +6939,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + [[package]] name = "typeid" version = "1.0.3" @@ -7203,6 +7415,19 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-wlr" version = "0.3.12" @@ -7371,6 +7596,119 @@ dependencies = [ "wezterm-dynamic", ] +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.1", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.13.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.19", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libloading", + "log", + "naga", + "portable-atomic", + "portable-atomic-util", + "raw-window-handle", + "renderdoc-sys", + "thiserror 2.0.19", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.1" @@ -7700,10 +8038,12 @@ version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ + "ahash", "android-activity", "atomic-waker", "bitflags 2.13.1", "block2 0.5.1", + "bytemuck", "calloop 0.13.0", "cfg_aliases", "concurrent-queue", @@ -7713,24 +8053,34 @@ dependencies = [ "dpi", "js-sys", "libc", + "memmap2", "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit 0.2.2", "orbclient", + "percent-encoding", "pin-project", "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", "smol_str", "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", "web-sys", "web-time", "windows-sys 0.52.0", + "x11-dl", + "x11rb", "xkbcommon-dl", ] @@ -7789,13 +8139,28 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "x11rb" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ + "as-raw-xcb-connection", "gethostname", + "libc", + "libloading", + "once_cell", "rustix 1.1.4", "x11rb-protocol", ] diff --git a/Cargo.toml b/Cargo.toml index 56efde8b..920675e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ tokio = { version = "1.53.1", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" +rustls = { version = "0.23.42", features = ["aws-lc-rs"] } reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } base64 = "0.23.0" thiserror = "2.0.19" @@ -63,4 +64,4 @@ debug = true split-debuginfo = "unpacked" [profile.dev.package."*"] -opt-level = 1 \ No newline at end of file +opt-level = 1 diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index 407c27c7..5dcc957e 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -127,11 +127,12 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { use maxima::unix::wine::run_wine_command; run_wine_command( - args.path, + args.path.into(), Some(args.args), None, false, CommandType::WaitForExitAndRun, + Some(&args.slug), ) .await?; diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index cee2bc6e..ac549093 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -17,6 +17,7 @@ regex = { workspace = true } inquire = { workspace = true } futures = { workspace = true } reqwest = { workspace = true } +rustls = { workspace = true } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true } diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index c8e4faf9..d4447a8f 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -30,7 +30,14 @@ use maxima::{ }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceLegacyOffer, ServicePlayer, }, - }, ooa, rtm::client::BasicPresence, util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, + }, + ooa, + rtm::client::BasicPresence, + util::{ + log::init_logger, + native::{maxima_dir, take_foreground_focus}, + registry::check_registry_validity, + }, }; static MANUAL_LOGIN_PATTERN: LazyLock = @@ -56,6 +63,7 @@ enum Mode { ListGames, LocateGame { path: String, + slug: String, }, CloudSync { game_slug: String, @@ -111,6 +119,7 @@ struct Args { #[tokio::main] async fn main() { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); if let Err(e) = startup().await { match std::env::var("RUST_BACKTRACE") { Ok(_) => error!("{}:\n{}", e, e.backtrace()), @@ -263,13 +272,21 @@ async fn startup() -> Result<()> { Err(err) => bail!("Error fetching offer for slug `{}`: {}", slug, err), } } 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 } @@ -323,10 +340,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; @@ -334,17 +350,19 @@ 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(); + 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(()) } @@ -352,7 +370,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 { @@ -368,10 +386,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() @@ -389,6 +413,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(()); @@ -398,6 +434,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(); @@ -582,7 +620,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 @@ -772,6 +810,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, diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index fbd23d77..5edd5981 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -53,7 +53,7 @@ encoding_rs = "0.8.35" tokio-util = { version = "0.7.19", features = ["io", "compat"] } uuid = "1.24.0" tokio-rustls = "0.26.4" -rustls = "0.23.42" +rustls = { workspace = true } webpki-roots = "1.0.9" aes = "0.9.2" ecb = { version = "0.2.0", features = ["alloc", "block-padding", "zeroize"] } diff --git a/maxima-lib/src/content/exclusion.rs b/maxima-lib/src/content/exclusion.rs index c685869e..a47f6d5a 100644 --- a/maxima-lib/src/content/exclusion.rs +++ b/maxima-lib/src/content/exclusion.rs @@ -1,46 +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}; -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(slug: &str) -> 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 + 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() - { + 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 12d34f80..fd2f52f2 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -17,16 +17,27 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{fs, sync::Notify}; use tokio_util::sync::CancellationToken; -use globset::GlobSet; use crate::{ content::{ - ContentService, downloader::{DownloadError, ZipDownloader}, zip::{CompressionType, ZipError, ZipFileEntry}, - }, core::{ - MaximaEvent, auth::storage::LockedAuthStorage, background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::ServiceLayerError, - }, util::native::{NativeError, maxima_dir}, + ContentService, + downloader::{DownloadError, ZipDownloader}, + exclusion::get_exclusion_list, + zip::{CompressionType, ZipError, ZipFileEntry}, + }, + core::{ + MaximaEvent, + auth::storage::LockedAuthStorage, + manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, + service_layer::ServiceLayerError, + }, + gameinfo::GameInstallInfo, + util::native::{NativeError, maxima_dir}, }; +#[cfg(unix)] +use crate::core::launch::mx_linux_setup; + const QUEUE_FILE: &str = "download_queue.json"; const MAX_CONCURRENT_DOWNLOADS: usize = 16; @@ -35,6 +46,8 @@ pub struct QueuedGame { offer_id: String, build_id: String, path: PathBuf, + slug: String, + wine_prefix: Option, } #[derive(Default, Getters, Serialize, Deserialize)] @@ -136,6 +149,9 @@ type ProgressCallback = Box; pub struct GameDownloader { offer_id: String, + slug: String, + path: PathBuf, + wine_prefix: Option, downloader: Arc, entries: Vec, output_dir: PathBuf, @@ -163,9 +179,8 @@ impl GameDownloader { for ele in downloader.manifest().entries() { // TODO: Filtering - if exclusion_list.is_match(&ele.name()) - { - info!("Excluding file from download: {}", ele.name()); + 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()); @@ -180,6 +195,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, output_dir: game.path.clone(), @@ -196,6 +214,8 @@ impl GameDownloader { self.prepare_download_vars(); let notify_done = notify.clone(); + 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( downloader_arc, @@ -204,6 +224,8 @@ impl GameDownloader { completed_bytes, notify, output_dir, + slug, + game_install_info, ) .await; if let Err(err) = dl { @@ -240,6 +262,8 @@ impl GameDownloader { completed_bytes: Arc, notify: Arc, output_dir: PathBuf, + slug: String, + game_install_info: GameInstallInfo, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(entries.len()); @@ -283,6 +307,8 @@ impl GameDownloader { return Ok(()); } + #[cfg(unix)] + mx_linux_setup(Some(&slug)).await?; // Wine Prefix needs setup before touchup info!("Files downloaded, running touchup..."); let client = reqwest::Client::new(); let resp = client @@ -502,4 +528,4 @@ impl ContentManager { Ok(event) } -} \ No newline at end of file +} diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 6c083418..9cc6b5b4 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -8,10 +8,10 @@ use crate::core::{ }; pub mod downloader; +pub mod exclusion; pub mod manager; pub mod zip; pub mod zlib; -pub mod exclusion; #[derive(Clone)] pub struct ServiceAvailableBuilds { diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index da48d516..c5e8129b 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; @@ -148,7 +148,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") { @@ -166,7 +166,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; @@ -245,7 +245,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::{ @@ -272,7 +272,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,17 +476,16 @@ 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::{DateTime, TimeZone}; 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 slug { + Some(slug) => wine_prefix_dir(Some(slug)).unwrap(), + None => 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; 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 0af28dcb..b7fb943e 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 { @@ -38,11 +42,12 @@ 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 { @@ -76,11 +85,12 @@ pub async fn request_library_injection(pid: u32, path: &str) -> Result<(), Nativ module_path()? .safe_parent()? .join("wine-helper.exe") - .safe_str()?, + .safe_str()?.into(), Some(vec!["inject", b64.as_str()]), None, false, CommandType::RunInPrefix, + slug, ) .await?; diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index fc05430d..37c86049 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -98,27 +98,29 @@ 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()), )) } #[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) + .unwrap() + .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 +129,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 +198,7 @@ pub struct CloudSyncLock<'a> { manifest: CloudSyncManifest, mode: CloudSyncLockMode, allowed_files: Vec, + slug: String, } impl<'a> CloudSyncLock<'a> { @@ -206,6 +209,7 @@ impl<'a> CloudSyncLock<'a> { lock: String, mode: CloudSyncLockMode, allowed_files: Vec, + slug: &str, ) -> Result { let res = client.get(manifest_url).send().await?; @@ -233,6 +237,7 @@ impl<'a> CloudSyncLock<'a> { manifest, mode, allowed_files, + slug: slug.to_owned(), }) } @@ -269,7 +274,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 +414,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, @@ -569,11 +574,12 @@ impl CloudSyncClient { multiplayer_id ); + 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?; @@ -588,7 +594,7 @@ impl CloudSyncClient { return Err(CloudSyncError::NoConfig(offer.offer_id().clone())); } - self.obtain_lock_raw(&id, mode, allowed_files).await + self.obtain_lock_raw(&id, mode, allowed_files, &slug).await } pub async fn obtain_lock_raw( @@ -596,7 +602,8 @@ impl CloudSyncClient { id: &str, mode: CloudSyncLockMode, allowed_files: Vec, - ) -> Result, CloudSyncError> { + slug: &str, + ) -> Result { let (token, user_id) = acquire_auth(&self.auth).await?; let res = self @@ -623,6 +630,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 85b0ec3c..03304d9b 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}; @@ -109,6 +113,7 @@ pub struct ActiveGameContext { launch_id: String, game_path: String, content_id: String, + slug: Option, offer: Option, mode: LaunchMode, injections: Vec, @@ -130,11 +135,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(), @@ -157,6 +164,7 @@ impl ActiveGameContext { pub struct BootstrapLaunchArgs { pub path: String, pub args: Vec, + pub slug: String, } impl Display for LaunchMode { @@ -231,8 +239,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(_) => {} @@ -240,13 +254,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"); } @@ -277,8 +297,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"); } @@ -302,6 +328,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)?); @@ -372,6 +399,7 @@ pub async fn start_game( offer, mode, child, + slug, )); Ok(()) @@ -398,12 +426,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, setup_wine_registry, + install_wine, setup_wine_registry, wine_prefix_dir, }; + 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(); @@ -420,8 +449,7 @@ pub async fn mx_linux_setup() -> Result<(), NativeError> { } } - 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 9bec5486..08128060 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -11,11 +11,13 @@ use super::{ ServiceUserGameProduct, }, }; -#[cfg(unix)] -use crate::unix::fs::case_insensitive_path; -use crate::util::native::{NativeError, SafeStr}; -use crate::util::registry::{RegistryError, parse_partial_registry_path, parse_registry_path}; +use crate::util::registry::{RegistryError, parse_registry_path_json}; +use crate::{ + gameinfo::load_game_info_from_json, + util::native::{NativeError, SafeStr, maxima_dir}, +}; use derive_getters::Getters; +use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; @@ -56,37 +58,55 @@ pub struct OwnedOffer { } impl OwnedOffer { - pub async fn is_installed(&self) -> bool { + #[cfg(windows)] + pub async fn check_install_win_registry(&self) -> bool { + use crate::util::registry::parse_registry_path_regkey; + let Some(path) = &self.offer.install_check_override().as_ref() else { log::warn!("[{}] No install_check_override", self.slug); return false; }; - let path = match parse_registry_path(path).await { - Ok(p) => p, - Err(_) => { - 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); + manifest_path.exists() + } else { + false + } + } + + pub async fn is_installed(&self) -> bool { + let maxima_dir = match maxima_dir() { + Ok(dir) => dir, + Err(_) => return false, }; - if path.starts_with("[") { - log::warn!("[{}] Registry key unresolved: {:?}", self.slug, path); - return false; + + 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, } - let exists = path.exists(); - log::debug!( - "[{}] install check path {:?} exists={}", - self.slug, - path, - exists - ); - exists + + #[cfg(not(windows))] + game_info_path.exists() } + // This is unused pub async fn install_check_path(&self) -> Result { - Ok(parse_registry_path( - self.offer + Ok(parse_registry_path_json( + &self + .offer .install_check_override() .as_ref() .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, + Some(&self.slug), ) .await? .safe_str()? @@ -106,7 +126,7 @@ impl OwnedOffer { }; if let Some(path) = path { - Ok(parse_registry_path(path).await?) + Ok(parse_registry_path_json(path, Some(&self.slug)).await?) } else { Err(LibraryError::NoPath(self.slug.clone())) } @@ -130,33 +150,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()))?, - ) - .await? - .safe_str()? - .to_owned(), - ); - - path.join(MANIFEST_RELATIVE_PATH) + info!("Checking local manifest for `{}`", 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 }; + let path = game_install_info.path().join(MANIFEST_RELATIVE_PATH); + info!("Checking for manifest at `{}`", path.display()); + if !path.exists() { + return Ok(None); + } Ok(Some(manifest::read(path).await?)) } diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 21b00933..078e8f3a 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -316,30 +316,33 @@ impl DiPManifest { } #[cfg(unix)] - pub async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { - use crate::{ - core::launch::mx_linux_setup, - unix::{ - fs::case_insensitive_path, - wine::{CommandType, invalidate_mx_wine_registry, run_wine_command}, - }, + pub async fn run_touchup( + &self, + install_path: &PathBuf, + slug: &str, + ) -> Result<(), ManifestError> { + use crate::unix::{ + fs::case_insensitive_path, + wine::{CommandType, invalidate_mx_wine_registry, run_wine_command}, }; - mx_linux_setup().await?; - let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, )); let args = self.collect_touchup_args(&install_path, self.locale())?; - let path = case_insensitive_path(install_path.join(self.touchup.path())); - - run_wine_command(path, Some(args), None, true, CommandType::Run).await?; + let path = install_path.join(&self.touchup.path()); + let path = case_insensitive_path(path).to_string_lossy().to_string(); + run_wine_command(path.into(), 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: &Path) -> 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 c1788019..9d295c0f 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -35,16 +35,16 @@ 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: &Path) -> Result<(), ManifestError>; + async fn run_touchup(&self, install_path: &Path, slug: &str) -> Result<(), ManifestError>; fn execute_path(&self, trial: bool) -> Option; fn version(&self) -> Option; fn needs_touchup_on_locate(&self) -> bool; } #[async_trait::async_trait] impl GameManifest for DiPManifest { - async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { + async fn run_touchup(&self, install_path: &Path, slug: &str) -> Result<(), ManifestError> { let install_path = install_path.to_path_buf(); - self.run_touchup(&install_path).await + self.run_touchup(&install_path, slug).await } fn execute_path(&self, trial: bool) -> Option { @@ -66,9 +66,9 @@ impl GameManifest for DiPManifest { #[async_trait::async_trait] impl GameManifest for PreDiPManifest { - async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { + async fn run_touchup(&self, install_path: &Path, slug: &str) -> Result<(), ManifestError> { let install_path = install_path.to_path_buf(); - self.run_touchup(&install_path).await + 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 5954c4de..20b40cde 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -246,32 +246,35 @@ impl PreDiPManifest { } #[cfg(unix)] - pub async fn run_touchup(&self, install_path: &Path) -> Result<(), ManifestError> { - use crate::{ - core::launch::mx_linux_setup, - unix::{ - fs::case_insensitive_path, - wine::{CommandType, invalidate_mx_wine_registry, run_wine_command}, - }, + pub async fn run_touchup( + &self, + install_path: &PathBuf, + slug: &str, + ) -> Result<(), ManifestError> { + use crate::unix::{ + fs::case_insensitive_path, + wine::{CommandType, invalidate_mx_wine_registry, run_wine_command}, }; - mx_linux_setup().await?; - let install_path = PathBuf::from(remove_trailing_slash( install_path.to_str().ok_or(ManifestError::Decode)?, )); let args = self.collect_touchup_args(&install_path, self.locale())?; - let path = case_insensitive_path( - install_path.join(remove_leading_slash(&self.executable.filePath)), - ); - run_wine_command(path, Some(args), None, true, CommandType::Run).await?; + let path = install_path.join(remove_leading_slash(&self.executable.filePath)); + let path = case_insensitive_path(path).to_string_lossy().to_string(); + run_wine_command(path.into(), 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: &Path) -> 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/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs new file mode 100644 index 00000000..698d148f --- /dev/null +++ b/maxima-lib/src/gameinfo/mod.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Deserializer, 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), +} + +// The serializers are for making sure that None goes to and from an empty string + +fn prefix_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))) + } +} + +fn prefix_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 { + pub path: PathBuf, + #[serde( + deserialize_with = "prefix_from_string", + serialize_with = "prefix_to_string" + )] + pub wine_prefix: Option, +} + +impl GameInstallInfo { + pub fn new(path: PathBuf, wine_prefix: Option) -> Self { + Self { path, wine_prefix } + } + + pub fn path(&self) -> PathBuf { + self.path.clone() + } + + 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 + 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_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)); + 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 7236c077..75abdb8e 100644 --- a/maxima-lib/src/lib.rs +++ b/maxima-lib/src/lib.rs @@ -1,5 +1,6 @@ pub mod content; pub mod core; +pub mod gameinfo; pub mod lsx; pub mod ooa; pub mod rtm; diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 4a3d5719..d9904266 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -185,15 +185,22 @@ 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 { @@ -237,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 f577cc7f..1f4e0991 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 e15e8808..3ef269e0 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -9,6 +9,8 @@ use base64::{DecodeError, Engine, engine::general_purpose}; use crate::core::{auth::hardware::HardwareInfo, endpoints::API_PROXY_NOVAFUSION_LICENSES}; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; +#[cfg(unix)] +use crate::util::native::SafeStr; use crate::util::native::{NativeError, SafeParent}; use quick_xml::DeError; use regex::Regex; @@ -164,8 +166,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); } @@ -192,6 +197,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(); @@ -205,7 +211,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(), @@ -214,7 +220,7 @@ pub async fn request_and_save_license( None, ) .await?; - save_licenses(&license, state).await?; + save_licenses(&license, state, slug).await?; Ok(()) } @@ -330,8 +336,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( @@ -352,20 +362,20 @@ pub async fn save_licenses(license: &License, state: OOAState) -> Result<(), Lic } #[cfg(windows)] -pub fn get_license_dir() -> Result { - let path = format!("C:/{}", LICENSE_PATH); +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)) } #[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()?, - LICENSE_PATH + 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 71b557ef..1eb149df 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -1,14 +1,18 @@ +use regex::Regex; +use std::ffi::OsString; +use std::fs::create_dir_all; +use std::path::PathBuf; +use std::sync::LazyLock; use std::{ collections::HashMap, env, ffi::OsStr, - fs::{File, create_dir_all, remove_dir_all, remove_file}, + fs::{File, remove_dir_all, remove_file}, io::Read, - path::PathBuf, + path::{Path, PathBuf}, process::{ExitStatus, Stdio}, }; -use flate2::read::GzDecoder; use log::{info, warn}; use regex::Regex; use reqwest::StatusCode; @@ -21,10 +25,15 @@ use tokio::{ }; use xz2::read::XzDecoder; -use crate::util::{ - github::{GithubRelease, fetch_github_release, fetch_github_releases, github_download_asset}, - native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, - registry::RegistryError, +use crate::{ + gameinfo::load_game_info_from_json, + util::{ + github::{ + GithubRelease, fetch_github_release, fetch_github_releases, github_download_asset, + }, + native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, + registry::RegistryError, + }, }; static PROTON_PATTERN: LazyLock = LazyLock::new(|| { @@ -69,9 +78,20 @@ struct Versions { umu: String, } -/// Returns internal prtoton pfx path -pub fn wine_prefix_dir() -> Result { - Ok(maxima_dir()?.join("wine/prefix")) +/// Returns internal proton pfx path +pub fn wine_prefix_dir(slug: Option<&str>) -> Result { + 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() {} + 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 { @@ -183,7 +203,7 @@ pub(crate) async fn install_runtime( }; let res = match ureq::get(&runtime.url) - .set("User-Agent", "ArmchairDevelopers/Maxima") + .header("User-Agent", "ArmchairDevelopers/Maxima") .call() { Err(err) => return Err(NativeError::Download(DownloadError::Request1(err))), @@ -195,7 +215,7 @@ pub(crate) async fn install_runtime( } let mut body: Vec = vec![]; - res.into_reader().read_to_end(&mut body)?; + res.into_body().as_reader().read_to_end(&mut body)?; if path.exists() { remove_dir_all(&path)?; @@ -234,17 +254,19 @@ fn get_wine_release() -> Result { } pub async fn run_wine_command, T: AsRef>( - arg: T, + arg: OsString, args: Option, 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).unwrap(); 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()); @@ -260,7 +282,7 @@ pub async fn run_wine_command, T: AsRef>( .env("UMU_ZENITY", "1") .env("WINEDEBUG", "fixme-all") .env("LD_PRELOAD", "") // Fixes some log errors for some games - .arg(arg); + .arg(&arg); if !wine_path.ends_with("umu-run") { // wsock32 is used as a proxy for Northstar (Titanfall 2). TODO: provide user-facing option for this! @@ -276,6 +298,11 @@ pub async fn run_wine_command, T: AsRef>( if let Some(cwd) = cwd { child.current_dir(cwd); + } else if arg.to_string_lossy().contains("/") { // Some games (like Mass Effect LE) require the working directory to be set to the game directory, other it crashes + let mut cwd_dir = PathBuf::from(arg.clone()); + cwd_dir.pop(); + info!("Setting current working directory to: {:?}", cwd_dir); + child.current_dir(cwd_dir); } let status: ExitStatus; @@ -329,8 +356,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(()) } @@ -375,7 +400,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 @@ -421,11 +446,12 @@ pub async fn setup_wine_registry() -> Result<(), NativeError> { } run_wine_command( - "regedit", + "regedit".into(), Some(vec![path.safe_str()?]), None, false, CommandType::Run, + slug, ) .await?; @@ -473,8 +499,11 @@ 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("system.reg"); +pub async fn parse_mx_wine_registry(slug: Option<&str>) -> Result { + let path = wine_prefix_dir(slug) + .unwrap() + .join("pfx") + .join("system.reg"); if !path.exists() { return Ok(HashMap::new()); } @@ -497,8 +526,12 @@ 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?; +#[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>, +) -> 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 f5c8e3f0..f63f5fe5 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -1,3 +1,7 @@ +use log::info; +use std::path::PathBuf; +use thiserror::Error; + #[cfg(windows)] use std::ptr; @@ -6,10 +10,10 @@ use widestring::U16CString; #[cfg(windows)] use windows_sys::Win32::{ - Foundation::{ERROR_CANCELLED, CloseHandle, GetLastError}, + Foundation::{CloseHandle, ERROR_CANCELLED, GetLastError}, System::Registry::{HKEY, KEY_QUERY_VALUE, RegCloseKey, RegOpenKeyExW, RegQueryValueExW}, + System::Threading::{INFINITE, WaitForSingleObject}, UI::Shell::{SEE_MASK_NO_CONSOLE, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, ShellExecuteExW}, - System::Threading::{WaitForSingleObject, INFINITE} }; #[cfg(windows)] @@ -18,11 +22,10 @@ use winreg::{ enums::{HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE}, }; -use std::path::PathBuf; #[cfg(unix)] use std::{collections::HashMap, env, fs}; -use thiserror::Error; +use crate::gameinfo::load_game_info_from_json; #[cfg(unix)] use crate::unix::fs::case_insensitive_path; @@ -67,32 +70,6 @@ pub enum RegistryError { QrcUnregistered, } -#[cfg(windows)] -pub fn check_registry_validity() -> Result<(), RegistryError> { - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - let origin = hklm.open_subkey(format!("{}\\Origin", REG_ARCH_PATH))?; - - let path: String = origin.get_value("ClientPath")?; - let valid = path == bootstrap_path()?.safe_str()?; - if !valid { - return Err(RegistryError::InvalidStoredClientPath); - } - - let eax32 = hklm.open_subkey(REG_EAX32_PATH)?; - let install_succesful: String = eax32.get_value("InstallSuccessful")?; - if install_succesful != "true" { - return Err(RegistryError::InvalidInstallKey); - } - - let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); - let qrc = hkcr.open_subkey("qrc"); - if qrc.is_err() { - return Err(RegistryError::InvalidQrcProtocol); - } - - Ok(()) -} - #[cfg(windows)] unsafe fn open_and_query_reg( sub_key: &str, @@ -179,7 +156,33 @@ fn inject_wow6432node(sub_key: &str) -> Option { } #[cfg(windows)] -async fn read_reg_key(path: &str) -> Result, RegistryError> { +pub fn check_registry_validity() -> Result<(), RegistryError> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let origin = hklm.open_subkey(format!("{}\\Origin", REG_ARCH_PATH))?; + + let path: String = origin.get_value("ClientPath")?; + let valid = path == bootstrap_path()?.safe_str()?; + if !valid { + return Err(RegistryError::InvalidStoredClientPath); + } + + let eax32 = hklm.open_subkey(REG_EAX32_PATH)?; + let install_succesful: String = eax32.get_value("InstallSuccessful")?; + if install_succesful != "true" { + return Err(RegistryError::InvalidInstallKey); + } + + let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); + let qrc = hkcr.open_subkey("qrc"); + if qrc.is_err() { + return Err(RegistryError::InvalidQrcProtocol); + } + + Ok(()) +} + +#[cfg(windows)] +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)..]; @@ -202,19 +205,14 @@ async fn read_reg_key(path: &str) -> Result, RegistryError> { Ok(None) } -#[cfg(unix)] -async fn read_reg_key(path: &str) -> Result, RegistryError> { - use crate::unix::wine::get_mx_wine_registry_value; - Ok(get_mx_wine_registry_value(path).await?) -} - -pub async fn parse_registry_path(key: &str) -> Result { +#[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 == ']') .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, None).await? { Some(path) => path.replace("\\", "/").replace("//", "/"), None => return Ok(PathBuf::from(key.to_owned())), }; @@ -232,13 +230,33 @@ 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_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 (note this could be something other than the exe like the manifest) + // Extract everything after the last ] and append it to the install path + let after_bracket = &key[(idx.unwrap() + 1)..]; + let path = game_install_info.path().join(after_bracket); + #[cfg(unix)] + let path = case_insensitive_path(path); + Ok(path) +} + +#[cfg(false)] // Block out unused method +pub async fn parse_partial_registry_path( + key: &str, + slug: Option<&str>, +) -> Result { let mut parts = key .split(|c: char| 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-resources/Cargo.toml b/maxima-resources/Cargo.toml index 3033b1d0..749bbe9b 100644 --- a/maxima-resources/Cargo.toml +++ b/maxima-resources/Cargo.toml @@ -4,7 +4,7 @@ version = { workspace = true } authors = { workspace = true } edition = { workspace = true } -[target.'cfg(windows)'.dependencies] +[dependencies] winres = "0.1.12" [lib] diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index cdf0a0db..7fec298f 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -348,4 +348,4 @@ fn run_(_tx: std_mpsc::Sender, stop_rx: oneshot::Receiver<()>) { pub fn start_service() -> Result<(), ServerError> { service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; Ok(()) -} \ No newline at end of file +} diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index a65d6bcd..af587551 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -9,7 +9,7 @@ maxima-lib = { path = "../maxima-lib" } windows-sys = { workspace = true, features = [ "Win32_System_ProcessStatus", "Win32_Networking_WinInet" ] } anyhow = { workspace = true } clap = { workspace = true } -eframe = { version = "0.35.0", default-features = false, features = [ "persistence", "glow" ] } +eframe = { version = "0.35.0", default-features = false, features = [ "persistence", "glow", "wayland", "x11" ] } egui = "0.35.0" egui_extras = {version = "0.35.0", features = [ "svg", "all_loaders" ] } image = { version = "0.25.10", features = [ "png", "jpeg" ] } @@ -20,13 +20,14 @@ tokio = { workspace = true } log = { workspace = true } puffin_http = "0.17.0" puffin = "0.20.0" -rustls = { version = "0.23.41", features = ["aws-lc-rs"] } +rustls = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } sys-locale = "0.3.2" humansize = { version = "2.1.3", features = ["no_alloc"] } fuzzy-matcher = "0.3.7" thiserror = { workspace = true } +winit = { version = "0.30", features = ["default"] } [target.'cfg(windows)'.dependencies] is_elevated = { workspace = true } diff --git a/maxima-ui/res/locale/en_us.json b/maxima-ui/res/locale/en_us.json index 5992187f..d4f85b12 100644 --- a/maxima-ui/res/locale/en_us.json +++ b/maxima-ui/res/locale/en_us.json @@ -94,7 +94,8 @@ }, "game_installation" : { "header": "Game Installation", - "default_folder": "Default installation folder", + "default_folder": "Default installation folder", + "default_wine_prefix": "Default Wine prefix folder", "ignore_ood_warning": "Ignore out-of-date game launch warning" }, "performance" : { diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index a77fb638..f6ec9cf5 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -1,5 +1,5 @@ use crate::{ - GameDetailsWrapper, GameInfo, GameVersionInfo, + GameDetailsWrapper, GameInfo, GameSettings, GameVersionInfo, bridge_thread::{BackendError, InteractThreadGameListResponse, MaximaLibResponse}, ui_image::UIImageCacheLoaderCommand, }; @@ -215,7 +215,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] @@ -254,10 +253,10 @@ pub async fn get_games_request( }; let slug = game_info.slug.clone(); - let settings = crate::GameSettings { - cloud_saves: true, - launch_args: String::new(), + let settings = GameSettings { exe_override: String::new(), + launch_args: String::new(), + cloud_saves: true, }; channel diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index a5d62c62..8a7139b9 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,4 +1,4 @@ -use egui::Context; +use egui::{Context, Key::W}; use log::{error, info, warn}; use thiserror::Error; @@ -28,15 +28,15 @@ use maxima::{ ServiceLayerError, ServicePlayer, }, }, + gameinfo::GameInstallInfo, lsx::service::LSXServerError, rtm::RtmError, util::{ - native::NativeError, + native::{NativeError, maxima_dir}, registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; use std::{path::PathBuf, time::Duration}; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; // TODO(headassbtw): integrate these all into the enums pub struct InteractThreadLoginResponse { @@ -79,11 +79,11 @@ pub enum MaximaLibRequest { GetFriendsRequest, GetGameDetailsRequest(String), StartGameRequest(GameInfo, Option), - InstallGameRequest(String, PathBuf), - CancelInstallRequest(String), + InstallGameRequest(String, String, PathBuf, Option), // offer, slug, path, wine prefix (unix only) PauseInstallRequest(String), MoveInstallToTopRequest(String), - LocateGameRequest(String), + LocateGameRequest(String, String, Option), // slug, path, wine prefix (unix only) + CancelInstallRequest(String), ShutdownRequest, } @@ -507,10 +507,7 @@ impl BridgeThread { }; f().await } - MaximaLibRequest::LocateGameRequest(mut path) => { - #[cfg(unix)] - maxima::core::launch::mx_linux_setup().await?; - + MaximaLibRequest::LocateGameRequest(slug, mut path, wine_prefix) => { if path.ends_with('/') || path.ends_with('\\') { path.pop(); } @@ -535,7 +532,7 @@ impl BridgeThread { }; if should_force_touchup { - match manifest.run_touchup(&path).await { + match manifest.run_touchup(&path, &slug).await { Ok(()) => InteractThreadLocateGameResponse::Success, Err(err) => { warn!("Touchup failed during locate, treating as success: {}", err); @@ -558,11 +555,14 @@ impl BridgeThread { .send(MaximaLibResponse::LocateGameResponse(response)) .ok(); info!("finished locating"); + let game_install_info = + GameInstallInfo::new(PathBuf::from(path.clone()), wine_prefix); + game_install_info.save_to_json(&slug); ctx.request_repaint(); Ok(()) } - MaximaLibRequest::InstallGameRequest(offer, 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?; let build = builds.live_build().ok_or(BackendError::NoLiveBuildAvailable)?; @@ -570,6 +570,8 @@ impl BridgeThread { .offer_id(offer) .build_id(build.build_id().to_owned()) .path(path) + .slug(slug) + .wine_prefix(wine_prefix) .build()?; maxima.content_manager().add_install(game).await?; Self::update_queue(maxima.content_manager(), backend_responder.clone()); diff --git a/maxima-ui/src/main.rs b/maxima-ui/src/main.rs index c8c977de..e6b39b89 100644 --- a/maxima-ui/src/main.rs +++ b/maxima-ui/src/main.rs @@ -31,7 +31,7 @@ use crate::bridge_thread::{BackendError, BridgeThread, InteractThreadLocateGameR use crate::renderers::app_bg_renderer::AppBgRenderer; use crate::renderers::game_view_bg_renderer::GameViewBgRenderer; use crate::translation_manager::{TranslationManager, positional_replace}; - +use maxima::util::native::maxima_dir; pub mod bridge; pub mod util; mod views; @@ -245,6 +245,7 @@ pub enum BackendStallState { pub struct InstallModalState { locate_path: String, install_folder: String, + wine_prefix: String, locating: bool, locate_response: Option, should_close: bool, @@ -255,6 +256,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, @@ -341,6 +343,7 @@ impl FrontendPerformanceSettings { #[derive(Default, 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, @@ -916,9 +919,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() { - let _ = self.backend.backend_commander.send( - bridge_thread::MaximaLibRequest::LocateGameRequest(self.installer_state.locate_path.clone()), - ); + #[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; + + let _ = self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::LocateGameRequest(slug.clone(), self.installer_state.locate_path.clone(), wine_prefix)); self.installer_state.locating = true; } }); @@ -936,16 +946,34 @@ 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() { + + 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; + 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 }); } - let _ = self.backend.backend_commander.send( - bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), path.join(slug)), - ); + let _ = self.backend.backend_commander.send(bridge_thread::MaximaLibRequest::InstallGameRequest(game.offer.clone(), slug.clone(), path.join(slug), wine_prefix)); clear = true; } @@ -960,6 +988,22 @@ impl MaximaEguiApp { egui::Label::new(egui::RichText::new(&self.locale.localization.modals.game_install.fresh_path_invalid).color(Color32::RED)).ui(ui); } } + #[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 self.installer_state.should_close { clear = true; } diff --git a/maxima-ui/src/translation_manager.rs b/maxima-ui/src/translation_manager.rs index 4111c2f6..9612daa1 100644 --- a/maxima-ui/src/translation_manager.rs +++ b/maxima-ui/src/translation_manager.rs @@ -174,6 +174,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 8d0c9b94..5aad12b8 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 7753037764473d4a01ab3430dc22823b53fd4271 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:21:15 -0500 Subject: [PATCH 38/57] Refresh UI after game is done downloading or located --- maxima-ui/src/bridge_processor.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 4264e362..91194c89 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -75,6 +75,10 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } app.installer_state.locate_response = Some(res); app.installer_state.locating = false; + app.backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::GetGamesRequest) + .unwrap(); } DownloadProgressChanged(offer_id, progress) => { if let Some(dl_ing) = app.installing_now.as_mut() { @@ -84,7 +88,12 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { } } } - DownloadFinished(_) => {} + DownloadFinished(_) => { + app.backend + .backend_commander + .send(bridge_thread::MaximaLibRequest::GetGamesRequest) + .unwrap(); + } DownloadQueueUpdate(current, queue) => { if let Some(current) = current { if !app.installing_now.as_ref().is_some_and(|n| n.offer == current) { From a73c725912f386d20a4b66e6cabc79f055f3a45c Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 21 May 2026 17:40:34 -0500 Subject: [PATCH 39/57] Wine improvements --- maxima-lib/src/core/auth/hardware.rs | 2 +- maxima-lib/src/core/launch.rs | 13 +-- maxima-lib/src/unix/wine.rs | 118 ++++++--------------------- 3 files changed, 34 insertions(+), 99 deletions(-) diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index c5e8129b..31ecd7f2 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -486,7 +486,7 @@ fn get_root_creation_str(slug: Option<&str>) -> String { Some(slug) => wine_prefix_dir(Some(slug)).unwrap(), None => return date_str, }; - let date_str = match fs::metadata(wine_prefix.join("drive_c")) { + let date_str = match fs::metadata(wine_prefix.join("pfx").join("drive_c")) { Ok(metadata) => { let nsec = (metadata.mtime_nsec() / 1_000_000) * 1_000_000; // Convert Unix timestamp to a DateTime diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 03304d9b..a2058f69 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -234,9 +234,15 @@ pub async fn start_game( }; let dir = path.safe_parent()?.safe_str()?; + #[cfg(unix)] let path = case_insensitive_path(path.clone()); + let path = path.safe_str()?; + + #[cfg(unix)] + let path = path.replace("\\", "/"); + info!("Game path: {}", path); let slug = if let LaunchMode::Online(ref _offer_id) = mode { @@ -428,8 +434,8 @@ 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, wine_prefix_dir, + check_runtime_validity, get_lutris_runtimes, install_runtime, setup_wine_registry, + wine_prefix_dir, }; std::fs::create_dir_all(wine_prefix_dir(slug).unwrap())?; @@ -437,9 +443,6 @@ pub async fn mx_linux_setup(slug: Option<&str>) -> Result<(), NativeError> { let skip = std::env::var("MAXIMA_DISABLE_WINE_VERIFICATION").is_ok(); if !skip { - if !check_wine_validity().await? { - install_wine().await?; - } let runtimes = get_lutris_runtimes().await?; if !check_runtime_validity("eac_runtime", &runtimes).await? { install_runtime("eac_runtime", &runtimes).await?; diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 1eb149df..0b298c0c 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -9,12 +9,10 @@ use std::{ ffi::OsStr, fs::{File, remove_dir_all, remove_file}, io::Read, - path::{Path, PathBuf}, process::{ExitStatus, Stdio}, }; use log::{info, warn}; -use regex::Regex; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use tar::Archive; @@ -73,7 +71,6 @@ pub(crate) struct LutrisRuntime { #[derive(Serialize, Deserialize, Default)] #[serde(default)] struct Versions { - proton: String, eac_runtime: String, umu: String, } @@ -94,8 +91,19 @@ pub fn wine_prefix_dir(slug: Option<&str>) -> Result { Ok(prefix_path) } -pub fn proton_dir() -> Result { - Ok(maxima_dir()?.join("wine/proton")) +pub fn proton_dir() -> Result { + if let Ok(path) = env::var("MAXIMA_PROTON_PATH") { + let path = PathBuf::from(path); + if path.exists() { + return Ok(path.into()); + } else { + warn!( + "MAXIMA_PROTON_PATH is set to {} but it doesn't exist", + path.display() + ); + } + } + Ok("GE-Proton".into()) } pub fn wine_dir() -> Result { @@ -106,8 +114,11 @@ pub fn eac_dir() -> Result { Ok(maxima_dir()?.join("wine/eac_runtime")) } -pub fn umu_bin() -> Result { - Ok(maxima_dir()?.join("wine/umu/umu-run")) +pub fn script_path() -> Result { + match env::var("MAXIMA_WINE_SCRIPT") { + Ok(path) => Ok(PathBuf::from(path)), + Err(_) => Ok(maxima_dir()?.join("wine/umu/umu-run")), + } } fn versions() -> Result { @@ -126,26 +137,7 @@ fn set_versions(versions: Versions) -> Result<(), NativeError> { Ok(()) } -pub(crate) async fn check_wine_validity() -> Result { - if !proton_dir()?.exists() { - return Ok(false); - } - - let version = versions()?.proton; - - let release = get_wine_release(); - if let Err(err) = release { - if !version.is_empty() { - warn!("Failed to check wine release, rate limited?"); - return Ok(true); - } - - return Err(NativeError::Wine(err)); - } - - Ok(version == release?.tag_name) -} - +#[cfg(target_os = "linux")] pub(crate) async fn get_lutris_runtimes() -> Result, WineError> { let client = reqwest::Client::builder() .user_agent("ArmchairDevelopers/Maxima") @@ -237,22 +229,7 @@ pub(crate) async fn install_runtime( set_versions(versions) } -fn get_wine_release() -> Result { - let releases = fetch_github_releases("GloriousEggroll", "proton-ge-custom")?; - - let mut release = None; - for r in releases { - if r.tag_name.ends_with("LoL") { - continue; - } - - release = Some(r); - break; - } - - release.ok_or(WineError::Fetch) -} - +#[cfg(target_os = "linux")] pub async fn run_wine_command, T: AsRef>( arg: OsString, args: Option, @@ -264,14 +241,12 @@ pub async fn run_wine_command, T: AsRef>( let proton_path = proton_dir()?; let proton_prefix_path = wine_prefix_dir(slug).unwrap(); let eac_path = eac_dir()?; - let umu_bin = umu_bin()?; + let script_path = script_path()?; info!("Wine Prefix: {:?}", proton_prefix_path); - let wine_path = - env::var("MAXIMA_WINE_COMMAND").unwrap_or_else(|_| umu_bin.to_string_lossy().to_string()); // Create command with all necessary wine env variables - let mut binding = Command::new(wine_path.clone()); + let mut binding = Command::new(script_path.clone()); let mut child = binding .env("WINEPREFIX", proton_prefix_path) .env("GAMEID", "umu-0") @@ -284,7 +259,7 @@ pub async fn run_wine_command, T: AsRef>( .env("LD_PRELOAD", "") // Fixes some log errors for some games .arg(&arg); - if !wine_path.ends_with("umu-run") { + if env::var("MAXIMA_NORTHSTAR").is_ok() { // wsock32 is used as a proxy for Northstar (Titanfall 2). TODO: provide user-facing option for this! child = child.env( "WINEDLLOVERRIDES", @@ -308,6 +283,7 @@ pub async fn run_wine_command, T: AsRef>( let status: ExitStatus; let mut output_str = String::new(); + info!("Running command: {:?}", child); if want_output { let output = child .stdout(Stdio::piped()) @@ -330,51 +306,7 @@ pub async fn run_wine_command, T: AsRef>( Ok(output_str.to_string()) } -pub(crate) async fn install_wine() -> Result<(), NativeError> { - let release = get_wine_release()?; - let asset = match release - .assets - .iter() - .find(|x| PROTON_PATTERN.is_match(&x.name)) - { - Some(asset) => asset, - None => return Err(NativeError::Wine(WineError::Fetch)), - }; - - let dir = maxima_dir()?.join("downloads"); - create_dir_all(&dir)?; - - let path = dir.join(&asset.name); - github_download_asset(asset, &path)?; - extract_wine(&path)?; - - let mut versions = versions()?; - versions.proton = release.tag_name; - set_versions(versions)?; - - if let Err(err) = remove_file(&path) { - warn!("Failed to delete {:?} - {:?}", path, err); - } - - Ok(()) -} - -fn extract_wine(archive_path: &PathBuf) -> Result<(), NativeError> { - info!("Extracting proton..."); - - let dir = proton_dir()?; - if dir.exists() { - remove_dir_all(&dir)?; - } - - create_dir_all(&dir)?; - - let archive_file = File::open(archive_path)?; - let archive_decoder = GzDecoder::new(archive_file); - let archive = Archive::new(archive_decoder); - extract_archive(dir, archive) -} - +#[cfg(target_os = "linux")] fn extract_archive( dir: PathBuf, mut archive: Archive, From 796ab184663e13871b639b0f025cc46b5db325d2 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:02:32 -0500 Subject: [PATCH 40/57] Trim null byte in incoming LSX messages, fixes game issues --- maxima-lib/src/lsx/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index d9904266..ad84081b 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -318,7 +318,7 @@ impl Connection { let state = self.state.write().await; - let trimmed_buffer = &buffer[..n]; + let trimmed_buffer = &buffer[..(n-1)]; let message = if let EncryptionState::Enabled(key) = state.encryption { simple_decrypt(trimmed_buffer, &key) } else { From 60a7b6b7b6f2ea7a57e07ffbf59de87eee563b19 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:36:15 -0500 Subject: [PATCH 41/57] Rename MAXIMA_WINE_SCRIPT to MAXIMA_UMU_LOCATION --- maxima-lib/src/unix/wine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 0b298c0c..e3d3f2fa 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -115,7 +115,7 @@ pub fn eac_dir() -> Result { } pub fn script_path() -> Result { - match env::var("MAXIMA_WINE_SCRIPT") { + match env::var("MAXIMA_UMU_LOCATION") { Ok(path) => Ok(PathBuf::from(path)), Err(_) => Ok(maxima_dir()?.join("wine/umu/umu-run")), } From 5bdcec552772da18c9f1f1e11236b9ac38f6d7bf Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:18:39 -0500 Subject: [PATCH 42/57] Update touchup handling stuff for service (Linux still runs through normal path) --- maxima-cli/src/main.rs | 24 ++++---------- maxima-lib/src/content/manager.rs | 23 +++----------- maxima-lib/src/core/background_service_win.rs | 1 + maxima-lib/src/core/library.rs | 2 +- maxima-lib/src/core/manifest/mod.rs | 31 +++++++++++++++++-- maxima-service/src/service/mod.rs | 4 +-- maxima-ui/src/bridge_thread.rs | 26 +++++----------- 7 files changed, 51 insertions(+), 60 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index d4447a8f..7169671b 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -1,3 +1,4 @@ +use maxima::core::{manifest::handle_touchup_request}; use clap::{Parser, Subcommand}; use anyhow::{bail, Result}; @@ -17,7 +18,7 @@ use is_elevated::is_elevated; #[cfg(windows)] use maxima::{ - core::background_service::{request_registry_setup, BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, + core::background_service::{request_registry_setup}, util::service::{is_service_running, is_service_valid, register_service_user, start_service}, }; @@ -30,10 +31,7 @@ use maxima::{ }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceLegacyOffer, ServicePlayer, }, - }, - ooa, - rtm::client::BasicPresence, - util::{ + }, ooa, rtm::client::BasicPresence, util::{ log::init_logger, native::{maxima_dir, take_foreground_focus}, registry::check_registry_validity, @@ -760,19 +758,9 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { Ok(()) } -async fn locate_game(_maxima_arc: LockedMaxima, path: &str) -> Result<()> { - let path = PathBuf::from(path); - let manifest = manifest::read(path.join(MANIFEST_RELATIVE_PATH)).await?; - let client = reqwest::Client::new(); - let resp = client - .post(format!("http://127.0.0.1:{}/touchup", BACKGROUND_SERVICE_PORT)) - .json(&ServiceTouchupRequest { - output_dir: path.to_string_lossy().into_owned(), - }) - .send() - .await?; - info!("Installation finished!"); - manifest.run_touchup(&path).await?; +async fn locate_game(_maxima_arc: LockedMaxima, install_path: &str, slug: &str) -> Result<()> { + let path: PathBuf = PathBuf::from(install_path); + handle_touchup_request(&path, slug).await?; info!("Installed!"); Ok(()) } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index fd2f52f2..58816e65 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -1,4 +1,4 @@ -use crate::content::exclusion::get_exclusion_list; +use crate::{content::exclusion::get_exclusion_list, core::manifest::handle_touchup_request}; use std::{ path::PathBuf, sync::{ @@ -22,7 +22,6 @@ use crate::{ content::{ ContentService, downloader::{DownloadError, ZipDownloader}, - exclusion::get_exclusion_list, zip::{CompressionType, ZipError, ZipFileEntry}, }, core::{ @@ -174,7 +173,7 @@ impl GameDownloader { debug!("URL: {}", url.url()); let downloader = ZipDownloader::new(url.url()).await?; - let exclusion_list = get_exclusion_list(game.offer_id.clone()); + let exclusion_list = get_exclusion_list(&game.slug()); let mut entries = Vec::new(); for ele in downloader.manifest().entries() { @@ -307,24 +306,10 @@ impl GameDownloader { return Ok(()); } - #[cfg(unix)] - mx_linux_setup(Some(&slug)).await?; // Wine Prefix needs setup before touchup - info!("Files downloaded, running touchup..."); - let client = reqwest::Client::new(); - let resp = client - .post(format!("http://127.0.0.1:{}/touchup", BACKGROUND_SERVICE_PORT)) - .json(&ServiceTouchupRequest { - output_dir: output_dir.to_string_lossy().into_owned(), - }) - .send() - .await?; + handle_touchup_request(&output_dir, &slug).await?; // output dir is install path - if !resp.status().is_success() { - let text = resp.text().await.unwrap_or_default(); - return Err(DownloaderError::Other(anyhow::anyhow!("Touchup request failed: {}", text))); - } info!("Installation finished!"); - + game_install_info.save_to_json(&slug); completed_bytes.fetch_add(1, Ordering::SeqCst); notify.notify_one(); Ok(()) diff --git a/maxima-lib/src/core/background_service_win.rs b/maxima-lib/src/core/background_service_win.rs index 891239a4..1756e7aa 100644 --- a/maxima-lib/src/core/background_service_win.rs +++ b/maxima-lib/src/core/background_service_win.rs @@ -19,6 +19,7 @@ pub struct ServiceLibraryInjectionRequest { #[derive(serde::Serialize, serde::Deserialize)] pub struct ServiceTouchupRequest { pub output_dir: String, + pub slug: String, } pub async fn request_library_injection( diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 08128060..bcbbea40 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -161,7 +161,7 @@ impl OwnedOffer { if !path.exists() { return Ok(None); } - Ok(Some(manifest::read(path).await?)) + Ok(Some(manifest::load_manifest_from_disk(path).await?)) } pub fn offer_id(&self) -> &String { diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index 9d295c0f..8c36bf41 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -12,6 +12,8 @@ pub enum ManifestError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] + ServiceRun(#[from] reqwest::Error), + #[error(transparent)] Xml(#[from] DeError), #[error(transparent)] Native(#[from] crate::util::native::NativeError), @@ -103,8 +105,8 @@ pub fn from_bytes(bytes: &[u8]) -> Result Result, ManifestError> { - let bytes = tokio::fs::read(&path).await?; +pub async fn load_manifest_from_disk(path_to_xml: PathBuf) -> Result, ManifestError> { + let bytes = tokio::fs::read(&path_to_xml).await?; if let Ok(m) = from_bytes::(&bytes) { return Ok(Box::new(m)); @@ -118,3 +120,28 @@ pub async fn read(path: PathBuf) -> Result, ManifestError> }), } } + +// 2 different thing for windows and unix as only windows needs elevation, + +#[cfg(windows)] +pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<(), ManifestError> { + use crate::core::background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}; + + let client = reqwest::Client::new(); + let _ = client + .post(format!("http://127.0.0.1:{}/touchup", BACKGROUND_SERVICE_PORT)) + .json(&ServiceTouchupRequest { + output_dir: install_path.to_string_lossy().into_owned(), + slug: slug.to_string(), + }) + .send() + .await?; + Ok(()) +} + + +#[cfg(unix)] +pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<(), ManifestError> { + let manifest = load_manifest_from_disk(Path::new(install_path).join(MANIFEST_RELATIVE_PATH)).await.unwrap(); + manifest.run_touchup(Path::new(install_path), slug).await +} diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 7fec298f..1413897e 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -291,11 +291,11 @@ async fn req_touchup(body: web::Bytes) -> Result { info!("Running touchup"); let request: ServiceTouchupRequest = serde_json::from_slice(&body)?; - let manifest = maxima::core::manifest::read( + let manifest = maxima::core::manifest::load_manifest_from_disk( Path::new(&request.output_dir).join(maxima::core::manifest::MANIFEST_RELATIVE_PATH), ) .await?; - manifest.run_touchup(Path::new(&request.output_dir)).await?; + manifest.run_touchup(Path::new(&request.output_dir), &request.slug).await?; Ok(HttpResponse::Ok().body("Done")) } diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 8a7139b9..e60c8bfd 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,3 +1,7 @@ +use maxima::core::manifest::handle_touchup_request; +use tokio::sync::mpsc::unbounded_channel; +use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::UnboundedReceiver; use egui::{Context, Key::W}; use log::{error, info, warn}; use thiserror::Error; @@ -512,27 +516,13 @@ impl BridgeThread { path.pop(); } - let path = PathBuf::from(path); + let path = PathBuf::from(path); let manifest_path = path.join(MANIFEST_RELATIVE_PATH); - - let response = match manifest::read(manifest_path.clone()).await { + let response = match manifest::load_manifest_from_disk(manifest_path.clone()).await { Ok(manifest) => { - let should_force_touchup = match tokio::fs::read_to_string(&manifest_path).await { - Ok(xml) => { - xml.contains("forceTouchupInstallerAfterUpdate=\"true\"") - || xml.contains("true") - } - Err(err) => { - warn!( - "Failed to inspect forceTouchupInstallerAfterUpdate during locate ({}), skipping touchup", - err - ); - false - } - }; - + let should_force_touchup = manifest.needs_touchup_on_locate(); if should_force_touchup { - match manifest.run_touchup(&path, &slug).await { + match handle_touchup_request(&path, &slug).await { Ok(()) => InteractThreadLocateGameResponse::Success, Err(err) => { warn!("Touchup failed during locate, treating as success: {}", err); From 53c00f47013091579a2b485b49dcb7425c5ede67 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:17:09 -0500 Subject: [PATCH 43/57] Load zlib state only if file is not 0 bytes, set write or append depending on file existence --- maxima-lib/src/content/downloader.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 07d6bc38..f99b2f49 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -13,7 +13,7 @@ use log::{debug, error, info}; use reqwest::{Client, StatusCode}; use strum_macros::Display; use tokio::{ - fs::{OpenOptions, create_dir, create_dir_all}, io::{AsyncWrite, AsyncWriteExt, BufReader}, + fs::{self, OpenOptions, create_dir, create_dir_all}, io::{AsyncWrite, AsyncWriteExt, BufReader}, }; use tokio_util::compat::FuturesAsyncReadCompatExt; @@ -41,16 +41,18 @@ trait RestorableDecoder { struct RestorableDeflateDecoder { inner: DeflateDecoder, file_name: String, + file_path: PathBuf, bytes_written: usize, bytes_since_last: usize, should_save: bool, } impl RestorableDeflateDecoder { - fn new(decoder: DeflateDecoder, file_name: String) -> Self { + fn new(decoder: DeflateDecoder, file_name: String, file_path: PathBuf) -> Self { Self { inner: decoder, file_name, + file_path, bytes_written: 0, bytes_since_last: 0, should_save: false, @@ -106,6 +108,9 @@ impl RestorableDecoder for RestorableDeflateDecoder { } fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError> { + if !self.file_path.exists() || std::fs::metadata(self.file_path.clone()).unwrap().len() == 0 { + return Err(DecoderRestoreError::CacheEmpty); + } let path = self .state_path() .map_err(|_| DecoderRestoreError::CacheEmpty)?; @@ -216,11 +221,14 @@ impl ZipDownloader { return Ok(0); } - let file = OpenOptions::new() - .create(true) - .append(true) - .open(&file_path) - .await?; + let mut file_opts = OpenOptions::new(); + if file_path.exists() { + file_opts.append(true); // Existence check because the file is created with read only perms with append (bug in tokio maybe?) + } else { + file_opts.write(true); + } + + let file = file_opts.create(true).open(&file_path).await?; let mut compressed_offset = 0; let writer = tokio::io::BufWriter::new(file); @@ -234,7 +242,7 @@ impl ZipDownloader { } CompressionType::Deflate => { let decoder = DeflateDecoder::new(writer); - let mut decoder = RestorableDeflateDecoder::new(decoder, entry.name().into()); + let mut decoder = RestorableDeflateDecoder::new(decoder, entry.name().into(), file_path.clone()); match decoder.restore_state() { Ok((bytes_in, _bytes_out)) => { From 98027e75d26c47d2ed984a949d23873a2ac97ad2 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:35:27 -0500 Subject: [PATCH 44/57] Fix game path read on registry install check fallback --- maxima-lib/src/core/library.rs | 11 ++++---- maxima-lib/src/util/registry.rs | 47 +++++++++++++++------------------ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index bcbbea40..21413d8f 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -60,19 +60,18 @@ pub struct OwnedOffer { impl OwnedOffer { #[cfg(windows)] pub async fn check_install_win_registry(&self) -> bool { - use crate::util::registry::parse_registry_path_regkey; + use crate::util::registry::{parse_partial_registry_path}; let Some(path) = &self.offer.install_check_override().as_ref() else { log::warn!("[{}] No install_check_override", self.slug); 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); + if let Ok(gamedir) = parse_partial_registry_path(path).await { + use crate::gameinfo::GameInstallInfo; + let game_install_info: GameInstallInfo = GameInstallInfo::new(gamedir.clone(), None); game_install_info.save_to_json(&self.slug); - manifest_path.exists() + gamedir.exists() } else { false } diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index f63f5fe5..7cd1455c 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -205,7 +205,23 @@ async fn read_reg_key(path: &str, _slug: Option<&str>) -> Result, Ok(None) } -#[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_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 (note this could be something other than the exe like the manifest) + // Extract everything after the last ] and append it to the install path + let after_bracket = &key[(idx.unwrap() + 1)..]; + let path = game_install_info.path().join(after_bracket); + #[cfg(unix)] + let path = case_insensitive_path(path); + Ok(path) +} + +#[cfg(windows)] pub async fn parse_registry_path_regkey(key: &str) -> Result { let mut parts = key .split(|c| c == '[' || c == ']') @@ -230,33 +246,16 @@ pub async fn parse_registry_path_regkey(key: &str) -> Result, -) -> 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 (note this could be something other than the exe like the manifest) - // Extract everything after the last ] and append it to the install path - let after_bracket = &key[(idx.unwrap() + 1)..]; - let path = game_install_info.path().join(after_bracket); - #[cfg(unix)] - let path = case_insensitive_path(path); - Ok(path) -} - -#[cfg(false)] // Block out unused method -pub async fn parse_partial_registry_path( - key: &str, - slug: Option<&str>, -) -> Result { +// [HKEY_LOCAL_MACHINE\SOFTWARE\BioWare\Mass Effect Legendary Edition\Install Dir]Game\Launcher\MassEffectLauncher.exe +// Will replace the registry key with the install dir and will only return that (IE will drop everything after the last ]) +#[cfg(windows)] +pub async fn parse_partial_registry_path(key: &str) -> Result { let mut parts = key .split(|c: char| 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? { + let path = match read_reg_key(first, None).await? { Some(path) => path.replace("\\", "/"), None => return Ok(PathBuf::from(key.to_owned())), }; @@ -266,8 +265,6 @@ pub async fn parse_partial_registry_path( PathBuf::from(key.to_owned()) }; - #[cfg(unix)] - let path = case_insensitive_path(path); Ok(path) } From 7de606ad0c322ae21db1daeee2c6b6db59533868 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:53:14 -0500 Subject: [PATCH 45/57] Proper downloading content (only increments when file in finished writing for now) --- maxima-cli/src/main.rs | 14 +++++++++----- maxima-lib/src/content/downloader.rs | 10 ++++++---- maxima-lib/src/content/manager.rs | 8 ++++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 7169671b..01839050 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -1,3 +1,5 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use maxima::content::manager::ProgressCallback; use maxima::core::{manifest::handle_touchup_request}; use clap::{Parser, Subcommand}; @@ -8,9 +10,7 @@ use regex::Regex; use reqwest; use std::{ - path::PathBuf, - sync::LazyLock, - time::Instant, + path::PathBuf, sync::{Arc, LazyLock}, time::Instant, }; #[cfg(windows)] @@ -454,7 +454,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { maxima.update().await; if let Some(downloader) = maxima.content_manager().current() { - info!("Downloading: {:.1}%/100%", downloader.percentage_done()); + info!("Downloading: {:.1}%/100%, {} bytes out of {}", downloader.percentage_done(), downloader.completed_bytes(), downloader.bytes_total()); } else { break; } @@ -510,9 +510,13 @@ async fn download_specific_file( bail!("Couldn't find the file {}", file); } + let completed_bytes = Arc::new(AtomicUsize::new(0)); + let on_progress: ProgressCallback = Box::new(move |bytes| { + completed_bytes.fetch_add(bytes, Ordering::SeqCst); + }); let ele = entry.unwrap(); let output_dir = std::env::current_dir()?.join("downloads"); - downloader.download_single_file(ele, &output_dir).await?; + downloader.download_single_file(ele, &output_dir, on_progress).await?; info!( "Downloaded file {} from game build {}", diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index f99b2f49..311dd9b1 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -18,8 +18,7 @@ use tokio::{ use tokio_util::compat::FuturesAsyncReadCompatExt; use crate::content::{ - zip::CompressionType, - zlib::{restore_zlib_state, write_zlib_state}, + manager::ProgressCallback, zip::CompressionType, zlib::{restore_zlib_state, write_zlib_state}, }; use super::zip::{ZipFile, ZipFileEntry}; @@ -198,6 +197,7 @@ impl ZipDownloader { &self, entry: &ZipFileEntry, output_dir: &Path, + progress_callback: ProgressCallback ) -> Result { let file_path = output_dir.join(entry.name()); @@ -259,7 +259,7 @@ impl ZipDownloader { Box::new(decoder) } }; - + progress_callback(compressed_offset as usize); let offset = entry.data_offset(); let start_offset = offset + compressed_offset as i64; // Add compressed offset, not decompressed let end_offset = offset + entry.compressed_size() - 1; @@ -309,12 +309,14 @@ impl ZipDownloader { } }; + let content_length = data.content_length().unwrap_or(0); // Because data gets moved with bytes_stream() let stream = ByteCountingStream::new(data.bytes_stream()); let mut stream_reader = BufReader::new(stream.into_async_read().compat()); + tokio::io::copy(&mut stream_reader, &mut writer).await?; writer.shutdown().await?; // flush decoder + BufWriter to disk - + progress_callback(content_length as usize); Ok(compressed_offset) } } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 58816e65..3fc3c0ce 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -144,7 +144,7 @@ impl DownloadQueue { } } -type ProgressCallback = Box; +pub type ProgressCallback = Box; pub struct GameDownloader { offer_id: String, @@ -282,7 +282,7 @@ impl GameDownloader { }); tokio::select! { - result = downloader.download_single_file(&ele, &output_dir) => { + result = downloader.download_single_file(&ele, &output_dir, on_progress) => { if let Err(err) = result { error!("File download failed: {}", err); } @@ -344,6 +344,10 @@ impl GameDownloader { pub fn offer_id(&self) -> &str { &self.offer_id } + + pub fn completed_bytes(&self) -> usize { + self.completed_bytes.load(Ordering::SeqCst) + } } #[derive(Getters)] From 8521e648d88ffe0fe8adeab2c7ffc001f13e24af Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:03:33 -0500 Subject: [PATCH 46/57] Fix current download not showing (displays progress properly as well) --- maxima-lib/src/content/manager.rs | 8 -------- maxima-lib/src/util/registry.rs | 8 ++++---- maxima-ui/src/bridge_processor.rs | 4 ++-- maxima-ui/src/bridge_thread.rs | 10 ++++++++++ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 3fc3c0ce..4c08997d 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -382,14 +382,6 @@ impl ContentManager { self.current = None; } - if let Some(current) = &self.queue.current { - if current == &game { - self.install_direct(game).await?; - return Ok(()); - } - self.queue.queued.push(current.clone()); - } - self.install_direct(game).await?; Ok(()) } diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 7cd1455c..b9c9ce5a 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -230,7 +230,7 @@ pub async fn parse_registry_path_regkey(key: &str) -> Result path.replace("\\", "/").replace("//", "/"), - None => return Ok(PathBuf::from(key.to_owned())), + None => return Err(RegistryError::InvalidInstallKey), }; let second = second.replace("\\", "/"); @@ -238,7 +238,7 @@ pub async fn parse_registry_path_regkey(key: &str) -> Result Result path.replace("\\", "/"), - None => return Ok(PathBuf::from(key.to_owned())), + None => return Err(RegistryError::InvalidInstallKey), }; return Ok(PathBuf::from(path.to_owned())); } else { - PathBuf::from(key.to_owned()) + return Err(RegistryError::InvalidInstallKey); }; Ok(path) diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 91194c89..2d226baa 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -106,8 +106,6 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { total_bytes: 0, }); } - } else { - app.installing_now = None; } app.install_queue.clear(); @@ -126,6 +124,8 @@ pub fn frontend_processor(app: &mut MaximaEguiApp, ctx: &egui::Context) { }, ); } + } else { + app.installing_now = None; } } DownloadFailed(offer, reason) => { diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index e60c8bfd..17ce7b6e 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -447,6 +447,16 @@ impl BridgeThread { _ => {} } + if let Some(dl) = maxima.content_manager().current() { + backend_responder.send(MaximaLibResponse::DownloadProgressChanged( + dl.offer_id().to_string(), + InteractThreadDownloadProgressResponse { + bytes: dl.bytes_downloaded(), + bytes_total: dl.bytes_total(), + }, + )).ok(); + } + for ev in maxima.consume_pending_events() { match ev { MaximaEvent::InstallFinished(offer_id) => { From d0114bc5a6d0717682ad60e0ea24fb3c7763e04f Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:41:28 +0200 Subject: [PATCH 47/57] fix french translation, usual dep update, fix cli potential hiccup & cleaner execute flow --- Cargo.lock | 754 ++++++++++++++++---------------- Cargo.toml | 12 +- maxima-cli/src/main.rs | 70 ++- maxima-lib/Cargo.toml | 22 +- maxima-lib/src/core/library.rs | 19 +- maxima-service/Cargo.toml | 2 +- maxima-ui/Cargo.toml | 8 +- maxima-ui/res/locale/fr_fr.json | 1 + 8 files changed, 452 insertions(+), 436 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac6a5e9f..12ba70e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "actix-codec" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +checksum = "31404e1443b7b7bcaa311c1af456775cc3f668e34573f1503d7ce4844327629e" dependencies = [ "bitflags 2.13.1", "bytes", @@ -48,9 +48,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.13.1" +version = "3.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" +checksum = "11004b0e9b44b4eb3d15e0c3132b96fb178c7e50a74758b2f17bb9cc9a7fb4f6" dependencies = [ "actix-codec", "actix-rt", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +checksum = "6a16bf2f19c2ad84842bdfe6f3665620e93197d5c607889bfa3aaac45e762fd1" dependencies = [ "futures-core", "tokio", @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.6.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +checksum = "5d44ae8a6516f4ac7bfc7b61aabcd286104e96b4b24c747ce220832a016056d9" dependencies = [ "actix-rt", "actix-service", @@ -132,7 +132,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.10", + "socket2", "tokio", "tracing", ] @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.14.0" +version = "4.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" +checksum = "bbacab3593b6b4f7be815076fc52d60a83c873426824675417e2abdd229e2e36" dependencies = [ "actix-codec", "actix-http", @@ -194,7 +194,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.5", + "socket2", "time", "tracing", "url", @@ -229,13 +229,13 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ "cipher", "cpubits", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -316,7 +316,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -327,9 +327,9 @@ checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -476,13 +476,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -530,7 +530,7 @@ dependencies = [ "num-traits", "pastey 0.1.1", "rayon", - "thiserror 2.0.19", + "thiserror 2.0.20", "v_frame", "y4m", ] @@ -560,9 +560,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "zeroize", @@ -570,9 +570,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -610,9 +610,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bincode" @@ -634,9 +634,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" dependencies = [ "bit-vec 0.9.1", ] @@ -758,9 +758,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "serde_core", @@ -804,13 +804,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -929,9 +929,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -962,12 +962,12 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -999,9 +999,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -1009,9 +1009,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -1028,7 +1028,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1061,7 +1061,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -1123,9 +1123,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -1275,18 +1275,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -1411,12 +1411,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.24.0", - "darling_macro 0.24.0", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -1435,15 +1435,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1459,13 +1459,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.24.0", + "darling_core 0.24.1", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1641,7 +1641,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1688,18 +1688,18 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecb" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfbf3db731928d6912bc3beda911d55b834cee3df6131ba79b337a1298a3fa9" +checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac" dependencies = [ "cipher", ] [[package]] name = "ecolor" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6758be723a3f298bbfda4db75748bc2ba0abafe096b6383c7c32da264764fbc3" +checksum = "fc883f505d446814c0226bef4b64564b9adb9966fc00c88c57ce6d827dd152d6" dependencies = [ "bytemuck", "emath", @@ -1708,9 +1708,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc8234e2f681b2afd2b2e8c332fa614de2fddbdcb28d0acf5c420449682ea90" +checksum = "2afc0cbcdb6896b7bfb1dbbebaf7b9af9635ff38fd01e89bdd0174c1717b1857" dependencies = [ "ahash", "bytemuck", @@ -1722,7 +1722,6 @@ dependencies = [ "glow", "glutin", "glutin-winit", - "home", "image", "js-sys", "log", @@ -1737,7 +1736,6 @@ dependencies = [ "serde", "static_assertions", "wasm-bindgen", - "wasm-bindgen-futures", "web-sys", "web-time", "windows-sys 0.61.2", @@ -1746,16 +1744,16 @@ dependencies = [ [[package]] name = "egui" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796c98d50b79631281d516343a6f6e93c0666462ca36e2c93b39f25d7793325" +checksum = "c977ac91dfaa651633fd9722e4ce9ccb32cda4c748b89a5cb57e504036e37c13" dependencies = [ "accesskit", "ahash", "bitflags 2.13.1", "emath", "epaint", - "itertools", + "itertools 0.15.0", "log", "nohash-hasher", "profiling", @@ -1763,13 +1761,14 @@ dependencies = [ "serde", "smallvec", "unicode-segmentation", + "web-sys", ] [[package]] name = "egui-wgpu" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e6cfac0725563555fa4f91e9f799b9d7c6c5dd831fca6abc8234afc64b7a34" +checksum = "2fc362cc6bc8c1d7169c9cb0e7741ff1e03063681373d6e55cc3b25fd21213f3" dependencies = [ "ahash", "bytemuck", @@ -1778,7 +1777,7 @@ dependencies = [ "epaint", "log", "profiling", - "thiserror 2.0.19", + "thiserror 2.0.20", "type-map", "web-time", "wgpu", @@ -1787,9 +1786,9 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea6bf3608db949588b95b8b341ee358d0c3f95cf4dc3f53d8d76717edee87db" +checksum = "9327fc8edef2c57db9bcbcacc82c8a4b1e8cc64cd41a67db4419dcf643d88c83" dependencies = [ "arboard", "bytemuck", @@ -1803,22 +1802,21 @@ dependencies = [ "serde", "smithay-clipboard", "web-time", - "webbrowser", "winit", ] [[package]] name = "egui_extras" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2bd33be7338367bf21f54d62e069d58a5fb3ae033fa8bc6df7a77b9ef4cf957" +checksum = "1e651e08ddf6bb8bfd964ad70e33bc721c093f2b77ab59580e7140c008dab68c" dependencies = [ "ahash", "egui", "ehttp", "enum-map", "image", - "itertools", + "itertools 0.15.0", "log", "mime_guess2", "profiling", @@ -1827,15 +1825,14 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52274b9bfb8d8e252cd0f00c9f6214f360d75a0962baa77a2d62ddb002fe99d4" +checksum = "69b65e45f8d8d7c6d848b3ec41f642b54ad828033f1511d4b4c9adc09040166a" dependencies = [ "bytemuck", "egui", "glow", "log", - "memoffset", "profiling", "winit", ] @@ -1856,15 +1853,15 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "emath" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4ec073c9898516584d8c6cfdcee95b530b3d941cd5031ef4050aa36812308b" +checksum = "dce03cf1604f74515830012c29991beb0b58f69e4e43f5afe22fa7afed65ccd5" dependencies = [ "bytemuck", "serde", @@ -1933,9 +1930,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e60a8888b51da911df23918fd7301359b1d43a406a0ff3b8863af093dd7fc6c" +checksum = "11863a6b2b7823010c1090ddf4706947bdda46766742def0673195cbf7ff4a73" dependencies = [ "ahash", "bytemuck", @@ -1959,9 +1956,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ee4e1f553a3584c301f3a56ff1a775f1384781396cea301c8d952e9b93f560" +checksum = "18dee69613aac468922cf28a32025eb7d7ed6985b61f73245848e58f37876c98" [[package]] name = "equator" @@ -2012,9 +2009,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.3.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" [[package]] name = "euclid" @@ -2044,10 +2041,11 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.12" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3" dependencies = [ + "autocfg", "indenter", "once_cell", ] @@ -2112,9 +2110,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "finl_unicode" @@ -2170,9 +2168,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.11.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" dependencies = [ "bytemuck", "serde", @@ -2205,7 +2203,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2237,9 +2235,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -2252,9 +2250,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -2262,15 +2260,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -2279,38 +2277,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -2344,9 +2342,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" dependencies = [ "rustversion", "typenum", @@ -2439,9 +2437,9 @@ dependencies = [ [[package]] name = "glifo" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" dependencies = [ "bytemuck", "foldhash 0.2.0", @@ -2461,9 +2459,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -2592,9 +2590,9 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.7.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.13.1", "bytemuck", @@ -2664,9 +2662,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2674,21 +2672,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "0.2.12" @@ -2722,9 +2705,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2766,9 +2749,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -2816,7 +2799,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2", "tokio", "tower-service", "tracing", @@ -2848,9 +2831,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2862,9 +2845,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2875,9 +2858,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2889,16 +2872,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2909,15 +2893,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2997,9 +2981,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" [[package]] name = "impl-more" @@ -3015,9 +2999,9 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -3062,11 +3046,11 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ - "darling 0.24.0", + "darling 0.24.1", "indoc", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3139,6 +3123,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -3157,7 +3150,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -3215,9 +3208,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -3232,7 +3225,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3322,14 +3315,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" dependencies = [ "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.9.1", + "redox_syscall 0.9.3", ] [[package]] @@ -3373,9 +3366,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -3411,9 +3404,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "serde_core", "value-bag", @@ -3430,9 +3423,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.1" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" dependencies = [ "hashbrown 0.17.1", ] @@ -3498,14 +3491,14 @@ dependencies = [ name = "maxima-bootstrap" version = "0.1.0" dependencies = [ - "base64 0.23.0", + "base64 0.23.1", "cacao", "maxima-lib", "maxima-resources", "reqwest", "serde", "serde_json", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "url", "urlencoding", @@ -3519,7 +3512,7 @@ name = "maxima-cli" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.23.0", + "base64 0.23.1", "clap", "futures", "inquire", @@ -3546,7 +3539,7 @@ dependencies = [ "async-compression", "async-trait", "base62", - "base64 0.23.0", + "base64 0.23.1", "bytebuffer", "bytes", "cbc", @@ -3562,7 +3555,7 @@ dependencies = [ "filetime", "flate2", "futures", - "generic-array 1.4.4", + "generic-array 1.4.5", "gethostname", "glob", "globset", @@ -3581,7 +3574,7 @@ dependencies = [ "postcard", "prost", "prost-build", - "quick-xml", + "quick-xml 0.42.0", "rand 0.10.2", "regex", "reqwest", @@ -3597,7 +3590,7 @@ dependencies = [ "strum_macros", "sysinfo", "tar", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-stream", @@ -3628,7 +3621,7 @@ name = "maxima-service" version = "0.1.0" dependencies = [ "actix-web", - "base64 0.23.0", + "base64 0.23.1", "derive_more", "directories", "hex", @@ -3641,7 +3634,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "structured-logger", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "windows-service", "windows-sys 0.61.2", @@ -3653,7 +3646,7 @@ name = "maxima-tui" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.23.0", + "base64 0.23.1", "clap", "color-eyre", "inquire", @@ -3697,7 +3690,7 @@ dependencies = [ "strum", "strum_macros", "sys-locale", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "windows-sys 0.61.2", "winit", @@ -3808,9 +3801,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -3841,29 +3834,41 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "naga" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" dependencies = [ "arrayvec", - "bit-set 0.9.1", + "bit-set 0.10.0", "bitflags 2.13.1", "cfg-if", "cfg_aliases", "codespan-reporting", "half", - "hashbrown 0.16.1", - "hexf-parse", + "hashbrown 0.17.1", "indexmap", "libm", "log", + "naga-types", "num-traits", "once_cell", "rustc-hash 1.1.0", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-ident", ] +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.20", +] + [[package]] name = "ndk" version = "0.9.0" @@ -4019,9 +4024,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -4509,9 +4514,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.4.0" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +checksum = "ade3be4664bc1ef537ce133015f04c176b737815c2ba9fd60edf212d6e90dd55" dependencies = [ "is-wsl", "libc", @@ -4575,9 +4580,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "palette" @@ -4693,9 +4698,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -4703,9 +4708,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -4713,9 +4718,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", @@ -4726,9 +4731,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", ] @@ -4771,7 +4776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -4832,9 +4837,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plain" @@ -4893,9 +4898,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -4921,9 +4926,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -5007,7 +5012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "petgraph", @@ -5026,7 +5031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.119", @@ -5051,7 +5056,7 @@ dependencies = [ "bincode", "byteorder", "cfg-if", - "itertools", + "itertools 0.14.0", "lz4_flex", "parking_lot", "serde", @@ -5119,6 +5124,15 @@ name = "quick-xml" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", "serde", @@ -5137,8 +5151,8 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.3", "rustls", - "socket2 0.6.5", - "thiserror 2.0.19", + "socket2", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -5146,9 +5160,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", @@ -5161,7 +5175,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.19", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -5176,7 +5190,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2", "tracing", "windows-sys 0.61.2", ] @@ -5204,9 +5218,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "rand_core 0.6.4", ] @@ -5298,13 +5312,13 @@ dependencies = [ "compact_str", "critical-section", "hashbrown 0.17.1", - "itertools", + "itertools 0.14.0", "kasuari", "lru", "palette", "serde", "strum", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -5363,7 +5377,7 @@ dependencies = [ "hashbrown 0.17.1", "indoc", "instability", - "itertools", + "itertools 0.14.0", "line-clipping", "ratatui-core", "serde", @@ -5389,7 +5403,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -5403,7 +5417,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha", "simd_helpers", - "thiserror 2.0.19", + "thiserror 2.0.20", "v_frame", "wasm-bindgen", ] @@ -5460,12 +5474,13 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.39.2" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", "font-types", + "once_cell", ] [[package]] @@ -5494,9 +5509,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e" +checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" dependencies = [ "bitflags 2.13.1", ] @@ -5509,7 +5524,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -5526,9 +5541,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -5772,9 +5787,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -5899,7 +5914,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -5952,7 +5967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -5974,7 +5989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -6072,9 +6087,9 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skrifa" -version = "0.42.1" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", "read-fonts", @@ -6159,7 +6174,7 @@ dependencies = [ "log", "memmap2", "rustix 1.1.4", - "thiserror 2.0.19", + "thiserror 2.0.20", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -6192,16 +6207,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.5" @@ -6290,15 +6295,15 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sval" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a370f3cd0422964fd9a19b6516f048527738e6ec50b1b0ff79b460b468390" +checksum = "b81b254da21fe1fcc4e3a74fe39b46e25e3a863078f8b71c954d47f84889dbc6" [[package]] name = "sval_buffer" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111e6e2dab25782acb41b281263f5f60d6f56a2ba0b3b076187ad23a1552544d" +checksum = "50be352d2822ffafb59e3e2ddac9d5ee60f2eeadbb7b5a2a951b9f3651e87a6f" dependencies = [ "sval", "sval_ref", @@ -6307,18 +6312,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590ac4ebd299740eac453162302a494e6d5b3be243feab8bf07d0d4331b6b2b3" +checksum = "b048ca293b998d9a45659159f94a64063791e74cdc670164943dbb434405573d" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b964d6d917267355d7f343c515b908586e5b4aeeada328738f703452f9412d6" +checksum = "e6b5888e40f80568733217f27b7317b845f463400ced36c424b1a804730e53b2" dependencies = [ "itoa", "ryu", @@ -6327,9 +6332,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea35e4d6b997acc7dc4786ab782f6a6c5000efe4ae93a2db89cf350775fe5fe" +checksum = "e17664d6bb6b74947afaab9d7c991caa9bf5638d4dee16fcbef637f440796049" dependencies = [ "itoa", "ryu", @@ -6338,9 +6343,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a73195ebd4b3e5866db2e1b75f3f383657ad5abc600c646d69b4f064fee89154" +checksum = "07c059969ca5ca163ea7fef6c9661758973d17691aba92abdcf5c428f4ec122c" dependencies = [ "sval", "sval_buffer", @@ -6349,18 +6354,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ef28df9d586bfd15115286651337cb5645f8c203160d22d2d1a20cf13c428c" +checksum = "42d6b29ff568c85c87561807f51d2adfff4b6016c6363133f7cd1652a12548f3" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.21.0" +version = "2.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d72fbe6537e88878f10237bde71ae4a1b65b2bf54bdf274abc566fc696334c92" +checksum = "8f33ec9edc42b12764d5c90ca0a1d84189c6bde81ed27507f1e661c6e4e05853" dependencies = [ "serde_core", "sval", @@ -6401,9 +6406,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -6570,11 +6575,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -6590,13 +6595,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -6682,9 +6687,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -6717,7 +6722,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.5", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -6730,7 +6735,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -7002,7 +7007,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools", + "itertools 0.14.0", "unicode-segmentation", "unicode-width", ] @@ -7027,11 +7032,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "flate2", "log", "percent-encoding", @@ -7044,11 +7049,11 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "http 1.5.0", "httparse", "log", @@ -7114,9 +7119,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "atomic", "getrandom 0.4.3", @@ -7154,9 +7159,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417d6197dd0ee696783d6be4276ac6ea74b985e00024c85ccfb37aff4f2bed82" +checksum = "27780e3aec98b13ac796bc0d5770205ee6e976d1ccb33a52da70b73ff0ff880d" dependencies = [ "erased-serde", "serde_core", @@ -7165,9 +7170,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f7251ecde2c9ed431bbe0659853e7991753447447bbf1ae59d8b31c578d4e" +checksum = "1c093d229c7ead9974b640e61170976e0eff3dac447cf6792c645f0b7f28b3de" dependencies = [ "sval", "sval_buffer", @@ -7186,25 +7191,24 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vello_common" -version = "0.0.9" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" dependencies = [ "bytemuck", "fearless_simd", "guillotiere", - "hashbrown 0.17.1", "log", "peniko", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "vello_cpu" -version = "0.0.9" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588691169aed86b5c8fb487266afee01323234e6fd0a3f2aaec0eaa8e4007f23" +checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" dependencies = [ "bytemuck", "glifo", @@ -7263,9 +7267,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -7276,9 +7280,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -7286,9 +7290,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7296,9 +7300,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -7309,9 +7313,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -7331,9 +7335,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", "downcast-rs", @@ -7448,7 +7452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.41.0", "quote", ] @@ -7466,9 +7470,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -7484,22 +7488,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webbrowser" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef62a3d5f7b2411119a11b6f62570dbff91d7105e011a20fb83fbf8f5761c40f" -dependencies = [ - "jni", - "log", - "ndk-context", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-foundation 0.3.2", - "url", - "web-sys", -] - [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -7598,9 +7586,9 @@ dependencies = [ [[package]] name = "wgpu" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" dependencies = [ "arrayvec", "bitflags 2.13.1", @@ -7608,7 +7596,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "document-features", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "js-sys", "log", "portable-atomic", @@ -7626,21 +7614,22 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" dependencies = [ "arrayvec", - "bit-set 0.9.1", + "bit-set 0.10.0", "bit-vec 0.9.1", "bitflags 2.13.1", "bytemuck", "cfg_aliases", "document-features", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "log", "naga", + "naga-types", "once_cell", "parking_lot", "portable-atomic", @@ -7648,7 +7637,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", @@ -7657,39 +7646,42 @@ dependencies = [ [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" dependencies = [ "bitflags 2.13.1", "cfg-if", "cfg_aliases", + "hashbrown 0.17.1", "libloading", "log", "naga", + "naga-types", "portable-atomic", "portable-atomic-util", "raw-window-handle", "renderdoc-sys", - "thiserror 2.0.19", + "static_assertions", + "thiserror 2.0.20", "wgpu-naga-bridge", "wgpu-types", ] [[package]] name = "wgpu-naga-bridge" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" dependencies = [ "naga", "wgpu-types", @@ -7697,15 +7689,17 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" dependencies = [ "bitflags 2.13.1", "bytemuck", "js-sys", "log", + "naga-types", "raw-window-handle", + "static_assertions", "web-sys", ] @@ -8128,16 +8122,16 @@ dependencies = [ "futures", "log", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "windows", "windows-core", ] [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "x11-dl" @@ -8208,9 +8202,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xmlwriter" @@ -8258,18 +8252,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -8305,9 +8299,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -8316,9 +8310,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -8327,13 +8321,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -8372,9 +8366,9 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" [[package]] name = "zune-inflate" diff --git a/Cargo.toml b/Cargo.toml index 920675e7..67c4985c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,15 +15,15 @@ members = [ tokio = { version = "1.53.1", features = ["rt-multi-thread", "time", "net", "sync", "macros", "fs", "process"] } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" -rustls = { version = "0.23.42", features = ["aws-lc-rs"] } +rustls = { version = "0.23.43", features = ["aws-lc-rs"] } reqwest = { version = "0.13.4", features = ["stream", "rustls"], default-features = false } -base64 = "0.23.0" -thiserror = "2.0.19" +base64 = "0.23.1" +thiserror = "2.0.20" anyhow = "1.0.104" -log = "0.4.33" +log = "0.4.34" regex = "1.13.1" -futures = "0.3.33" -clap = { version = "4.6.4", features = ["derive"] } +futures = "0.3.34" +clap = { version = "4.6.6", features = ["derive"] } inquire = "0.9.4" strum = { version = "0.28.0", features = ["derive"] } strum_macros = "0.28.0" diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 01839050..623c204f 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -7,7 +7,6 @@ use anyhow::{bail, Result}; use inquire::{Select, Text}; use log::{debug, error, info, warn}; use regex::Regex; -use reqwest; use std::{ path::PathBuf, sync::{Arc, LazyLock}, time::Instant, @@ -367,9 +366,9 @@ 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 mut owned_games = Vec::new(); let game = { - let mut owned_games = Vec::new(); for game in maxima.mut_library().games().await? { if game.base_offer().is_installed().await { continue; @@ -382,58 +381,70 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { .map(|g| g.name()) .collect::>(); - let name = - Select::new("What game would you like to install?", owned_games_strs).prompt()?; + let name = tokio::task::spawn_blocking(move || { + Select::new("What game would you like to install?", owned_games_strs).prompt() + }) + .await??; + 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 mut maxima = maxima_arc.lock().await; + let builds = maxima .content_manager() .service() .available_builds(&offer_id) .await?; - let build = builds.live_build(); - if build.is_none() { + + let available_live_build = builds.live_build(); + if available_live_build.is_none() { bail!("Couldn't find a suitable game build"); } - let build = build.unwrap(); + let build = available_live_build.unwrap(); info!("Installing game build {}", build.to_string()); let path = PathBuf::from( - Text::new("Where would you like to install the game? (must be an absolute path)") - .prompt()?, + tokio::task::spawn_blocking(|| { + Text::new("Where would you like to install the game? (must be an absolute path)").prompt() + }) + .await??, ); + if !path.is_absolute() { + error!("Path {:?} is not absolute.", path); + return Ok(()); + } #[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) + let input = tokio::task::spawn_blocking(|| { + Text::new("Where do you want to store the Wine prefix? (must be an absolute path)").prompt() + }) + .await??; + let p = PathBuf::from(input); + if !p.is_absolute() { + error!("Wine prefix {:?} is not absolute.", p); + return Ok(()); + } + Some(p) }; #[cfg(not(unix))] - let wine_prefix = PathBuf::new(); - - if !path.is_absolute() { - error!("Path {:?} is not absolute.", path); - return Ok(()); - } + let wine_prefix = None; let game = QueuedGameBuilder::default() .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)) + .slug(slug) + .wine_prefix(wine_prefix) .build()?; let start_time = Instant::now(); @@ -441,6 +452,10 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { drop(maxima); + const IDLE_TICK_LIMIT: usize = 5; + let mut idle_ticks = 0usize; + let mut saw_downloader = false; + loop { let mut maxima = maxima_arc.lock().await; @@ -454,9 +469,16 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { maxima.update().await; if let Some(downloader) = maxima.content_manager().current() { + saw_downloader = true; + idle_ticks = 0; info!("Downloading: {:.1}%/100%, {} bytes out of {}", downloader.percentage_done(), downloader.completed_bytes(), downloader.bytes_total()); - } else { + } else if saw_downloader { break; + } else { + idle_ticks += 1; + if idle_ticks >= IDLE_TICK_LIMIT { + bail!("Installer produced no download activity"); + } } drop(maxima); @@ -465,7 +487,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let elapsed_time = Instant::now() - start_time; info!( - "Download took {}.{}s", + "Download took {}.{:03}s", elapsed_time.as_secs(), elapsed_time.subsec_millis() ); diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 5edd5981..a0bfc9a3 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -16,10 +16,10 @@ rust-version = { workspace = true } postcard = { version = "1.1.3", features = ["use-std"] } scopeguard = "1.2.0" tokio = { workspace = true } -moka = { version = "0.12.15", features = ["sync"] } +moka = { version = "0.12.16", features = ["sync"] } serde = { workspace = true } serde_json = { workspace = true } -quick-xml = { version = "0.41.0", features = ["serialize"] } +quick-xml = { version = "0.42.0", features = ["serialize"] } pastey = "0.2.3" hex = { workspace = true } reqwest = { workspace = true, features = ["blocking", "form", "query", "gzip", "json"] } @@ -30,8 +30,8 @@ enable-ansi-support = "0.3.1" chrono = "0.4.45" regex = { workspace = true } directories = { workspace = true } -open = "5.4.0" -ureq = { version = "3.3.0", features = ["rustls"] } +open = "5.4.2" +ureq = { version = "3.4.0", features = ["rustls"] } sysinfo = "0.39.6" strum_macros = { workspace = true } sha2 = "0.11.0" @@ -51,25 +51,25 @@ futures = { workspace = true } prost = "0.14.4" encoding_rs = "0.8.35" tokio-util = { version = "0.7.19", features = ["io", "compat"] } -uuid = "1.24.0" +uuid = "1.25.0" tokio-rustls = "0.26.4" rustls = { workspace = true } webpki-roots = "1.0.9" aes = "0.9.2" -ecb = { version = "0.2.0", features = ["alloc", "block-padding", "zeroize"] } +ecb = { version = "0.2.1", features = ["alloc", "block-padding", "zeroize"] } cbc = { version = "0.2.1", features = ["alloc", "block-padding", "zeroize"] } glob = "0.3.4" md5 = "0.8.1" bytes = "1.12.1" -async-trait = "0.1.91" -crc32fast = "1.5.0" +async-trait = "0.1.92" +crc32fast = "1.5.1" pelite = "0.10.0" gethostname = "1.1.0" thiserror = { workspace = true } url = { workspace = true } -http = "1.4.2" -globset = "0.4" -generic-array = "1.4.4" +http = "1.5.0" +globset = "0.4.20" +generic-array = "1.4.5" rustls-pki-types = "1.15.1" tokio-stream = "0.1.19" sha1 = "0.11.0" diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 21413d8f..9c18530a 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -17,7 +17,7 @@ use crate::{ util::native::{NativeError, SafeStr, maxima_dir}, }; use derive_getters::Getters; -use log::info; +use log::{debug, info}; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; @@ -118,17 +118,16 @@ impl OwnedOffer { None => return Err(LibraryError::NoManifest(self.slug.clone())), }; - let path = if let Some(path) = manifest.execute_path(trial) { - &Some(path) - } else { - self.offer.execute_path_override() + let raw = manifest + .execute_path(trial) + .map(|p| p.to_owned()) + .or_else(|| self.offer.execute_path_override().clone()); + + let Some(raw) = raw else { + return Err(LibraryError::NoPath(self.slug.clone())); }; - if let Some(path) = path { - Ok(parse_registry_path_json(path, Some(&self.slug)).await?) - } else { - Err(LibraryError::NoPath(self.slug.clone())) - } + Ok(parse_registry_path_json(&raw, Some(&self.slug)).await?) } pub async fn installed_version(&self) -> Result { diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index e2a79b8e..50cb86f3 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -15,7 +15,7 @@ log = { workspace = true } regex = { workspace = true } directories = { workspace = true } structured-logger = "1.0.5" -actix-web = "4.14.0" +actix-web = "4.15.0" sha2 = "0.11.0" derive_more = "2.1.1" hex = { workspace = true } diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index af587551..40565bab 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -9,9 +9,9 @@ maxima-lib = { path = "../maxima-lib" } windows-sys = { workspace = true, features = [ "Win32_System_ProcessStatus", "Win32_Networking_WinInet" ] } anyhow = { workspace = true } clap = { workspace = true } -eframe = { version = "0.35.0", default-features = false, features = [ "persistence", "glow", "wayland", "x11" ] } -egui = "0.35.0" -egui_extras = {version = "0.35.0", features = [ "svg", "all_loaders" ] } +eframe = { version = "0.36.1", default-features = false, features = [ "persistence", "glow", "wayland", "x11" ] } +egui = "0.36.1" +egui_extras = {version = "0.36.1", features = [ "svg", "all_loaders" ] } image = { version = "0.25.10", features = [ "png", "jpeg" ] } reqwest = { workspace = true } serde = { workspace = true } @@ -27,7 +27,7 @@ sys-locale = "0.3.2" humansize = { version = "2.1.3", features = ["no_alloc"] } fuzzy-matcher = "0.3.7" thiserror = { workspace = true } -winit = { version = "0.30", features = ["default"] } +winit = { version = "0.30.13", features = ["default"] } [target.'cfg(windows)'.dependencies] is_elevated = { workspace = true } diff --git a/maxima-ui/res/locale/fr_fr.json b/maxima-ui/res/locale/fr_fr.json index 36b6010c..70722cd4 100644 --- a/maxima-ui/res/locale/fr_fr.json +++ b/maxima-ui/res/locale/fr_fr.json @@ -95,6 +95,7 @@ "game_installation" : { "header": "Installation du jeu", "default_folder": "Dossier d'installation par défaut", + "default_wine_prefix": "Préfixe Wine par défaut", "ignore_ood_warning": "Ignorer l'avertissement de lancement de jeu obsolète" }, "performance" : { From d38e92ebc41d4f102116c41911312f350f708149 Mon Sep 17 00:00:00 2001 From: theofficialomega <30985701+BellezaEmporium@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:42:45 +0200 Subject: [PATCH 48/57] cargo fmt --- maxima-cli/src/main.rs | 74 ++++++++++++------- maxima-lib/build.rs | 2 +- maxima-lib/src/content/downloader.rs | 51 +++++++++---- maxima-lib/src/content/manager.rs | 16 ++-- maxima-lib/src/content/mod.rs | 15 ++-- maxima-lib/src/content/zip.rs | 8 +- maxima-lib/src/content/zlib.rs | 19 +++-- maxima-lib/src/core/background_service_nix.rs | 6 +- maxima-lib/src/core/library.rs | 2 +- maxima-lib/src/core/manifest/dip.rs | 10 ++- maxima-lib/src/core/manifest/mod.rs | 16 ++-- maxima-lib/src/core/manifest/pre_dip.rs | 10 ++- maxima-lib/src/lsx/connection.rs | 2 +- maxima-lib/src/rtm/client.rs | 27 ++++--- maxima-lib/src/rtm/connection.rs | 4 +- maxima-lib/src/unix/wine.rs | 3 +- maxima-lib/src/util/registry.rs | 2 +- maxima-service/src/service/mod.rs | 20 +++-- maxima-ui/src/bridge_thread.rs | 8 +- maxima-ui/src/views/game_view.rs | 6 +- 20 files changed, 190 insertions(+), 111 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 623c204f..20c32cbe 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -1,15 +1,17 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use maxima::content::manager::ProgressCallback; -use maxima::core::{manifest::handle_touchup_request}; use clap::{Parser, Subcommand}; +use maxima::content::manager::ProgressCallback; +use maxima::core::manifest::handle_touchup_request; +use std::sync::atomic::{AtomicUsize, Ordering}; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use inquire::{Select, Text}; use log::{debug, error, info, warn}; use regex::Regex; use std::{ - path::PathBuf, sync::{Arc, LazyLock}, time::Instant, + path::PathBuf, + sync::{Arc, LazyLock}, + time::Instant, }; #[cfg(windows)] @@ -17,20 +19,34 @@ use is_elevated::is_elevated; #[cfg(windows)] use maxima::{ - core::background_service::{request_registry_setup}, + core::background_service::request_registry_setup, util::service::{is_service_running, is_service_valid, register_service_user, start_service}, }; use maxima::{ - content::{ - ContentService, downloader::ZipDownloader, manager::QueuedGameBuilder, - }, core::{ - LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, auth::{ - TokenResponse, context::AuthContext, login::{begin_oauth_login_flow, manual_login}, nucleus_auth_exchange, nucleus_token_exchange, - }, clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, library::OwnedTitle, manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ - SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, ServiceLegacyOffer, ServicePlayer, + content::{ContentService, downloader::ZipDownloader, manager::QueuedGameBuilder}, + core::{ + LockedMaxima, Maxima, MaximaEvent, MaximaOptionsBuilder, + auth::{ + TokenResponse, + context::AuthContext, + login::{begin_oauth_login_flow, manual_login}, + nucleus_auth_exchange, nucleus_token_exchange, + }, + clients::JUNO_PC_CLIENT_ID, + cloudsync::CloudSyncLockMode, + launch::{self, LaunchMode, LaunchOptions}, + library::OwnedTitle, + manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, + service_layer::{ + SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, + ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, + ServiceLegacyOffer, ServicePlayer, }, - }, ooa, rtm::client::BasicPresence, util::{ + }, + ooa, + rtm::client::BasicPresence, + util::{ log::init_logger, native::{maxima_dir, take_foreground_focus}, registry::check_registry_validity, @@ -386,10 +402,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { }) .await??; - owned_games - .iter() - .find(|g| g.name() == name) - .unwrap() + owned_games.iter().find(|g| g.name() == name).unwrap() }; let offer_id = game.base_offer().offer_id().to_owned(); @@ -413,7 +426,8 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let path = PathBuf::from( tokio::task::spawn_blocking(|| { - Text::new("Where would you like to install the game? (must be an absolute path)").prompt() + Text::new("Where would you like to install the game? (must be an absolute path)") + .prompt() }) .await??, ); @@ -425,7 +439,8 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { #[cfg(unix)] let wine_prefix = { let input = tokio::task::spawn_blocking(|| { - Text::new("Where do you want to store the Wine prefix? (must be an absolute path)").prompt() + Text::new("Where do you want to store the Wine prefix? (must be an absolute path)") + .prompt() }) .await??; let p = PathBuf::from(input); @@ -471,7 +486,12 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { if let Some(downloader) = maxima.content_manager().current() { saw_downloader = true; idle_ticks = 0; - info!("Downloading: {:.1}%/100%, {} bytes out of {}", downloader.percentage_done(), downloader.completed_bytes(), downloader.bytes_total()); + info!( + "Downloading: {:.1}%/100%, {} bytes out of {}", + downloader.percentage_done(), + downloader.completed_bytes(), + downloader.bytes_total() + ); } else if saw_downloader { break; } else { @@ -538,7 +558,9 @@ async fn download_specific_file( }); let ele = entry.unwrap(); let output_dir = std::env::current_dir()?.join("downloads"); - downloader.download_single_file(ele, &output_dir, on_progress).await?; + downloader + .download_single_file(ele, &output_dir, on_progress) + .await?; info!( "Downloaded file {} from game build {}", @@ -864,11 +886,7 @@ async fn start_game( launch::start_game( maxima_arc.clone(), - LaunchMode::OnlineOffline( - offer_id.to_owned(), - persona.to_owned(), - password.to_owned(), - ), + LaunchMode::OnlineOffline(offer_id.to_owned(), persona.to_owned(), password.to_owned()), launch_options, ) .await?; @@ -894,4 +912,4 @@ async fn start_game( } Ok(()) -} \ No newline at end of file +} diff --git a/maxima-lib/build.rs b/maxima-lib/build.rs index 43ba4c80..a334786a 100644 --- a/maxima-lib/build.rs +++ b/maxima-lib/build.rs @@ -11,4 +11,4 @@ fn main() -> std::io::Result<()> { ], &["src/rtm/proto/"], ) -} \ No newline at end of file +} diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 311dd9b1..7bcf2855 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -5,7 +5,7 @@ use std::{ task::{self, Poll}, }; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use async_compression::tokio::write::DeflateDecoder; use bytes::{Bytes, BytesMut}; use futures::{Stream, StreamExt, TryStreamExt}; @@ -13,12 +13,15 @@ use log::{debug, error, info}; use reqwest::{Client, StatusCode}; use strum_macros::Display; use tokio::{ - fs::{self, OpenOptions, create_dir, create_dir_all}, io::{AsyncWrite, AsyncWriteExt, BufReader}, + fs::{self, OpenOptions, create_dir, create_dir_all}, + io::{AsyncWrite, AsyncWriteExt, BufReader}, }; use tokio_util::compat::FuturesAsyncReadCompatExt; use crate::content::{ - manager::ProgressCallback, zip::CompressionType, zlib::{restore_zlib_state, write_zlib_state}, + manager::ProgressCallback, + zip::CompressionType, + zlib::{restore_zlib_state, write_zlib_state}, }; use super::zip::{ZipFile, ZipFileEntry}; @@ -69,7 +72,7 @@ impl RestorableDeflateDecoder { .ok_or_else(|| anyhow::anyhow!("no cache dir"))? .join("Maxima"); std::fs::create_dir_all(&dir)?; - + let safe_name = self.file_name.replace(['/', '\\'], "_"); Ok(dir.join(format!("{}.state", safe_name))) } @@ -107,7 +110,8 @@ impl RestorableDecoder for RestorableDeflateDecoder { } fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError> { - if !self.file_path.exists() || std::fs::metadata(self.file_path.clone()).unwrap().len() == 0 { + if !self.file_path.exists() || std::fs::metadata(self.file_path.clone()).unwrap().len() == 0 + { return Err(DecoderRestoreError::CacheEmpty); } let path = self @@ -179,7 +183,7 @@ impl ZipDownloader { let _ = std::fs::remove_file(dir.join("Maxima").join(format!("{}.state", safe_name))); } } - + pub async fn new(url: &str) -> Result { let manifest = ZipFile::fetch(url).await?; Ok(Self { @@ -197,7 +201,7 @@ impl ZipDownloader { &self, entry: &ZipFileEntry, output_dir: &Path, - progress_callback: ProgressCallback + progress_callback: ProgressCallback, ) -> Result { let file_path = output_dir.join(entry.name()); @@ -242,12 +246,13 @@ impl ZipDownloader { } CompressionType::Deflate => { let decoder = DeflateDecoder::new(writer); - let mut decoder = RestorableDeflateDecoder::new(decoder, entry.name().into(), file_path.clone()); + let mut decoder = + RestorableDeflateDecoder::new(decoder, entry.name().into(), file_path.clone()); match decoder.restore_state() { Ok((bytes_in, _bytes_out)) => { // bytes_in = compressed bytes consumed (from zlib state) - compressed_offset = bytes_in; // Use this for HTTP range + compressed_offset = bytes_in; // Use this for HTTP range // Note: we don't need bytes_out here, but we keep it for set_len decoder.get_mut().get_mut().set_len(_bytes_out).await?; } @@ -261,10 +266,15 @@ impl ZipDownloader { }; progress_callback(compressed_offset as usize); let offset = entry.data_offset(); - let start_offset = offset + compressed_offset as i64; // Add compressed offset, not decompressed + let start_offset = offset + compressed_offset as i64; // Add compressed offset, not decompressed let end_offset = offset + entry.compressed_size() - 1; - debug!("Requesting range: {}-{} (entry compressed size: {})", start_offset, end_offset, entry.compressed_size()); + debug!( + "Requesting range: {}-{} (entry compressed size: {})", + start_offset, + end_offset, + entry.compressed_size() + ); // Ensure we don't request past the end or invalid ranges if compressed_offset >= *entry.compressed_size() as u64 { @@ -274,7 +284,11 @@ impl ZipDownloader { return Ok(compressed_offset); } if start_offset > end_offset { - bail!("Invalid range calculation: start {} > end {}", start_offset, end_offset); + bail!( + "Invalid range calculation: start {} > end {}", + start_offset, + end_offset + ); } let range = format!("bytes={}-{}", start_offset, end_offset); @@ -302,7 +316,10 @@ impl ZipDownloader { break res; } Err(err) if attempt < MAX_RETRIES => { - error!("Failed to download ({}) attempt {attempt}: {err}", file_path.display()); + error!( + "Failed to download ({}) attempt {attempt}: {err}", + file_path.display() + ); tokio::time::sleep(std::time::Duration::from_secs(1 << attempt)).await; } Err(err) => return Err(err.into()), @@ -313,7 +330,6 @@ impl ZipDownloader { let stream = ByteCountingStream::new(data.bytes_stream()); let mut stream_reader = BufReader::new(stream.into_async_read().compat()); - tokio::io::copy(&mut stream_reader, &mut writer).await?; writer.shutdown().await?; // flush decoder + BufWriter to disk progress_callback(content_length as usize); @@ -331,7 +347,10 @@ where S: Stream>, { fn new(inner: S) -> Self { - Self { inner, byte_count: 0 } + Self { + inner, + byte_count: 0, + } } } @@ -361,4 +380,4 @@ where Poll::Pending => Poll::Pending, } } -} \ No newline at end of file +} diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 4c08997d..f8036002 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -2,8 +2,8 @@ use crate::{content::exclusion::get_exclusion_list, core::manifest::handle_touch use std::{ path::PathBuf, sync::{ - atomic::{AtomicUsize, Ordering}, Arc, + atomic::{AtomicUsize, Ordering}, }, }; @@ -114,7 +114,10 @@ impl DownloadQueue { Err(err) => { // Keep the corrupt file for inspection instead of silently discarding it let backup = file.with_extension("json.bak"); - error!("Corrupt download queue, backing up to {}: {err}", backup.display()); + error!( + "Corrupt download queue, backing up to {}: {err}", + backup.display() + ); let _ = fs::rename(&file, &backup).await; Ok(Self::default()) } @@ -175,8 +178,7 @@ impl GameDownloader { let downloader = ZipDownloader::new(url.url()).await?; let exclusion_list = get_exclusion_list(&game.slug()); let mut entries = Vec::new(); - for ele in downloader.manifest().entries() - { + 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 @@ -458,7 +460,11 @@ impl ContentManager { return Ok(()); // already at the top } - let Some(index) = self.queue.queued.iter().position(|g| g.offer_id() == offer_id) + let Some(index) = self + .queue + .queued + .iter() + .position(|g| g.offer_id() == offer_id) else { return Ok(()); // not in the queue at all }; diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 9cc6b5b4..fac4a480 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -1,9 +1,14 @@ -use std::time::Duration; use anyhow::Result; +use std::time::Duration; use crate::core::{ - auth::storage::LockedAuthStorage, cache::DynamicCache, service_layer::{ - SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, ServiceAvailableBuildsBuilder, ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, ServiceDownloadUrlMetadata, ServiceDownloadUrlRequestBuilder, ServiceLayerClient, ServiceLayerError, + auth::storage::LockedAuthStorage, + cache::DynamicCache, + service_layer::{ + SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, + ServiceAvailableBuildsBuilder, ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, + ServiceDownloadUrlMetadata, ServiceDownloadUrlRequestBuilder, ServiceLayerClient, + ServiceLayerError, }, }; @@ -50,7 +55,7 @@ impl ContentService { pub async fn available_builds( &self, - offer_id: &str + offer_id: &str, ) -> Result { let cache_key = "builds_".to_owned() + offer_id; if let Some(cached) = self.request_cache.get(&cache_key) { @@ -96,4 +101,4 @@ impl ContentService { self.request_cache.insert(cache_key, url.clone()); Ok(url) } -} \ No newline at end of file +} diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index 5702533c..d0b04459 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -1,9 +1,9 @@ use bytebuffer::{ByteBuffer, Endian}; use derive_getters::Getters; -use encoding_rs::{WINDOWS_1252}; +use encoding_rs::WINDOWS_1252; use log::{debug, warn}; -use reqwest::header::ToStrError; use reqwest::Client; +use reqwest::header::ToStrError; use std::cmp; use std::string::FromUtf8Error; use thiserror::Error; @@ -419,7 +419,7 @@ impl ZipFile { idx: i, total: eocd.total_entries, err: format!("{:?}", err).to_string(), - }) + }); } Ok(e) => e, }; @@ -450,4 +450,4 @@ impl ZipFile { Ok(()) } -} \ No newline at end of file +} diff --git a/maxima-lib/src/content/zlib.rs b/maxima-lib/src/content/zlib.rs index 0903295b..6b0a46e9 100644 --- a/maxima-lib/src/content/zlib.rs +++ b/maxima-lib/src/content/zlib.rs @@ -146,14 +146,13 @@ pub fn restore_zlib_state(buf: &mut Bytes, stream: &mut mz_stream) { let zalloc = stream.zalloc; state_ref.window = unsafe { zalloc(stream.opaque, 1, window_size as c_uint) as *mut c_char }; - assert!(!state_ref.window.is_null(), "zalloc failed for inflate window"); + assert!( + !state_ref.window.is_null(), + "zalloc failed for inflate window" + ); unsafe { - ptr::copy_nonoverlapping( - window.as_ptr(), - state_ref.window as *mut u8, - window_size, - ); + ptr::copy_nonoverlapping(window.as_ptr(), state_ref.window as *mut u8, window_size); } } @@ -161,7 +160,11 @@ pub fn restore_zlib_state(buf: &mut Bytes, stream: &mut mz_stream) { let distcode = buf.get_u32() as isize; let nextcode = buf.get_u32() as isize; - for (name, idx) in [("lencode", lencode), ("distcode", distcode), ("next", nextcode)] { + for (name, idx) in [ + ("lencode", lencode), + ("distcode", distcode), + ("next", nextcode), + ] { assert!( (0..Z_ENOUGH as isize).contains(&idx), "Can't deserialize this zlib state, {name} out of range" @@ -175,4 +178,4 @@ pub fn restore_zlib_state(buf: &mut Bytes, stream: &mut mz_stream) { state_ref.lenbits = buf.get_u32(); state_ref.distbits = buf.get_u32(); -} \ No newline at end of file +} diff --git a/maxima-lib/src/core/background_service_nix.rs b/maxima-lib/src/core/background_service_nix.rs index b7fb943e..02dc3fa3 100644 --- a/maxima-lib/src/core/background_service_nix.rs +++ b/maxima-lib/src/core/background_service_nix.rs @@ -42,7 +42,8 @@ pub async fn wine_get_pid( module_path()? .safe_parent()? .join("wine-helper.exe") - .safe_str()?.into(), + .safe_str()? + .into(), Some(vec!["get_pid", b64.as_str()]), None, true, @@ -85,7 +86,8 @@ pub async fn request_library_injection( module_path()? .safe_parent()? .join("wine-helper.exe") - .safe_str()?.into(), + .safe_str()? + .into(), Some(vec!["inject", b64.as_str()]), None, false, diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 9c18530a..8e8efa2e 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -60,7 +60,7 @@ pub struct OwnedOffer { impl OwnedOffer { #[cfg(windows)] pub async fn check_install_win_registry(&self) -> bool { - use crate::util::registry::{parse_partial_registry_path}; + use crate::util::registry::parse_partial_registry_path; let Some(path) = &self.offer.install_check_override().as_ref() else { log::warn!("[{}] No install_check_override", self.slug); diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 078e8f3a..5fa06a2f 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -332,7 +332,15 @@ impl DiPManifest { let args = self.collect_touchup_args(&install_path, self.locale())?; let path = install_path.join(&self.touchup.path()); let path = case_insensitive_path(path).to_string_lossy().to_string(); - run_wine_command(path.into(), Some(args), None, true, CommandType::Run, Some(slug)).await?; + run_wine_command( + path.into(), + Some(args), + None, + true, + CommandType::Run, + Some(slug), + ) + .await?; invalidate_mx_wine_registry().await; Ok(()) } diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index 8c36bf41..0491ee4b 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -105,7 +105,9 @@ pub fn from_bytes(bytes: &[u8]) -> Result Result, ManifestError> { +pub async fn load_manifest_from_disk( + path_to_xml: PathBuf, +) -> Result, ManifestError> { let bytes = tokio::fs::read(&path_to_xml).await?; if let Ok(m) = from_bytes::(&bytes) { @@ -121,7 +123,7 @@ pub async fn load_manifest_from_disk(path_to_xml: PathBuf) -> Result Result<(), ManifestError> { @@ -129,7 +131,10 @@ pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<( let client = reqwest::Client::new(); let _ = client - .post(format!("http://127.0.0.1:{}/touchup", BACKGROUND_SERVICE_PORT)) + .post(format!( + "http://127.0.0.1:{}/touchup", + BACKGROUND_SERVICE_PORT + )) .json(&ServiceTouchupRequest { output_dir: install_path.to_string_lossy().into_owned(), slug: slug.to_string(), @@ -139,9 +144,10 @@ pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<( Ok(()) } - #[cfg(unix)] pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<(), ManifestError> { - let manifest = load_manifest_from_disk(Path::new(install_path).join(MANIFEST_RELATIVE_PATH)).await.unwrap(); + let manifest = load_manifest_from_disk(Path::new(install_path).join(MANIFEST_RELATIVE_PATH)) + .await + .unwrap(); manifest.run_touchup(Path::new(install_path), slug).await } diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 20b40cde..083a6fb3 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -263,7 +263,15 @@ impl PreDiPManifest { let path = install_path.join(remove_leading_slash(&self.executable.filePath)); let path = case_insensitive_path(path).to_string_lossy().to_string(); - run_wine_command(path.into(), Some(args), None, true, CommandType::Run, Some(slug)).await?; + run_wine_command( + path.into(), + Some(args), + None, + true, + CommandType::Run, + Some(slug), + ) + .await?; invalidate_mx_wine_registry().await; Ok(()) diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index ad84081b..8bba9e42 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -318,7 +318,7 @@ impl Connection { let state = self.state.write().await; - let trimmed_buffer = &buffer[..(n-1)]; + let trimmed_buffer = &buffer[..(n - 1)]; let message = if let EncryptionState::Enabled(key) = state.encryption { simple_decrypt(trimmed_buffer, &key) } else { diff --git a/maxima-lib/src/rtm/client.rs b/maxima-lib/src/rtm/client.rs index 3aa657be..27fe7cd4 100644 --- a/maxima-lib/src/rtm/client.rs +++ b/maxima-lib/src/rtm/client.rs @@ -12,8 +12,9 @@ use super::{ RtmError, connection::RtmConnectionManager, proto::{ - BasicPresenceType, HeartbeatV1, LoginV3Response, Player, PresenceUpdateV1, PresenceSubscribeAllFriendsV1, - RichPresenceType, RichPresenceV1, SessionCleanupV1, communication_v1, success_v1, + BasicPresenceType, HeartbeatV1, LoginV3Response, Player, PresenceSubscribeAllFriendsV1, + PresenceUpdateV1, RichPresenceType, RichPresenceV1, SessionCleanupV1, communication_v1, + success_v1, }, }; use crate::{ @@ -172,9 +173,7 @@ impl RtmClient { return Ok(()); } - let res: ClientVersion = serde_json::from_str( - &presence.client_version, - )?; + let res: ClientVersion = serde_json::from_str(&presence.client_version)?; if res.client_type != "Client" && res.client_type != "LegacyClient" { return Ok(()); @@ -249,8 +248,8 @@ impl RtmClient { pub async fn set_presence( &mut self, basic_presence: BasicPresence, - game_name: &str, // e.g. "Apex Legends" - offer_id: &str, // e.g. "Origin.OFR.50.0002148" + game_name: &str, // e.g. "Apex Legends" + offer_id: &str, // e.g. "Origin.OFR.50.0002148" ) -> Result<(), RtmError> { let rpc_data = CustomRichPresenceData { game_product_id: offer_id.to_owned(), @@ -290,7 +289,11 @@ impl RtmClient { } /// Subscribe to a list of user IDs' presences - pub async fn subscribe(&mut self, persona: Vec, players: &[String]) -> Result<(), RtmError> { + pub async fn subscribe( + &mut self, + persona: Vec, + players: &[String], + ) -> Result<(), RtmError> { send_and_forget_rtm_request!(self.conn_man, PresenceSubscribe, PresenceSubscribeV1, { persona_id: vec![], players: players.iter().map(|id| Player{ player_id: id.to_owned(), product_id: String::from("origin"), }).collect() @@ -298,9 +301,13 @@ impl RtmClient { .await } - pub async fn subscribe_all(&mut self) -> Result<(), RtmError> { - send_and_forget_rtm_request!(self.conn_man, PresenceSubscribeAllFriendsV1, PresenceSubscribeAllFriendsV1, {}) + send_and_forget_rtm_request!( + self.conn_man, + PresenceSubscribeAllFriendsV1, + PresenceSubscribeAllFriendsV1, + {} + ) .await } diff --git a/maxima-lib/src/rtm/connection.rs b/maxima-lib/src/rtm/connection.rs index 5f1ff126..f4ec9c1e 100644 --- a/maxima-lib/src/rtm/connection.rs +++ b/maxima-lib/src/rtm/connection.rs @@ -214,9 +214,7 @@ impl RtmConnectionManager { } _ => Err(RtmError::NoBody), }, - Err(_) => Err(RtmError::Io(io::Error::other( - "Failed to receive response", - ))), + Err(_) => Err(RtmError::Io(io::Error::other("Failed to receive response"))), } } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index e3d3f2fa..3c5e879b 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -273,7 +273,8 @@ pub async fn run_wine_command, T: AsRef>( if let Some(cwd) = cwd { child.current_dir(cwd); - } else if arg.to_string_lossy().contains("/") { // Some games (like Mass Effect LE) require the working directory to be set to the game directory, other it crashes + } else if arg.to_string_lossy().contains("/") { + // Some games (like Mass Effect LE) require the working directory to be set to the game directory, other it crashes let mut cwd_dir = PathBuf::from(arg.clone()); cwd_dir.pop(); info!("Setting current working directory to: {:?}", cwd_dir); diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index b9c9ce5a..b8991a28 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -246,7 +246,7 @@ pub async fn parse_registry_path_regkey(key: &str) -> Result Result { diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 1413897e..1c1ee270 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -5,7 +5,7 @@ use std::sync::mpsc as std_mpsc; use std::thread; use std::time::Duration; -use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web::{HttpResponse, Responder, get, post, web}; use log::{error, info}; use maxima::core::background_service::{ BACKGROUND_SERVICE_PORT, ServiceLibraryInjectionRequest, ServiceTouchupRequest, @@ -17,8 +17,7 @@ use maxima::util::service::SERVICE_NAME; use structured_logger::json::new_writer; use tokio::sync::oneshot; use windows_service::service::{ - ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, - ServiceType, + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, }; use windows_service::{ service_control_handler::{self, ServiceControlHandlerResult}, @@ -173,9 +172,8 @@ fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { let _ = stop_tx.send(()); let _ = _handle.join(); - status_handle.set_service_status(stopped_status(ServiceExitCode::Win32( - ERROR_TIMEOUT, - )))?; + status_handle + .set_service_status(stopped_status(ServiceExitCode::Win32(ERROR_TIMEOUT)))?; return Err(ServerError::BindTimeout); } @@ -208,10 +206,8 @@ fn bootstrap_service(_arguments: Vec) -> Result<(), ServerError> { Ok(Event::Exited(Err(error))) => { error!("HTTP server failed unexpectedly: {error}"); - let win32_code = error - .raw_os_error() - .unwrap_or(ERROR_UNEXPECTED_EXIT as i32) - as u32; + let win32_code = + error.raw_os_error().unwrap_or(ERROR_UNEXPECTED_EXIT as i32) as u32; break (false, ServiceExitCode::Win32(win32_code)); } @@ -295,7 +291,9 @@ async fn req_touchup(body: web::Bytes) -> Result { Path::new(&request.output_dir).join(maxima::core::manifest::MANIFEST_RELATIVE_PATH), ) .await?; - manifest.run_touchup(Path::new(&request.output_dir), &request.slug).await?; + manifest + .run_touchup(Path::new(&request.output_dir), &request.slug) + .await?; Ok(HttpResponse::Ok().body("Done")) } diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 17ce7b6e..0f514f32 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,10 +1,10 @@ -use maxima::core::manifest::handle_touchup_request; -use tokio::sync::mpsc::unbounded_channel; -use tokio::sync::mpsc::UnboundedSender; -use tokio::sync::mpsc::UnboundedReceiver; use egui::{Context, Key::W}; use log::{error, info, warn}; +use maxima::core::manifest::handle_touchup_request; use thiserror::Error; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::unbounded_channel; use crate::{ GameDetails, GameInfo, GameSettings, diff --git a/maxima-ui/src/views/game_view.rs b/maxima-ui/src/views/game_view.rs index 9e94173e..406e941f 100644 --- a/maxima-ui/src/views/game_view.rs +++ b/maxima-ui/src/views/game_view.rs @@ -291,8 +291,7 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { ); } - if hero_vis_frac < 1.0 - { + if hero_vis_frac < 1.0 { let full_tint = if ui.is_enabled() { Color32::from_black_alpha(20) } else { @@ -300,7 +299,8 @@ pub fn game_view_details_panel(app: &mut MaximaEguiApp, ui: &mut Ui) { }; let fade_tint = Color32::from_black_alpha(0); - let bottom_y = hero_rect.left_bottom() - vec2(0.0, hero_rect.height() * hero_vis_frac); + let bottom_y = + hero_rect.left_bottom() - vec2(0.0, hero_rect.height() * hero_vis_frac); let mut mesh = Mesh::default(); // bottom edge fades to transparent — reads as the hero "dissolving" From 53e8dbb31ddca5a53539a87e6e9fb195751d3937 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:42:47 -0500 Subject: [PATCH 49/57] Disable state serialization on Linux, cleanup handling of manifest and gameinfo JSON creation --- maxima-bootstrap/src/main.rs | 4 ++- maxima-cli/src/main.rs | 2 +- maxima-lib/src/content/downloader.rs | 7 +++++ maxima-lib/src/content/manager.rs | 11 ++++--- maxima-lib/src/core/auth/hardware.rs | 4 +-- maxima-lib/src/core/background_service_nix.rs | 8 ++--- maxima-lib/src/core/background_service_win.rs | 1 - maxima-lib/src/core/cloudsync.rs | 2 +- maxima-lib/src/core/launch.rs | 4 +-- maxima-lib/src/core/manifest/dip.rs | 14 ++------- maxima-lib/src/core/manifest/mod.rs | 30 +++++++++++-------- maxima-lib/src/core/manifest/pre_dip.rs | 14 ++------- maxima-lib/src/gameinfo/mod.rs | 1 - maxima-lib/src/ooa/mod.rs | 2 +- maxima-lib/src/unix/wine.rs | 19 ++++++------ maxima-service/src/service/mod.rs | 4 +-- maxima-ui/src/bridge_thread.rs | 5 +--- 17 files changed, 60 insertions(+), 72 deletions(-) diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index 5dcc957e..aa79f146 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -125,6 +125,8 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { use maxima::unix::wine::CommandType; use maxima::unix::wine::run_wine_command; +use maxima::unix::wine::wine_prefix_dir; + run_wine_command( args.path.into(), @@ -132,7 +134,7 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { None, false, CommandType::WaitForExitAndRun, - Some(&args.slug), + &wine_prefix_dir(&args.slug).unwrap(), ) .await?; diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 20c32cbe..14a07646 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -808,7 +808,7 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { async fn locate_game(_maxima_arc: LockedMaxima, install_path: &str, slug: &str) -> Result<()> { let path: PathBuf = PathBuf::from(install_path); - handle_touchup_request(&path, slug).await?; + handle_touchup_request(&path, None, slug).await?; info!("Installed!"); Ok(()) } diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 7bcf2855..e248c429 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -109,6 +109,12 @@ impl RestorableDecoder for RestorableDeflateDecoder { } } + #[cfg(unix)] + fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError> { + return Err(DecoderRestoreError::CacheEmpty); + } + + #[cfg(windows)] fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError> { if !self.file_path.exists() || std::fs::metadata(self.file_path.clone()).unwrap().len() == 0 { @@ -151,6 +157,7 @@ impl AsyncWrite for RestorableDeflateDecoder { self.update_bytes_written(*n); } + #[cfg(windows)] if self.should_save { if let Poll::Ready(Ok(())) = Pin::new(&mut self.inner).poll_flush(cx) { debug!("save interval reached, serializing state..."); diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index f8036002..4586f2c7 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -216,7 +216,7 @@ impl GameDownloader { let notify_done = notify.clone(); let slug = self.slug.clone(); - let game_install_info = GameInstallInfo::new(self.path.clone(), self.wine_prefix.clone()); + let wine_prefix = self.wine_prefix.clone(); tokio::spawn(async move { let dl = GameDownloader::start_downloads( downloader_arc, @@ -226,7 +226,7 @@ impl GameDownloader { notify, output_dir, slug, - game_install_info, + wine_prefix, ) .await; if let Err(err) = dl { @@ -262,9 +262,9 @@ impl GameDownloader { cancel_token: CancellationToken, completed_bytes: Arc, notify: Arc, - output_dir: PathBuf, + output_dir: PathBuf, // Install dir slug: String, - game_install_info: GameInstallInfo, + wine_prefix_path: Option, ) -> Result<(), DownloaderError> { let mut handles = Vec::with_capacity(entries.len()); @@ -308,10 +308,9 @@ impl GameDownloader { return Ok(()); } - handle_touchup_request(&output_dir, &slug).await?; // output dir is install path + handle_touchup_request(&output_dir, None, &slug).await?; info!("Installation finished!"); - game_install_info.save_to_json(&slug); completed_bytes.fetch_add(1, Ordering::SeqCst); notify.notify_one(); Ok(()) diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index 31ecd7f2..2cfac08f 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -476,14 +476,14 @@ impl HardwareInfo { } #[cfg(unix)] -fn get_root_creation_str(slug: Option<&str>) -> String { +fn get_root_creation_str(slug: Option<&str>) -> String { //Option for slug incase of PCSign use crate::unix::wine::wine_prefix_dir; use chrono::{DateTime, TimeZone}; use std::{fs, os::unix::fs::MetadataExt}; let date_str = String::from("1970010100:00:00.000000000+0000"); let wine_prefix = match slug { - Some(slug) => wine_prefix_dir(Some(slug)).unwrap(), + Some(slug) => wine_prefix_dir(slug).unwrap(), None => return date_str, }; let date_str = match fs::metadata(wine_prefix.join("pfx").join("drive_c")) { diff --git a/maxima-lib/src/core/background_service_nix.rs b/maxima-lib/src/core/background_service_nix.rs index 02dc3fa3..4cb769df 100644 --- a/maxima-lib/src/core/background_service_nix.rs +++ b/maxima-lib/src/core/background_service_nix.rs @@ -5,8 +5,7 @@ use serde::Serialize; use std::sync::LazyLock; use crate::{ - unix::wine::{CommandType, run_wine_command}, - util::native::{NativeError, SafeParent, SafeStr, module_path}, + unix::wine::{CommandType, run_wine_command, wine_prefix_dir}, util::native::{NativeError, SafeParent, SafeStr, module_path}, }; static PID_PATTERN: LazyLock = LazyLock::new(|| { @@ -37,6 +36,7 @@ pub async fn wine_get_pid( name: name.to_owned(), }; + let wine_prefix_path = wine_prefix_dir(slug.unwrap())?; let b64 = general_purpose::STANDARD.encode(serde_json::to_string(&launch_args)?); let output = run_wine_command( module_path()? @@ -48,7 +48,7 @@ pub async fn wine_get_pid( None, true, CommandType::RunInPrefix, - slug, + &wine_prefix_path, ) .await?; @@ -92,7 +92,7 @@ pub async fn request_library_injection( None, false, CommandType::RunInPrefix, - slug, + &wine_prefix_dir(slug.unwrap()).unwrap(), ) .await?; diff --git a/maxima-lib/src/core/background_service_win.rs b/maxima-lib/src/core/background_service_win.rs index 1756e7aa..891239a4 100644 --- a/maxima-lib/src/core/background_service_win.rs +++ b/maxima-lib/src/core/background_service_win.rs @@ -19,7 +19,6 @@ pub struct ServiceLibraryInjectionRequest { #[derive(serde::Serialize, serde::Deserialize)] pub struct ServiceTouchupRequest { pub output_dir: String, - pub slug: String, } pub async fn request_library_injection( diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 37c86049..76bcad92 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -107,7 +107,7 @@ fn home_dir(_slug: Option<&str>) -> Result { #[cfg(unix)] fn home_dir(slug: Option<&str>) -> Result { use crate::unix::wine::wine_prefix_dir; - Ok(wine_prefix_dir(slug) + Ok(wine_prefix_dir(slug.unwrap()) .unwrap() .join("drive_c/users/steamuser")) } diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index a2058f69..21aed3e9 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -252,7 +252,7 @@ pub async fn start_game( }; #[cfg(unix)] - mx_linux_setup(slug.as_deref()).await?; + mx_linux_setup(slug.as_deref().unwrap()).await?; match mode { LaunchMode::Offline(_) => {} @@ -432,7 +432,7 @@ async fn request_opaque_ooa_token(access_token: &str) -> Result) -> Result<(), NativeError> { +pub async fn mx_linux_setup(slug: &str) -> Result<(), NativeError> { use crate::unix::wine::{ check_runtime_validity, get_lutris_runtimes, install_runtime, setup_wine_registry, wine_prefix_dir, diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 5fa06a2f..9e6af175 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -319,7 +319,7 @@ impl DiPManifest { pub async fn run_touchup( &self, install_path: &PathBuf, - slug: &str, + wine_prefix_path: Option, ) -> Result<(), ManifestError> { use crate::unix::{ fs::case_insensitive_path, @@ -332,15 +332,7 @@ impl DiPManifest { let args = self.collect_touchup_args(&install_path, self.locale())?; let path = install_path.join(&self.touchup.path()); let path = case_insensitive_path(path).to_string_lossy().to_string(); - run_wine_command( - path.into(), - Some(args), - None, - true, - CommandType::Run, - Some(slug), - ) - .await?; + run_wine_command(path.into(), Some(args), None, true, CommandType::Run, &wine_prefix_path.unwrap()).await?; invalidate_mx_wine_registry().await; Ok(()) } @@ -349,7 +341,7 @@ impl DiPManifest { pub async fn run_touchup( &self, install_path: &PathBuf, - _slug: &str, + _wine_prefix_path: Option, ) -> 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 0491ee4b..a7a992c5 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -37,16 +37,16 @@ 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: &Path, slug: &str) -> Result<(), ManifestError>; + async fn run_touchup(&self, install_path: &Path, wine_prefix_path: Option) -> Result<(), ManifestError>; fn execute_path(&self, trial: bool) -> Option; fn version(&self) -> Option; fn needs_touchup_on_locate(&self) -> bool; } #[async_trait::async_trait] impl GameManifest for DiPManifest { - async fn run_touchup(&self, install_path: &Path, slug: &str) -> Result<(), ManifestError> { + async fn run_touchup(&self, install_path: &Path, wine_prefix_path: Option) -> Result<(), ManifestError> { let install_path = install_path.to_path_buf(); - self.run_touchup(&install_path, slug).await + self.run_touchup(&install_path, wine_prefix_path).await } fn execute_path(&self, trial: bool) -> Option { @@ -68,9 +68,9 @@ impl GameManifest for DiPManifest { #[async_trait::async_trait] impl GameManifest for PreDiPManifest { - async fn run_touchup(&self, install_path: &Path, slug: &str) -> Result<(), ManifestError> { + async fn run_touchup(&self, install_path: &Path, wine_prefix_path: Option) -> Result<(), ManifestError> { let install_path = install_path.to_path_buf(); - self.run_touchup(&install_path, slug).await + self.run_touchup(&install_path, wine_prefix_path).await } fn execute_path(&self, _: bool) -> Option { @@ -126,8 +126,8 @@ pub async fn load_manifest_from_disk( // 2 different thing for windows and unix as only windows needs elevation, #[cfg(windows)] -pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<(), ManifestError> { - use crate::core::background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}; +pub async fn handle_touchup_request(install_path: &Path, _wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { + use crate::{core::background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, gameinfo::GameInstallInfo}; let client = reqwest::Client::new(); let _ = client @@ -137,17 +137,21 @@ pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<( )) .json(&ServiceTouchupRequest { output_dir: install_path.to_string_lossy().into_owned(), - slug: slug.to_string(), }) .send() .await?; + let game_install_info = GameInstallInfo::new(install_path.to_path_buf(), None); + game_install_info.save_to_json(slug); Ok(()) } #[cfg(unix)] -pub async fn handle_touchup_request(install_path: &Path, slug: &str) -> Result<(), ManifestError> { - let manifest = load_manifest_from_disk(Path::new(install_path).join(MANIFEST_RELATIVE_PATH)) - .await - .unwrap(); - manifest.run_touchup(Path::new(install_path), slug).await +pub async fn handle_touchup_request(install_path: &Path, wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { + use crate::gameinfo::GameInstallInfo; + + let manifest = load_manifest_from_disk(Path::new(install_path).join(MANIFEST_RELATIVE_PATH)).await.unwrap(); + manifest.run_touchup(Path::new(install_path), wine_prefix_path.clone()).await?; + let game_install_info = GameInstallInfo::new(install_path.to_path_buf(), wine_prefix_path); + game_install_info.save_to_json(slug); + Ok(()) } diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 083a6fb3..13fcbd3a 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -249,7 +249,7 @@ impl PreDiPManifest { pub async fn run_touchup( &self, install_path: &PathBuf, - slug: &str, + wine_prefix_path: Option, ) -> Result<(), ManifestError> { use crate::unix::{ fs::case_insensitive_path, @@ -263,15 +263,7 @@ impl PreDiPManifest { let path = install_path.join(remove_leading_slash(&self.executable.filePath)); let path = case_insensitive_path(path).to_string_lossy().to_string(); - run_wine_command( - path.into(), - Some(args), - None, - true, - CommandType::Run, - Some(slug), - ) - .await?; + run_wine_command(path.into(), Some(args), None, true, CommandType::Run, &wine_prefix_path.unwrap()).await?; invalidate_mx_wine_registry().await; Ok(()) @@ -281,7 +273,7 @@ impl PreDiPManifest { pub async fn run_touchup( &self, install_path: &PathBuf, - _slug: &str, + _wine_prefix_path: Option, ) -> Result<(), ManifestError> { use crate::util::native::NativeError; use tokio::process::Command; diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index 698d148f..4bdaf431 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -65,7 +65,6 @@ impl GameInstallInfo { self.wine_prefix.clone() } - // 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(); diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index 3ef269e0..c6db95d7 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -374,7 +374,7 @@ pub fn get_license_dir(slug: Option<&str>) -> Result { let path = format!( "{}/drive_c/{}", - wine_prefix_dir(slug).unwrap().safe_str()?, + wine_prefix_dir(slug.unwrap()).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 3c5e879b..47fa16d0 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -76,8 +76,8 @@ struct Versions { } /// Returns internal proton pfx path -pub fn wine_prefix_dir(slug: Option<&str>) -> Result { - let game_install_info = load_game_info_from_json(slug.unwrap()).unwrap(); +pub fn wine_prefix_dir(slug: &str) -> Result { + let game_install_info = load_game_info_from_json(slug).unwrap(); let prefix_path = game_install_info.wine_prefix().unwrap(); if !prefix_path.exists() {} @@ -114,7 +114,7 @@ pub fn eac_dir() -> Result { Ok(maxima_dir()?.join("wine/eac_runtime")) } -pub fn script_path() -> Result { +pub fn umu_script_path() -> Result { match env::var("MAXIMA_UMU_LOCATION") { Ok(path) => Ok(PathBuf::from(path)), Err(_) => Ok(maxima_dir()?.join("wine/umu/umu-run")), @@ -236,17 +236,16 @@ pub async fn run_wine_command, T: AsRef>( cwd: Option, want_output: bool, command_type: CommandType, - slug: Option<&str>, + proton_prefix_path: &PathBuf, ) -> Result { let proton_path = proton_dir()?; - let proton_prefix_path = wine_prefix_dir(slug).unwrap(); let eac_path = eac_dir()?; - let script_path = script_path()?; + let umu_script_path = umu_script_path()?; info!("Wine Prefix: {:?}", proton_prefix_path); // Create command with all necessary wine env variables - let mut binding = Command::new(script_path.clone()); + let mut binding = Command::new(umu_script_path.clone()); let mut child = binding .env("WINEPREFIX", proton_prefix_path) .env("GAMEID", "umu-0") @@ -333,7 +332,7 @@ fn extract_archive( Ok(()) } -pub async fn setup_wine_registry(slug: Option<&str>) -> Result<(), NativeError> { +pub async fn setup_wine_registry(slug: &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 @@ -384,7 +383,7 @@ pub async fn setup_wine_registry(slug: Option<&str>) -> Result<(), NativeError> None, false, CommandType::Run, - slug, + &wine_prefix_dir(slug).unwrap(), ) .await?; @@ -432,7 +431,7 @@ async fn parse_wine_registry(file_path: &str) -> WineRegistry { registry_map.clone() } -pub async fn parse_mx_wine_registry(slug: Option<&str>) -> Result { +pub async fn parse_mx_wine_registry(slug: &str) -> Result { let path = wine_prefix_dir(slug) .unwrap() .join("pfx") diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 1c1ee270..576043b6 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -291,9 +291,7 @@ async fn req_touchup(body: web::Bytes) -> Result { Path::new(&request.output_dir).join(maxima::core::manifest::MANIFEST_RELATIVE_PATH), ) .await?; - manifest - .run_touchup(Path::new(&request.output_dir), &request.slug) - .await?; + manifest.run_touchup(Path::new(&request.output_dir), None).await?; Ok(HttpResponse::Ok().body("Done")) } diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 0f514f32..538998aa 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -532,7 +532,7 @@ impl BridgeThread { Ok(manifest) => { let should_force_touchup = manifest.needs_touchup_on_locate(); if should_force_touchup { - match handle_touchup_request(&path, &slug).await { + match handle_touchup_request(&path, wine_prefix, &slug).await { Ok(()) => InteractThreadLocateGameResponse::Success, Err(err) => { warn!("Touchup failed during locate, treating as success: {}", err); @@ -555,9 +555,6 @@ impl BridgeThread { .send(MaximaLibResponse::LocateGameResponse(response)) .ok(); info!("finished locating"); - let game_install_info = - GameInstallInfo::new(PathBuf::from(path.clone()), wine_prefix); - game_install_info.save_to_json(&slug); ctx.request_repaint(); Ok(()) } From 210a1582bb4b1ac6a024654dc4a84b5cf925469d Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:43:15 -0500 Subject: [PATCH 50/57] Create gameinfo JSON when touchup is not needed, improve PathBuf handling surrounding touchup --- maxima-cli/src/main.rs | 2 +- maxima-lib/src/content/manager.rs | 2 +- maxima-lib/src/core/manifest/mod.rs | 24 +++++++++++------------- maxima-service/src/service/mod.rs | 4 ++-- maxima-ui/src/bridge_thread.rs | 11 +++++++++-- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 14a07646..d1b973ee 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -808,7 +808,7 @@ async fn list_games(maxima_arc: LockedMaxima) -> Result<()> { async fn locate_game(_maxima_arc: LockedMaxima, install_path: &str, slug: &str) -> Result<()> { let path: PathBuf = PathBuf::from(install_path); - handle_touchup_request(&path, None, slug).await?; + handle_touchup_request(path, None, slug).await?; info!("Installed!"); Ok(()) } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 4586f2c7..23993fe7 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -308,7 +308,7 @@ impl GameDownloader { return Ok(()); } - handle_touchup_request(&output_dir, None, &slug).await?; + handle_touchup_request(output_dir, None, &slug).await?; info!("Installation finished!"); completed_bytes.fetch_add(1, Ordering::SeqCst); diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index a7a992c5..be474616 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -37,16 +37,15 @@ 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: &Path, wine_prefix_path: Option) -> Result<(), ManifestError>; + async fn run_touchup(&self, install_path: &PathBuf, wine_prefix_path: Option) -> Result<(), ManifestError>; fn execute_path(&self, trial: bool) -> Option; fn version(&self) -> Option; fn needs_touchup_on_locate(&self) -> bool; } #[async_trait::async_trait] impl GameManifest for DiPManifest { - async fn run_touchup(&self, install_path: &Path, wine_prefix_path: Option) -> Result<(), ManifestError> { - let install_path = install_path.to_path_buf(); - self.run_touchup(&install_path, wine_prefix_path).await + async fn run_touchup(&self, install_path: &PathBuf, wine_prefix_path: Option) -> Result<(), ManifestError> { + self.run_touchup(install_path, wine_prefix_path).await } fn execute_path(&self, trial: bool) -> Option { @@ -68,9 +67,8 @@ impl GameManifest for DiPManifest { #[async_trait::async_trait] impl GameManifest for PreDiPManifest { - async fn run_touchup(&self, install_path: &Path, wine_prefix_path: Option) -> Result<(), ManifestError> { - let install_path = install_path.to_path_buf(); - self.run_touchup(&install_path, wine_prefix_path).await + async fn run_touchup(&self, install_path: &PathBuf, wine_prefix_path: Option) -> Result<(), ManifestError> { + self.run_touchup(install_path, wine_prefix_path).await } fn execute_path(&self, _: bool) -> Option { @@ -126,7 +124,7 @@ pub async fn load_manifest_from_disk( // 2 different thing for windows and unix as only windows needs elevation, #[cfg(windows)] -pub async fn handle_touchup_request(install_path: &Path, _wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { +pub async fn handle_touchup_request(install_path: PathBuf, _wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { use crate::{core::background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, gameinfo::GameInstallInfo}; let client = reqwest::Client::new(); @@ -140,18 +138,18 @@ pub async fn handle_touchup_request(install_path: &Path, _wine_prefix_path: Opti }) .send() .await?; - let game_install_info = GameInstallInfo::new(install_path.to_path_buf(), None); + let game_install_info = GameInstallInfo::new(install_path, None); game_install_info.save_to_json(slug); Ok(()) } #[cfg(unix)] -pub async fn handle_touchup_request(install_path: &Path, wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { +pub async fn handle_touchup_request(install_path: PathBuf, wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { use crate::gameinfo::GameInstallInfo; - let manifest = load_manifest_from_disk(Path::new(install_path).join(MANIFEST_RELATIVE_PATH)).await.unwrap(); - manifest.run_touchup(Path::new(install_path), wine_prefix_path.clone()).await?; - let game_install_info = GameInstallInfo::new(install_path.to_path_buf(), wine_prefix_path); + let manifest = load_manifest_from_disk(install_path.join(MANIFEST_RELATIVE_PATH)).await.unwrap(); + manifest.run_touchup(&install_path, wine_prefix_path.clone()).await?; + let game_install_info = GameInstallInfo::new(install_path, wine_prefix_path); game_install_info.save_to_json(slug); Ok(()) } diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index 576043b6..df7389b9 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -1,6 +1,6 @@ use std::ffi::OsString; use std::fs::File; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::mpsc as std_mpsc; use std::thread; use std::time::Duration; @@ -291,7 +291,7 @@ async fn req_touchup(body: web::Bytes) -> Result { Path::new(&request.output_dir).join(maxima::core::manifest::MANIFEST_RELATIVE_PATH), ) .await?; - manifest.run_touchup(Path::new(&request.output_dir), None).await?; + manifest.run_touchup(&PathBuf::from(request.output_dir), None).await?; Ok(HttpResponse::Ok().body("Done")) } diff --git a/maxima-ui/src/bridge_thread.rs b/maxima-ui/src/bridge_thread.rs index 538998aa..e639a796 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -526,13 +526,13 @@ impl BridgeThread { path.pop(); } - let path = PathBuf::from(path); + let path = PathBuf::from(path); let manifest_path = path.join(MANIFEST_RELATIVE_PATH); let response = match manifest::load_manifest_from_disk(manifest_path.clone()).await { Ok(manifest) => { let should_force_touchup = manifest.needs_touchup_on_locate(); if should_force_touchup { - match handle_touchup_request(&path, wine_prefix, &slug).await { + match handle_touchup_request(path, wine_prefix, &slug).await { Ok(()) => InteractThreadLocateGameResponse::Success, Err(err) => { warn!("Touchup failed during locate, treating as success: {}", err); @@ -540,6 +540,13 @@ impl BridgeThread { } } } else { + #[cfg(windows)] + let game_install_info = GameInstallInfo::new(path, None); + + #[cfg(unix)] + let game_install_info = GameInstallInfo::new(path, wine_prefix); + + game_install_info.save_to_json(&slug); InteractThreadLocateGameResponse::Success } } From 7e4df3ae543f3603788a12b8c2a9aab5845390d5 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:17:04 -0500 Subject: [PATCH 51/57] Refactor and cleanup LSX Server --- maxima-lib/src/core/mod.rs | 11 +- maxima-lib/src/lsx/connection.rs | 144 ++++++------------ maxima-lib/src/lsx/request/account.rs | 6 +- maxima-lib/src/lsx/request/auth.rs | 6 +- maxima-lib/src/lsx/request/challenge.rs | 8 +- maxima-lib/src/lsx/request/config.rs | 4 +- maxima-lib/src/lsx/request/core.rs | 8 +- maxima-lib/src/lsx/request/game.rs | 9 +- maxima-lib/src/lsx/request/igo.rs | 7 +- maxima-lib/src/lsx/request/license.rs | 7 +- maxima-lib/src/lsx/request/offer.rs | 7 +- maxima-lib/src/lsx/request/profile.rs | 34 ++--- .../src/lsx/request/progressive_install.rs | 6 +- maxima-lib/src/lsx/request/voip.rs | 4 +- maxima-lib/src/lsx/service.rs | 90 ++++++----- 15 files changed, 153 insertions(+), 198 deletions(-) diff --git a/maxima-lib/src/core/mod.rs b/maxima-lib/src/core/mod.rs index c4419690..8a3ac02b 100644 --- a/maxima-lib/src/core/mod.rs +++ b/maxima-lib/src/core/mod.rs @@ -429,9 +429,14 @@ impl Maxima { self.lsx_port = port; } - /// Tracks how many LSX clients are currently connected. - pub(super) fn set_lsx_connections(&mut self, connections: u16) { - self.lsx_connections = connections; + pub fn inc_connected_lsx(&mut self) { + self.lsx_connections += 1; + } + + pub fn dec_connected_lsx(&mut self) { + if self.lsx_connections > 0 { + self.lsx_connections -= 1; + } } pub fn set_player_started(&mut self) { diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index 8bba9e42..c6b05eeb 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -1,12 +1,10 @@ use derive_getters::Getters; -use log::{debug, error, warn}; +use log::{debug, error, info, warn}; use quick_xml::DeError; use rand::rand_core::Rng; use regex::Regex; use std::{ - io::{ErrorKind, Read, Write}, - path::PathBuf, - sync::{Arc, LazyLock}, + io::{ErrorKind}, path::PathBuf, sync::{LazyLock}, }; use sysinfo::{Pid, System}; use thiserror::Error; @@ -15,6 +13,7 @@ use tokio::{ io::AsyncWriteExt, sync::{MutexGuard, RwLock}, }; +use tokio::sync::mpsc::Sender; use super::{ request::{ @@ -106,47 +105,19 @@ pub enum EncryptionState { #[derive(Getters)] pub struct ConnectionState { - #[getter(skip)] maxima: LockedMaxima, + access_token: String, challenge: String, encryption: EncryptionState, pid: u32, - /// Message responses that are waiting to be sent - queued_messages: Vec, } -pub type LockedConnectionState = Arc>; - impl ConnectionState { /// Enable encryption on the packet after next pub fn enable_encryption(&mut self, encryption_key: [u8; 16]) { self.encryption = EncryptionState::Ready(encryption_key); } - pub async fn maxima(&mut self) -> MutexGuard<'_, Maxima> { - self.maxima.lock().await - } - - pub fn maxima_arc(&self) -> LockedMaxima { - self.maxima.clone() - } - - pub async fn access_token(&mut self) -> Result { - self.maxima().await.access_token().await - } - - pub fn queue_message(&mut self, message: LSX) -> Result<(), LSXConnectionError> { - let mut str = quick_xml::se::to_string(&message)?; - debug!("Queuing LSX Message: {}", str); - - if let EncryptionState::Enabled(key) = self.encryption { - str = simple_encrypt(str.as_bytes(), &key) - }; - - str += "\0"; - self.queued_messages.push(str); - Ok(()) - } } pub fn get_os_pid(context: &ActiveGameContext) -> Result { @@ -204,15 +175,16 @@ pub async fn get_wine_pid( } pub struct Connection { - maxima: LockedMaxima, stream: TcpStream, - state: LockedConnectionState, + state: ConnectionState, + tx: Sender, } impl Connection { pub async fn new( maxima_arc: LockedMaxima, mut stream: TcpStream, + tx: Sender, ) -> Result { stream.set_nodelay(true)?; @@ -269,44 +241,57 @@ impl Connection { drop(maxima); - let state = Arc::new(RwLock::new(ConnectionState { + let state = ConnectionState { maxima: maxima_arc.clone(), + access_token: maxima_arc.lock().await.access_token().await.unwrap(), challenge, encryption: EncryptionState::Disabled, pid: pid.unwrap_or(0), - queued_messages: Vec::new(), - })); + }; Ok(Self { - maxima: maxima_arc, stream, state, + tx, }) } - // State - - pub async fn maxima(&self) -> MutexGuard<'_, Maxima> { - self.maxima.lock().await - } - // Initialization - pub async fn send_challenge(&mut self) -> Result<(), LSXConnectionError> { + pub async fn queue_challenge(&mut self) -> Result<(), LSXConnectionError> { let challenge = create_lsx_message(LSXMessageType::Event(LSXEvent { sender: CORE_SENDER.to_string(), value: LSXEventType::Challenge(LSXChallenge { attr_build: CHALLENGE_BUILD.to_string(), - attr_key: self.state.read().await.challenge.to_owned(), + attr_key: self.state.challenge.to_owned(), attr_version: CHALLENGE_VERSION.to_string(), }), })); - self.state.write().await.queue_message(challenge)?; + self.queue_message(challenge).await?; + Ok(()) + } + + pub async fn write_message(&mut self, message: String) -> Result<(), LSXConnectionError> { + self.stream.write_all(message.as_bytes()).await?; + let _ = self.stream.flush().await; Ok(()) } - pub async fn listen(&mut self) -> Result<(), LSXConnectionError> { + pub async fn queue_message(&mut self, message: LSX) -> Result<(), LSXConnectionError> { + let mut str = quick_xml::se::to_string(&message)?; + debug!("Queuing LSX Message: {}", str); + + if let EncryptionState::Enabled(key) = self.state.encryption { + str = simple_encrypt(str.as_bytes(), &key) + }; + + str += "\0"; // Sent strings need to be null terminated + let _ = self.tx.send(str).await; + Ok(()) + } + + pub async fn read_incoming_messages(&mut self) -> Result<(), LSXConnectionError> { self.stream.readable().await?; let mut buffer = [0; 1024 * 8]; let n = match self.stream.try_read(&mut buffer) { @@ -316,19 +301,15 @@ impl Connection { Err(e) => return Err(e.into()), }; - let state = self.state.write().await; - let trimmed_buffer = &buffer[..(n - 1)]; - let message = if let EncryptionState::Enabled(key) = state.encryption { + let message = if let EncryptionState::Enabled(key) = self.state.encryption { simple_decrypt(trimmed_buffer, &key) } else { String::from_utf8_lossy(trimmed_buffer).trim().to_owned() }; - drop(state); - for mat in LSX_PATTERN.find_iter(message.as_str()) { - if let Err(err) = self.process_message(mat.as_str()).await { + if let Err(err) = self.process_incoming_message(mat.as_str()).await { error!("Failed to process message: {}", err); } } @@ -336,25 +317,9 @@ impl Connection { Ok(()) } - pub async fn process_queue(&mut self) -> Result<(), LSXConnectionError> { - let mut state = self.state.write().await; - for message in &state.queued_messages { - if let Err(err) = self.stream.write(message.as_bytes()).await { - error!("Failed to send LSX message: {}", err); - } - } - - if !state.queued_messages.is_empty() { - self.stream.flush().await?; - } - - state.queued_messages.clear(); - Ok(()) - } - // Message Processing - async fn process_message(&mut self, message: &str) -> Result<(), LSXConnectionError> { + async fn process_incoming_message(&mut self, message: &str) -> Result<(), LSXConnectionError> { debug!("Received LSX Message: {}", message); let mut message = message.to_string(); @@ -362,51 +327,37 @@ impl Connection { message = message.replace(r#"version="" "#, ""); let lsx_message: LSX = quick_xml::de::from_str(message.as_str())?; - let state = self.state.clone(); - let reply = match lsx_message.value { LSXMessageType::Request(msg) => { - Connection::process_request_message(&self.state, msg).await? + self.process_request_message(msg).await? } - LSXMessageType::Event(msg) => { - Connection::process_event_message(&self.state, msg).await? + LSXMessageType::Event(_) => { + None // Blank for now } LSXMessageType::Response(_) => { - warn!("Unexpected LSX Response message received, ignoring"); + warn!("Unexpected LSX Response message received, ignoring (server message type sent to server)"); None } }; if let Some(reply) = reply { - self.state - .write() - .await - .queue_message(LSX { value: reply })?; + self.queue_message(LSX { value: reply }).await?; } - let mut state = self.state.write().await; - if let EncryptionState::Ready(key) = state.encryption { - state.encryption = EncryptionState::Enabled(key); + if let EncryptionState::Ready(key) = &self.state.encryption { + self.state.encryption = EncryptionState::Enabled(*key); } Ok(()) } - async fn process_event_message( - _: &LockedConnectionState, - _: LSXEvent, - ) -> Result, LSXConnectionError> { - Ok(None) - } - async fn process_request_message( - state: &LockedConnectionState, + &mut self, message: LSXRequest, ) -> Result, LSXConnectionError> { { let (maxima_arc, pid) = { - let s = state.read().await; - (s.maxima_arc(), *s.pid()) + (self.state.maxima.clone(), *self.state.pid()) }; maxima_arc @@ -416,7 +367,8 @@ impl Connection { } let result = lsx_message_matcher!( - state.clone(), message.value, LSXRequestType; + + &mut self.state, message.value, LSXRequestType; ChallengeResponse handle_challenge_response, GetBlockList handle_get_block_list_request, diff --git a/maxima-lib/src/lsx/request/account.rs b/maxima-lib/src/lsx/request/account.rs index 8fd2992e..dcca0d46 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -1,10 +1,10 @@ +use crate::lsx::connection::ConnectionState; use crate::{ core::service_layer::{ SERVICE_REQUEST_SDKENTITLEMENTS, ServiceEntitlement, ServiceSdkEntitlementsRequestBuilder, ServiceSdkEntitlementsResult, }, lsx::{ - connection::LockedConnectionState, request::LSXRequestError, types::{ LSXEntitlement, LSXQueryEntitlements, LSXQueryEntitlementsResponse, LSXResponseType, @@ -14,10 +14,10 @@ use crate::{ }; pub async fn handle_query_entitlements_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXQueryEntitlements, ) -> Result, LSXRequestError> { - let maxima = state.write().await.maxima_arc(); + let maxima = state.maxima().clone(); let maxima = maxima.lock().await; let service_layer = maxima.service_layer(); diff --git a/maxima-lib/src/lsx/request/auth.rs b/maxima-lib/src/lsx/request/auth.rs index 0e279024..6abc0f56 100644 --- a/maxima-lib/src/lsx/request/auth.rs +++ b/maxima-lib/src/lsx/request/auth.rs @@ -3,7 +3,7 @@ use log::{error, info}; use crate::{ core::auth::{context::AuthContext, nucleus_auth_exchange}, lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXAuthCode, LSXGetAuthCode, LSXResponseType}, }, @@ -11,7 +11,7 @@ use crate::{ }; pub async fn handle_auth_code_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXGetAuthCode, ) -> Result, LSXRequestError> { let client_id = request.attr_ClientId; @@ -19,7 +19,7 @@ pub async fn handle_auth_code_request( let mut context = AuthContext::new()?; - let access_token = state.write().await.access_token().await?; + let access_token = state.access_token(); context.set_access_token(&access_token); let auth_res = nucleus_auth_exchange(&context, &client_id, "code").await; diff --git a/maxima-lib/src/lsx/request/challenge.rs b/maxima-lib/src/lsx/request/challenge.rs index a6208d63..af8b24a7 100644 --- a/maxima-lib/src/lsx/request/challenge.rs +++ b/maxima-lib/src/lsx/request/challenge.rs @@ -2,7 +2,7 @@ use log::{debug, info}; use crate::{ lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXChallengeAccepted, LSXChallengeResponse, LSXResponseType}, }, @@ -11,10 +11,10 @@ use crate::{ }; pub async fn handle_challenge_response( - state: LockedConnectionState, + state: &mut ConnectionState, message: LSXChallengeResponse, ) -> Result, LSXRequestError> { - let valid = check_challenge_response(&message.attr_response, state.read().await.challenge()); + let valid = check_challenge_response(&message.attr_response, state.challenge()); if !valid { return Err(LSXRequestError::InvalidChallengeResponse); } @@ -33,7 +33,7 @@ pub async fn handle_challenge_response( ); let encryption_key = make_lsx_key(seed); - state.write().await.enable_encryption(encryption_key); + state.enable_encryption(encryption_key); debug!( "Encryption key: {}, version: {}", diff --git a/maxima-lib/src/lsx/request/config.rs b/maxima-lib/src/lsx/request/config.rs index 9da8579b..35c622a7 100644 --- a/maxima-lib/src/lsx/request/config.rs +++ b/maxima-lib/src/lsx/request/config.rs @@ -2,7 +2,7 @@ use std::sync::LazyLock; use crate::{ lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXGetConfig, LSXGetConfigResponse, LSXResponseType, LSXService}, }, @@ -49,7 +49,7 @@ static SERVICES: LazyLock> = LazyLock::new(|| { }); pub async fn handle_config_request( - _: LockedConnectionState, + _: &mut ConnectionState, _: LSXGetConfig, ) -> Result, LSXRequestError> { let services = &SERVICES; diff --git a/maxima-lib/src/lsx/request/core.rs b/maxima-lib/src/lsx/request/core.rs index b8c3cddf..302015bb 100644 --- a/maxima-lib/src/lsx/request/core.rs +++ b/maxima-lib/src/lsx/request/core.rs @@ -1,7 +1,7 @@ use crate::{ core::settings::MaximaSetting, lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{ LSXGetInternetConnectedState, LSXGetSetting, LSXGetSettingResponse, @@ -12,7 +12,7 @@ use crate::{ }; pub async fn handle_settings_request( - _: LockedConnectionState, + _: &mut ConnectionState, request: LSXGetSetting, ) -> Result, LSXRequestError> { let setting = match request.attr_SettingId { @@ -25,7 +25,7 @@ pub async fn handle_settings_request( } pub async fn handle_connectivity_request( - _: LockedConnectionState, + _: &mut ConnectionState, _: LSXGetInternetConnectedState, ) -> Result, LSXRequestError> { // TODO Actually check this @@ -33,7 +33,7 @@ pub async fn handle_connectivity_request( } pub async fn handle_set_downloader_util_request( - _: LockedConnectionState, + _: &mut ConnectionState, _: LSXSetDownloaderUtilization, ) -> Result, LSXRequestError> { // TODO Actually set this diff --git a/maxima-lib/src/lsx/request/game.rs b/maxima-lib/src/lsx/request/game.rs index 584b0528..b7fd78c4 100644 --- a/maxima-lib/src/lsx/request/game.rs +++ b/maxima-lib/src/lsx/request/game.rs @@ -5,7 +5,7 @@ const LANGUAGES: &str = use crate::{ lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{ LSXGameInfoId, LSXGetAllGameInfo, LSXGetAllGameInfoResponse, LSXGetGameInfo, @@ -16,7 +16,7 @@ use crate::{ }; pub async fn handle_game_info_request( - _: LockedConnectionState, + _: &mut ConnectionState, request: LSXGetGameInfo, ) -> Result, LSXRequestError> { let game_info = match request.attr_GameInfoId { @@ -31,12 +31,11 @@ pub async fn handle_game_info_request( // pub async fn handle_all_game_info_request( - state: LockedConnectionState, + state: &mut ConnectionState, _request: LSXGetAllGameInfo, ) -> Result, LSXRequestError> { let current_offer = { - let mut state = state.write().await; - let maxima = state.maxima().await; + let maxima = state.maxima().lock().await; maxima .playing() diff --git a/maxima-lib/src/lsx/request/igo.rs b/maxima-lib/src/lsx/request/igo.rs index 52577b30..04e6d6b8 100644 --- a/maxima-lib/src/lsx/request/igo.rs +++ b/maxima-lib/src/lsx/request/igo.rs @@ -1,19 +1,18 @@ use log::info; use crate::lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXResponseType, LSXShowIGOWindow}, }; pub async fn handle_show_igo_window_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXShowIGOWindow, ) -> Result, LSXRequestError> { info!("Got request to show user {}", request.target_id); - let arc = state.write().await.maxima_arc(); - let maxima = arc.lock().await; + let maxima = state.maxima().lock().await; let data = maxima.player_by_id(&request.target_id.to_string()).await?; info!("{:?}", data); diff --git a/maxima-lib/src/lsx/request/license.rs b/maxima-lib/src/lsx/request/license.rs index 1f4e0991..f23c6b33 100644 --- a/maxima-lib/src/lsx/request/license.rs +++ b/maxima-lib/src/lsx/request/license.rs @@ -4,7 +4,7 @@ use std::env; use crate::{ core::{auth::hardware::HardwareInfo, launch::LaunchMode}, lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXRequestLicense, LSXRequestLicenseResponse, LSXResponseType}, }, @@ -13,7 +13,7 @@ use crate::{ }; pub async fn handle_license_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXRequestLicense, ) -> Result, LSXRequestError> { info!("Requesting OOA License and Denuvo Token"); @@ -22,8 +22,7 @@ pub async fn handle_license_request( return make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }); } - let arc = state.write().await.maxima_arc(); - let mut maxima = arc.lock().await; + let mut maxima = state.maxima().lock().await; let playing = maxima.playing().as_ref().unwrap(); let content_id = playing.content_id().to_owned(); diff --git a/maxima-lib/src/lsx/request/offer.rs b/maxima-lib/src/lsx/request/offer.rs index bbc60cbb..9b1918c6 100644 --- a/maxima-lib/src/lsx/request/offer.rs +++ b/maxima-lib/src/lsx/request/offer.rs @@ -3,7 +3,7 @@ use crate::{ SERVICE_REQUEST_ADDONSEARCH, ServiceAddonSearchRequestBuilder, ServiceAddonSearchResultRoot, }, lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXOffer, LSXQueryOffers, LSXQueryOffersResponse, LSXResponseType}, }, @@ -11,7 +11,7 @@ use crate::{ }; pub async fn handle_query_offers_request( - conn: LockedConnectionState, + conn: &mut ConnectionState, request: LSXQueryOffers, ) -> Result, LSXRequestError> { let mut rtn: Vec = Vec::new(); @@ -22,8 +22,7 @@ pub async fn handle_query_offers_request( String::new() }; - let mut conn = conn.write().await; - let maxima = conn.maxima().await; + let maxima = conn.maxima().lock().await; let offers: ServiceAddonSearchResultRoot = maxima .service_layer() .request( diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index 6462da0a..59c93d8a 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -6,7 +6,7 @@ use crate::core::service_layer::{ }; use crate::{ lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{ LSXBlockedUser, LSXErrorSuccess, LSXFriend, LSXFriendState, LSXGetBlockList, @@ -22,11 +22,10 @@ use crate::{ }; pub async fn handle_profile_request( - state: LockedConnectionState, + state: &mut ConnectionState, _: LSXGetProfile, ) -> Result, LSXRequestError> { - let arc = state.write().await.maxima_arc(); - let maxima = arc.lock().await; + let maxima = state.maxima().lock().await; let user = maxima.local_user().await?; let path = platform_path(maxima.avatar_image(user.id(), 208, 208).await?); @@ -56,7 +55,7 @@ pub async fn handle_profile_request( } pub async fn handle_presence_request( - _: LockedConnectionState, + _: &mut ConnectionState, _: LSXGetPresence, ) -> Result, LSXRequestError> { return make_lsx_handler_response!(Response, GetPresenceResponse, { @@ -74,7 +73,7 @@ pub async fn handle_presence_request( } pub async fn handle_set_presence_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXSetPresence, ) -> Result, LSXRequestError> { info!( @@ -86,8 +85,7 @@ pub async fn handle_set_presence_request( .unwrap_or(String::new()) ); - let arc = state.write().await.maxima_arc(); - let mut maxima = arc.lock().await; + let mut maxima = state.maxima().lock().await; let playing = maxima.playing().as_ref().unwrap(); if playing.mode().is_online_offline() { @@ -113,13 +111,12 @@ pub async fn handle_set_presence_request( } pub async fn handle_query_presence_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXQueryPresence, ) -> Result, LSXRequestError> { let mut friends = Vec::new(); - let mut state = state.write().await; - let mut maxima = state.maxima().await; + let mut maxima = state.maxima().lock().await; let presence_store = maxima.rtm().presence_store().lock().await; for user in request.Users { @@ -155,11 +152,10 @@ pub async fn handle_query_presence_request( } pub async fn handle_query_friends_request( - state: LockedConnectionState, + state: &mut ConnectionState, _: LSXQueryFriends, ) -> Result, LSXRequestError> { - let mut state = state.write().await; - let mut maxima = state.maxima().await; + let mut maxima = state.maxima().lock().await; let friends = maxima.friends(0).await?; let presence_store = maxima.rtm().presence_store().lock().await; @@ -218,13 +214,12 @@ pub async fn handle_query_friends_request( } pub async fn handle_get_block_list_request( - state: LockedConnectionState, + state: &mut ConnectionState, _: LSXGetBlockList, ) -> Result, LSXRequestError> { let mut list: Vec = Vec::new(); - let mut maxima = state.write().await; - let maxima = maxima.maxima().await; + let maxima = state.maxima().lock().await; let friends: ServiceFriends = maxima .service_layer() .request( @@ -254,13 +249,12 @@ pub async fn handle_get_block_list_request( } pub async fn handle_query_image_request( - state: LockedConnectionState, + state: &mut ConnectionState, request: LSXQueryImage, ) -> Result, LSXRequestError> { let parts = request.attr_ImageId.split(":").collect::>(); - let arc = state.write().await.maxima_arc(); - let maxima = arc.lock().await; + let maxima = state.maxima().lock().await; let path = platform_path( maxima diff --git a/maxima-lib/src/lsx/request/progressive_install.rs b/maxima-lib/src/lsx/request/progressive_install.rs index 2cf72a5d..b5629f6e 100644 --- a/maxima-lib/src/lsx/request/progressive_install.rs +++ b/maxima-lib/src/lsx/request/progressive_install.rs @@ -1,6 +1,6 @@ use crate::{ lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{ LSXAreChunksInstalled, LSXAreChunksInstalledResponse, @@ -12,7 +12,7 @@ use crate::{ }; pub async fn handle_pi_availability_request( - _: LockedConnectionState, + _: &mut ConnectionState, _: LSXIsProgressiveInstallationAvailable, ) -> Result, LSXRequestError> { return make_lsx_handler_response!(Response, IsProgressiveInstallationAvailableResponse, { @@ -22,7 +22,7 @@ pub async fn handle_pi_availability_request( } pub async fn handle_pi_installed_chunks_request( - _: LockedConnectionState, + _: &mut ConnectionState, request: LSXAreChunksInstalled, ) -> Result, LSXRequestError> { return make_lsx_handler_response!(Response, AreChunksInstalledResponse, { diff --git a/maxima-lib/src/lsx/request/voip.rs b/maxima-lib/src/lsx/request/voip.rs index 24ca32be..bbfd4b17 100644 --- a/maxima-lib/src/lsx/request/voip.rs +++ b/maxima-lib/src/lsx/request/voip.rs @@ -1,6 +1,6 @@ use crate::{ lsx::{ - connection::LockedConnectionState, + connection::ConnectionState, request::LSXRequestError, types::{LSXGetVoipStatus, LSXGetVoipStatusResponse, LSXResponseType}, }, @@ -8,7 +8,7 @@ use crate::{ }; pub async fn handle_voip_status_request( - _: LockedConnectionState, + _: &mut ConnectionState, _: LSXGetVoipStatus, ) -> Result, LSXRequestError> { return make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }); diff --git a/maxima-lib/src/lsx/service.rs b/maxima-lib/src/lsx/service.rs index dda1ecfb..81eb8d49 100644 --- a/maxima-lib/src/lsx/service.rs +++ b/maxima-lib/src/lsx/service.rs @@ -1,7 +1,6 @@ -use std::{io::ErrorKind, time::Duration}; - use log::{info, warn}; -use tokio::{net::TcpListener, time::sleep}; +use tokio::net::TcpListener; +use tokio::sync::mpsc::Receiver; use crate::lsx::connection::LSXConnectionError; use crate::{core::LockedMaxima, lsx::connection::Connection}; @@ -16,54 +15,63 @@ pub enum LSXServerError { } pub async fn start_server(port: u16, maxima: LockedMaxima) -> Result<(), LSXServerError> { - let addr = "127.0.0.1:".to_string() + port.to_string().as_str(); + let addr = format!("127.0.0.1:{}", port); let listener = TcpListener::bind(&addr).await?; info!("Listening on: {}", addr); - let mut connections: Vec = Vec::new(); - loop { - let mut idx = 0; - while idx < connections.len() { - if connections[idx].process_queue().await.is_err() - || connections[idx].listen().await.is_err() - { - warn!("LSX connection closed"); - connections.remove(idx); - maxima - .lock() - .await - .set_lsx_connections(connections.len() as u16); - } else { - idx += 1; + let (socket, addr) = match listener.accept().await { + Ok(s) => s, + Err(err) => return Err(LSXServerError::Io(err)), + }; + + info!("New LSX connection: {:?}", addr); + let (tx, rx) = tokio::sync::mpsc::channel(100); + let mut conn = match Connection::new(maxima.clone(), socket, tx).await { + Ok(c) => c, + Err(err) => { + warn!("Failed to establish LSX connection: {}", err); + continue; } - } + }; + conn.queue_challenge().await?; + + let mut maxima = maxima.lock().await; + maxima.inc_connected_lsx(); + maxima.set_player_started(); + drop(maxima); + + tokio::spawn(async move { + if let Err(err) = handle_client(conn, rx).await { + warn!("LSX connection error: {}", err); + } + }); + } +} + +pub async fn handle_client( + mut conn: Connection, + mut rx: Receiver, +) -> Result<(), LSXServerError> { + loop { tokio::select! { - accept_result = listener.accept() => { - match accept_result { - Ok((socket, addr)) => { - info!("New LSX connection: {:?}", addr); - match Connection::new(maxima.clone(), socket).await { - Ok(mut conn) => { - conn.send_challenge().await?; - connections.push(conn); - maxima.lock().await.set_lsx_connections(connections.len() as u16); - maxima.lock().await.set_player_started(); - } - Err(e) => warn!("Failed to establish LSX connection: {}", e), - } - } - Err(e) if e.kind() == ErrorKind::WouldBlock => { - sleep(Duration::from_millis(5)).await; + res = conn.read_incoming_messages() => { + match res { + Ok(_) => continue, + Err(err) => { + warn!("LSX connection error: {}", err); + return Err(LSXServerError::Conn(err)); } - Err(e) => return Err(LSXServerError::Io(e)), } - } - _ = sleep(Duration::from_millis(5)) => { - // YOUUUUUUUU SHALL NOT BLOCK ! - } + }, + Some(msg) = rx.recv() => { + if let Err(err) = conn.write_message(msg).await { + warn!("LSX connection error: {}", err); + return Err(LSXServerError::Conn(err)); + } + }, } } } From 0aaa4da523199dc420a543dc784877c386a17f1c Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 3 Sep 2026 18:37:41 -0500 Subject: [PATCH 52/57] Fix maxima-cli blocking, track bytes as they are being written, check PATH for umu-run on Linux --- Cargo.lock | 65 ++++++++++++++++------------ maxima-cli/src/main.rs | 7 +-- maxima-lib/Cargo.toml | 3 ++ maxima-lib/src/content/downloader.rs | 9 ++-- maxima-lib/src/unix/wine.rs | 9 ++-- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12ba70e5..0c6cd376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,12 +48,11 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.13.3" +version = "3.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11004b0e9b44b4eb3d15e0c3132b96fb178c7e50a74758b2f17bb9cc9a7fb4f6" +checksum = "86d62d1a48894ec9450bcde7ef1e3681205771ff6ea9c61cc5ae031145192787" dependencies = [ "actix-codec", - "actix-rt", "actix-service", "actix-utils", "base64 0.22.1", @@ -560,9 +559,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -570,9 +569,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -3315,9 +3314,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" dependencies = [ "bitflags 2.13.1", "libc", @@ -3423,9 +3422,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" +checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" dependencies = [ "hashbrown 0.17.1", ] @@ -3595,11 +3594,12 @@ dependencies = [ "tokio-rustls", "tokio-stream", "tokio-util", - "toml 1.1.4+spec-1.1.0", + "toml 1.1.5+spec-1.1.0", "ureq", "url", "uuid", "webpki-roots", + "which", "widestring", "windows-service", "windows-sys 0.61.2", @@ -3789,9 +3789,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -4514,9 +4514,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.4.2" +version = "5.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade3be4664bc1ef537ce133015f04c176b737815c2ba9fd60edf212d6e90dd55" +checksum = "7c603ab8300cf18bc3b14146b19fe3dfcc4843ae5a400cd0e7a30b95aa366634" dependencies = [ "is-wsl", "libc", @@ -6112,9 +6112,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" dependencies = [ "serde", ] @@ -6697,9 +6697,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "0ba2077be5cd93c7408c849e45a4ab261b59f923f4bcdee2a724b8ed5a175a77" dependencies = [ "tinyvec_macros", ] @@ -6785,9 +6785,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.4+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" dependencies = [ "indexmap", "serde_core", @@ -7149,9 +7149,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.13.2" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -7159,9 +7159,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27780e3aec98b13ac796bc0d5770205ee6e976d1ccb33a52da70b73ff0ff880d" +checksum = "0941feceafbe7a8f59ea1096d45b97002884a41306315ad797b3684b63a81d8c" dependencies = [ "erased-serde", "serde_core", @@ -7170,9 +7170,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c093d229c7ead9974b640e61170976e0eff3dac447cf6792c645f0b7f28b3de" +checksum = "839752af8179287d27eb2b94164641b1ede9e60ab7424163388dc21ebd0508cd" dependencies = [ "sval", "sval_buffer", @@ -7703,6 +7703,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + [[package]] name = "widestring" version = "1.2.1" diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index d1b973ee..21c3bd4a 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -397,10 +397,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { .map(|g| g.name()) .collect::>(); - let name = tokio::task::spawn_blocking(move || { - Select::new("What game would you like to install?", owned_games_strs).prompt() - }) - .await??; + let name = Select::new("What game would you like to install?", owned_games_strs).prompt()?; owned_games.iter().find(|g| g.name() == name).unwrap() }; @@ -408,8 +405,6 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { let offer_id = game.base_offer().offer_id().to_owned(); let slug = game.base_offer().slug().to_owned(); - let mut maxima = maxima_arc.lock().await; - let builds = maxima .content_manager() .service() diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index a0bfc9a3..4b102d94 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -104,6 +104,9 @@ is_elevated = { workspace = true } widestring = "1.2.1" wmi = "0.18.4" +[target.'cfg(unix)'.dependencies] +which = "8.0" + [target.'cfg(target_os = "macos")'.dependencies] smbios-lib = "0.9.4" diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index e248c429..30927270 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -333,13 +333,11 @@ impl ZipDownloader { } }; - let content_length = data.content_length().unwrap_or(0); // Because data gets moved with bytes_stream() - let stream = ByteCountingStream::new(data.bytes_stream()); + let stream = ByteCountingStream::new(data.bytes_stream(), progress_callback); let mut stream_reader = BufReader::new(stream.into_async_read().compat()); tokio::io::copy(&mut stream_reader, &mut writer).await?; writer.shutdown().await?; // flush decoder + BufWriter to disk - progress_callback(content_length as usize); Ok(compressed_offset) } } @@ -347,16 +345,18 @@ impl ZipDownloader { struct ByteCountingStream { inner: S, byte_count: usize, + progress_callback: ProgressCallback, } impl ByteCountingStream where S: Stream>, { - fn new(inner: S) -> Self { + fn new(inner: S, progress_callback: ProgressCallback) -> Self { Self { inner, byte_count: 0, + progress_callback, } } } @@ -377,6 +377,7 @@ where fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { match self.inner.poll_next_unpin(cx) { Poll::Ready(Some(Ok(chunk))) => { + (self.progress_callback)(chunk.len()); self.byte_count += chunk.len(); Poll::Ready(Some(Ok(chunk))) } diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 47fa16d0..9fe90d66 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -115,9 +115,12 @@ pub fn eac_dir() -> Result { } pub fn umu_script_path() -> Result { - match env::var("MAXIMA_UMU_LOCATION") { - Ok(path) => Ok(PathBuf::from(path)), - Err(_) => Ok(maxima_dir()?.join("wine/umu/umu-run")), + if let Ok(path) = env::var("MAXIMA_UMU_LOCATION") { + Ok(PathBuf::from(path)) + } else if let Ok(path) = which::which("umu-run") { + Ok(path) + } else { + Ok(maxima_dir()?.join("wine/umu/umu-run")) } } From 3beb51ab43a762a7fc67b6e4192804839c2e84a1 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:18:03 -0500 Subject: [PATCH 53/57] Remove unused dependencies --- Cargo.lock | 173 +----------------------------- maxima-bootstrap/Cargo.toml | 6 -- maxima-cli/Cargo.toml | 7 -- maxima-lib/Cargo.toml | 7 -- maxima-lib/src/content/manager.rs | 2 +- maxima-service/Cargo.toml | 9 +- maxima-tui/Cargo.toml | 5 - maxima-ui/Cargo.toml | 2 - 8 files changed, 4 insertions(+), 207 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c6cd376..2a556528 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - [[package]] name = "accesskit" version = "0.24.1" @@ -493,15 +477,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -595,12 +570,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "base62" -version = "2.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b" - [[package]] name = "base64" version = "0.22.1" @@ -694,7 +663,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1054,15 +1023,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.20", -] - [[package]] name = "codespan-reporting" version = "0.13.1" @@ -1369,7 +1329,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.7", + "generic-array", "typenum", ] @@ -1866,18 +1826,6 @@ dependencies = [ "serde", ] -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - [[package]] name = "enable-ansi-support" version = "0.3.1" @@ -2339,16 +2287,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "generic-array" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" -dependencies = [ - "rustversion", - "typenum", -] - [[package]] name = "gethostname" version = "1.1.0" @@ -2599,15 +2537,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -2639,20 +2568,6 @@ dependencies = [ "foldhash 0.2.0", ] -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", -] - [[package]] name = "heck" version = "0.5.0" @@ -3495,15 +3410,11 @@ dependencies = [ "maxima-lib", "maxima-resources", "reqwest", - "serde", "serde_json", "thiserror 2.0.20", "tokio", "url", "urlencoding", - "windows-service", - "windows-sys 0.61.2", - "winreg", ] [[package]] @@ -3511,22 +3422,15 @@ name = "maxima-cli" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.23.1", "clap", - "futures", "inquire", "is_elevated", "log", "maxima-lib", "maxima-resources", "regex", - "reqwest", "rustls", - "serde", - "serde_json", "tokio", - "windows-sys 0.61.2", - "winreg", ] [[package]] @@ -3537,7 +3441,6 @@ dependencies = [ "anyhow", "async-compression", "async-trait", - "base62", "base64 0.23.1", "bytebuffer", "bytes", @@ -3551,10 +3454,7 @@ dependencies = [ "ecb", "enable-ansi-support", "encoding_rs", - "filetime", - "flate2", "futures", - "generic-array 1.4.5", "gethostname", "glob", "globset", @@ -3565,12 +3465,10 @@ dependencies = [ "log", "mac_address", "md5", - "miniz_oxide 0.9.1", "moka", "open", "pastey 0.2.3", "pelite", - "postcard", "prost", "prost-build", "quick-xml 0.42.0", @@ -3606,7 +3504,6 @@ dependencies = [ "winreg", "wmi", "xz2", - "zstd", ] [[package]] @@ -3621,16 +3518,10 @@ name = "maxima-service" version = "0.1.0" dependencies = [ "actix-web", - "base64 0.23.1", - "derive_more", - "directories", "hex", "log", "maxima-lib", "maxima-resources", - "regex", - "reqwest", - "serde", "serde_json", "sha2 0.11.0", "structured-logger", @@ -3638,7 +3529,6 @@ dependencies = [ "tokio", "windows-service", "windows-sys 0.61.2", - "winreg", ] [[package]] @@ -3646,7 +3536,6 @@ name = "maxima-tui" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.23.1", "clap", "color-eyre", "inquire", @@ -3656,13 +3545,9 @@ dependencies = [ "maxima-resources", "ratatui", "regex", - "serde", - "serde_json", "strum", "tokio", "tokio-stream", - "windows-sys 0.61.2", - "winreg", ] [[package]] @@ -3692,8 +3577,6 @@ dependencies = [ "sys-locale", "thiserror 2.0.20", "tokio", - "windows-sys 0.61.2", - "winit", ] [[package]] @@ -3783,7 +3666,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", - "serde", "simd-adler32", ] @@ -4569,15 +4451,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - [[package]] name = "owo-colors" version = "4.4.0" @@ -4911,19 +4784,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - [[package]] name = "potential_utf" version = "0.1.6" @@ -5839,19 +5699,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit 0.19.2", - "tiny-skia", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -6217,15 +6064,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" -dependencies = [ - "lock_api", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6944,12 +6782,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - [[package]] name = "type-map" version = "0.5.1" @@ -8068,7 +7900,6 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", - "sctk-adwaita", "smithay-client-toolkit 0.19.2", "smol_str", "tracing", diff --git a/maxima-bootstrap/Cargo.toml b/maxima-bootstrap/Cargo.toml index 4ce10e21..043d5f71 100644 --- a/maxima-bootstrap/Cargo.toml +++ b/maxima-bootstrap/Cargo.toml @@ -8,7 +8,6 @@ edition = { workspace = true } [dependencies] maxima-lib = { path = "../maxima-lib" } tokio = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } reqwest = { workspace = true, features = ["query"] } base64 = { workspace = true } @@ -16,11 +15,6 @@ url = { workspace = true } urlencoding = "2.1.3" thiserror = { workspace = true } -[target.'cfg(windows)'.dependencies] -windows-sys = { workspace = true } -winreg = { workspace = true } -windows-service = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] cacao = "0.3.2" diff --git a/maxima-cli/Cargo.toml b/maxima-cli/Cargo.toml index ac549093..457e5075 100644 --- a/maxima-cli/Cargo.toml +++ b/maxima-cli/Cargo.toml @@ -8,20 +8,13 @@ edition = { workspace = true } maxima-lib = { path = "../maxima-lib" } clap = { workspace = true } tokio = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } anyhow = { workspace = true } -base64 = { workspace = true } log = { workspace = true } regex = { workspace = true } inquire = { workspace = true } -futures = { workspace = true } -reqwest = { workspace = true } rustls = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace = true } -winreg = { workspace = true } is_elevated = { workspace = true } [build-dependencies] diff --git a/maxima-lib/Cargo.toml b/maxima-lib/Cargo.toml index 4b102d94..774fb276 100644 --- a/maxima-lib/Cargo.toml +++ b/maxima-lib/Cargo.toml @@ -13,7 +13,6 @@ documentation = "https://docs.rs/maxima" rust-version = { workspace = true } [dependencies] -postcard = { version = "1.1.3", features = ["use-std"] } scopeguard = "1.2.0" tokio = { workspace = true } moka = { version = "0.12.16", features = ["sync"] } @@ -24,7 +23,6 @@ pastey = "0.2.3" hex = { workspace = true } reqwest = { workspace = true, features = ["blocking", "form", "query", "gzip", "json"] } base64 = { workspace = true } -base62 = "2.2.4" log = { workspace = true } enable-ansi-support = "0.3.1" chrono = "0.4.45" @@ -36,15 +34,12 @@ sysinfo = "0.39.6" strum_macros = { workspace = true } sha2 = "0.11.0" tar = "0.4.46" -miniz_oxide = { version = "0.9.1", features = ["serde"] } xz2 = "0.1.7" -zstd = "0.13.3" toml = "1.1.4" bytebuffer = "2.3.0" derive-getters = "0.5.0" derive_builder = "0.20.2" mac_address = "1.1.8" -filetime = "0.2.29" rand = "0.10.2" ring = "0.17.14" futures = { workspace = true } @@ -69,7 +64,6 @@ thiserror = { workspace = true } url = { workspace = true } http = "1.5.0" globset = "0.4.20" -generic-array = "1.4.5" rustls-pki-types = "1.15.1" tokio-stream = "0.1.19" sha1 = "0.11.0" @@ -77,7 +71,6 @@ libz-sys = "1.1.29" anyhow = { workspace = true } dirs = "6.0.0" async-compression = { version = "0.4.42", features = ["tokio", "deflate"] } -flate2 = { version = "1.1.10", features = ["zlib"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 23993fe7..c31f38fe 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -308,7 +308,7 @@ impl GameDownloader { return Ok(()); } - handle_touchup_request(output_dir, None, &slug).await?; + handle_touchup_request(output_dir, wine_prefix_path, &slug).await?; info!("Installation finished!"); completed_bytes.fetch_add(1, Ordering::SeqCst); diff --git a/maxima-service/Cargo.toml b/maxima-service/Cargo.toml index 50cb86f3..1100a966 100644 --- a/maxima-service/Cargo.toml +++ b/maxima-service/Cargo.toml @@ -7,17 +7,11 @@ edition = { workspace = true } [dependencies] maxima-lib = { path = "../maxima-lib" } tokio = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } -reqwest = { workspace = true } -base64 = { workspace = true } log = { workspace = true } -regex = { workspace = true } -directories = { workspace = true } structured-logger = "1.0.5" actix-web = "4.15.0" sha2 = "0.11.0" -derive_more = "2.1.1" hex = { workspace = true } thiserror = { workspace = true } @@ -27,7 +21,6 @@ windows-sys = { workspace = true, features = [ "Win32_System_Threading", "Win32_System_ProcessStatus", ] } -winreg = { workspace = true } windows-service = { workspace = true } [build-dependencies] @@ -35,4 +28,4 @@ maxima-resources = { path = "../maxima-resources" } [[bin]] name = "maxima-service" -path = "src/main.rs" \ No newline at end of file +path = "src/main.rs" diff --git a/maxima-tui/Cargo.toml b/maxima-tui/Cargo.toml index 59492b58..e27885d3 100644 --- a/maxima-tui/Cargo.toml +++ b/maxima-tui/Cargo.toml @@ -8,10 +8,7 @@ edition = { workspace = true } maxima-lib = { path = "../maxima-lib" } clap = { workspace = true } tokio = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } anyhow = { workspace = true } -base64 = { workspace = true } log = { workspace = true } regex = { workspace = true } inquire = { workspace = true } @@ -21,8 +18,6 @@ color-eyre = "0.6.5" tokio-stream = "0.1.19" [target.'cfg(windows)'.dependencies] -windows-sys = { workspace = true } -winreg = { workspace = true } is_elevated = { workspace = true } [build-dependencies] diff --git a/maxima-ui/Cargo.toml b/maxima-ui/Cargo.toml index 40565bab..ddfde8bf 100644 --- a/maxima-ui/Cargo.toml +++ b/maxima-ui/Cargo.toml @@ -6,7 +6,6 @@ edition = "2024" [dependencies] maxima-lib = { path = "../maxima-lib" } -windows-sys = { workspace = true, features = [ "Win32_System_ProcessStatus", "Win32_Networking_WinInet" ] } anyhow = { workspace = true } clap = { workspace = true } eframe = { version = "0.36.1", default-features = false, features = [ "persistence", "glow", "wayland", "x11" ] } @@ -27,7 +26,6 @@ sys-locale = "0.3.2" humansize = { version = "2.1.3", features = ["no_alloc"] } fuzzy-matcher = "0.3.7" thiserror = { workspace = true } -winit = { version = "0.30.13", features = ["default"] } [target.'cfg(windows)'.dependencies] is_elevated = { workspace = true } From 7769bd335f236da8600bf5c4f54f6dd682e39e93 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:24:49 -0500 Subject: [PATCH 54/57] Quick gameinfo cleanups --- maxima-lib/src/core/library.rs | 25 +++++++++++-------------- maxima-lib/src/gameinfo/mod.rs | 21 +++++++++------------ 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 8e8efa2e..8d3d80b8 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -78,23 +78,20 @@ impl OwnedOffer { } pub async fn is_installed(&self) -> bool { - let maxima_dir = match maxima_dir() { - Ok(dir) => dir, + + let gameinfo = match load_game_info_from_json(&self.slug) { + Ok(info) => info, Err(_) => return false, }; - - 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, + if gameinfo.path().exists() { + true + } else { + #[cfg(windows)] + return self.check_install_win_registry().await; + + #[cfg(not(windows))] + return false; } - - #[cfg(not(windows))] - game_info_path.exists() } // This is unused diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index 4bdaf431..b0bbeec7 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -6,7 +6,7 @@ use crate::util::native::maxima_dir; use thiserror::Error; #[derive(Error, Debug)] -pub enum GameVersionError { +pub enum GameInfoError { #[error(transparent)] Native(#[from] crate::util::native::NativeError), #[error(transparent)] @@ -14,7 +14,7 @@ pub enum GameVersionError { #[error(transparent)] Json(#[from] serde_json::Error), - #[error("game version info not found for `{0}`")] + #[error("game info not found for `{0}`")] NotFound(String), } @@ -67,21 +67,18 @@ impl GameInstallInfo { 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(); + let path = maxima_dir().unwrap().join("gameinfo"); + if std::fs::create_dir_all(&path).is_ok() { + let gameinfo_path = path.join(format!("{}.json", slug)); + fs::write(gameinfo_path, json).unwrap(); } } } } -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)); - let json = fs::read_to_string(path.unwrap())?; +pub fn load_game_info_from_json(slug: &str) -> Result { + let path = maxima_dir()?.join("gameinfo").join(format!("{}.json", slug)); + let json = fs::read_to_string(path)?; let game_install_info: GameInstallInfo = serde_json::from_str(&json)?; Ok(game_install_info) } From 2628c5dfe47030656a5ed4f12b5c30ff15ff87a6 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:26:30 -0500 Subject: [PATCH 55/57] Cargo clippy fix --- maxima-bootstrap/src/main.rs | 12 ++--- maxima-cli/src/main.rs | 22 ++++---- maxima-lib/src/content/downloader.rs | 9 ++-- maxima-lib/src/content/exclusion.rs | 2 +- maxima-lib/src/content/manager.rs | 33 +++++------- maxima-lib/src/content/mod.rs | 3 +- maxima-lib/src/content/zip.rs | 10 ++-- maxima-lib/src/core/auth/storage.rs | 2 - maxima-lib/src/core/background_service_win.rs | 3 +- maxima-lib/src/core/cloudsync.rs | 10 ++-- maxima-lib/src/core/library.rs | 13 +++-- maxima-lib/src/core/manifest/mod.rs | 4 +- maxima-lib/src/core/service_layer.rs | 5 +- maxima-lib/src/lsx/connection.rs | 8 +-- maxima-lib/src/lsx/request/account.rs | 2 +- maxima-lib/src/lsx/request/auth.rs | 4 +- maxima-lib/src/lsx/request/challenge.rs | 2 +- maxima-lib/src/lsx/request/config.rs | 2 +- maxima-lib/src/lsx/request/core.rs | 4 +- maxima-lib/src/lsx/request/game.rs | 6 +-- maxima-lib/src/lsx/request/license.rs | 2 +- maxima-lib/src/lsx/request/offer.rs | 2 +- maxima-lib/src/lsx/request/profile.rs | 18 +++---- .../src/lsx/request/progressive_install.rs | 8 +-- maxima-lib/src/lsx/request/voip.rs | 2 +- maxima-lib/src/ooa/mod.rs | 2 +- maxima-lib/src/rtm/client.rs | 8 +-- maxima-lib/src/util/native.rs | 3 +- maxima-lib/src/util/registry.rs | 32 +++++------- maxima-lib/src/util/service_win.rs | 4 +- maxima-tui/src/main.rs | 52 +++++-------------- maxima-tui/src/service.rs | 4 +- 32 files changed, 122 insertions(+), 171 deletions(-) diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index aa79f146..7fde6ef7 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -70,19 +70,19 @@ async fn handle_launch_args() -> Result { let result = run(&args).await; if cfg!(debug_assertions) || std::env::var("MAXIMA_DEBUG").is_ok() { - println!("Args: {:?}", &args); + println!("Args: {:?}", args); let str_result = result .as_ref() .map_err(|e| { let source = e.source(); - let error_str = if source.is_some() { + + + if source.is_some() { source.unwrap().to_string() } else { e.to_string() - }; - - error_str + } }) .err() .unwrap_or("Success".to_string()); @@ -116,7 +116,7 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { let mut binding = Command::new(args.path); let child = binding.args(args.args); - let status = child.spawn()?.wait().await?; + let _status = child.spawn()?.wait().await?; // bail!("{}", status.code().unwrap()); Ok(()) } diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 21c3bd4a..46b90c01 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -36,8 +36,6 @@ use maxima::{ clients::JUNO_PC_CLIENT_ID, cloudsync::CloudSyncLockMode, launch::{self, LaunchMode, LaunchOptions}, - library::OwnedTitle, - manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, service_layer::{ SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, @@ -48,7 +46,7 @@ use maxima::{ rtm::client::BasicPresence, util::{ log::init_logger, - native::{maxima_dir, take_foreground_focus}, + native::take_foreground_focus, registry::check_registry_validity, }, }; @@ -184,8 +182,8 @@ pub async fn login_flow(login_override: Option) -> Result let persona = &captures[1]; let password = &captures[2]; - let login_result = manual_login(persona, password).await?; - login_result + + manual_login(persona, password).await? } else { access_token.to_owned() }; @@ -417,7 +415,7 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { } let build = available_live_build.unwrap(); - info!("Installing game build {}", build.to_string()); + info!("Installing game build {}", build); let path = PathBuf::from( tokio::task::spawn_blocking(|| { @@ -526,10 +524,10 @@ async fn download_specific_file( } let build = build.unwrap(); - info!("Downloading file from game build {}", build.to_string()); + info!("Downloading file from game build {}", build); let url = content_service - .download_url(offer, Some(&build.build_id())) + .download_url(offer, Some(build.build_id())) .await?; debug!("URL: {}", url.url()); @@ -560,7 +558,7 @@ async fn download_specific_file( info!( "Downloaded file {} from game build {}", file, - build.to_string() + build ); Ok(()) } @@ -586,13 +584,13 @@ async fn generate_download_links(maxima_arc: LockedMaxima) -> Result<()> { info!("Working..."); let builds = content_service - .available_builds(&game.base_offer().offer_id()) + .available_builds(game.base_offer().offer_id()) .await?; let mut strs = String::new(); for build in builds.builds { let url = content_service - .download_url(&game.base_offer().offer_id(), Some(&build.build_id())) + .download_url(game.base_offer().offer_id(), Some(build.build_id())) .await; if url.is_err() { continue; @@ -841,7 +839,7 @@ async fn do_cloud_sync(maxima_arc: LockedMaxima, game_slug: &str, write: bool) - async fn start_game( offer_id: &str, - slug: &str, + _slug: &str, game_path_override: Option, game_args: Vec, login: Option, diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 30927270..26f5724f 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -13,7 +13,7 @@ use log::{debug, error, info}; use reqwest::{Client, StatusCode}; use strum_macros::Display; use tokio::{ - fs::{self, OpenOptions, create_dir, create_dir_all}, + fs::{OpenOptions, create_dir_all}, io::{AsyncWrite, AsyncWriteExt, BufReader}, }; use tokio_util::compat::FuturesAsyncReadCompatExt; @@ -158,12 +158,11 @@ impl AsyncWrite for RestorableDeflateDecoder { } #[cfg(windows)] - if self.should_save { - if let Poll::Ready(Ok(())) = Pin::new(&mut self.inner).poll_flush(cx) { + if self.should_save + && let Poll::Ready(Ok(())) = Pin::new(&mut self.inner).poll_flush(cx) { debug!("save interval reached, serializing state..."); self.save_state(); } - } poll_result } @@ -381,7 +380,7 @@ where self.byte_count += chunk.len(); Poll::Ready(Some(Ok(chunk))) } - Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(io::Error::other( + Poll::Ready(Some(Err(_e))) => Poll::Ready(Some(Err(io::Error::other( DownloadError::DownloadFailed(self.byte_count), )))), Poll::Ready(None) => Poll::Ready(None), diff --git a/maxima-lib/src/content/exclusion.rs b/maxima-lib/src/content/exclusion.rs index a47f6d5a..59a4197c 100644 --- a/maxima-lib/src/content/exclusion.rs +++ b/maxima-lib/src/content/exclusion.rs @@ -10,7 +10,7 @@ pub fn get_exclusion_list(slug: &str) -> GlobSet { if let Ok(dir) = maxima_dir() // Checks to make sure maxima directory exists { - let filepath = dir.join("exclude").join(&slug); // 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) { diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index c31f38fe..94252b30 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -7,7 +7,6 @@ use std::{ }, }; -use anyhow::bail; use derive_builder::Builder; use derive_getters::Getters; use futures::StreamExt; @@ -27,10 +26,9 @@ use crate::{ core::{ MaximaEvent, auth::storage::LockedAuthStorage, - manifest::{self, MANIFEST_RELATIVE_PATH, ManifestError}, + manifest::ManifestError, service_layer::ServiceLayerError, }, - gameinfo::GameInstallInfo, util::native::{NativeError, maxima_dir}, }; @@ -176,11 +174,11 @@ impl GameDownloader { debug!("URL: {}", url.url()); let downloader = ZipDownloader::new(url.url()).await?; - let exclusion_list = get_exclusion_list(&game.slug()); + let exclusion_list = get_exclusion_list(game.slug()); let mut entries = Vec::new(); 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()); Spams if a lot of files are excluded continue; } @@ -406,13 +404,12 @@ impl ContentManager { // Cancels the current download and removes the game from the queue, if present. pub async fn cancel_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { // Stop the in-flight download first so nothing keeps writing to disk. - if let Some(current) = &self.current { - if current.offer_id() == offer_id { + if let Some(current) = &self.current + && current.offer_id() == offer_id { current.cancel(); current.wait().await; // let the task settle before mutating state self.current = None; } - } if self .queue @@ -434,15 +431,14 @@ impl ContentManager { /// Pauses the active download. Partial files and saved zlib states stay /// on disk, so it can be resumed later via `install_now` with the same game. pub async fn pause_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { - if let Some(current) = &self.current { - if current.offer_id() == offer_id { + if let Some(current) = &self.current + && current.offer_id() == offer_id { current.cancel(); current.wait().await; self.current = None; self.queue.paused = true; self.queue.save().await?; } - } // If it's only queued (not downloading yet), there's nothing to pause. Ok(()) } @@ -485,8 +481,8 @@ impl ContentManager { pub(crate) async fn update(&mut self) -> Result, ContentManagerError> { let mut event = None; - if let Some(current) = &self.current { - if current.is_done() { + if let Some(current) = &self.current + && current.is_done() { let finished = self .queue .current @@ -498,19 +494,16 @@ impl ContentManager { self.queue.paused = false; // Reset pause flag when done self.queue.save().await?; } - } - if self.current.is_none() && !self.queue.paused { - if let Some(game) = self.queue.pop_next() { + if self.current.is_none() && !self.queue.paused + && let Some(game) = self.queue.pop_next() { self.install_direct(game).await?; } - } - if self.current.is_none() && self.queue.current.is_some() && !self.queue.paused { - if let Some(game) = self.queue.current.take() { + if self.current.is_none() && self.queue.current.is_some() && !self.queue.paused + && let Some(game) = self.queue.current.take() { self.install_direct(game).await?; } - } Ok(event) } diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index fac4a480..8fcd1ca2 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -5,8 +5,7 @@ use crate::core::{ auth::storage::LockedAuthStorage, cache::DynamicCache, service_layer::{ - SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, - ServiceAvailableBuildsBuilder, ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, + SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, ServiceDownloadUrlMetadata, ServiceDownloadUrlRequestBuilder, ServiceLayerClient, ServiceLayerError, }, diff --git a/maxima-lib/src/content/zip.rs b/maxima-lib/src/content/zip.rs index d0b04459..c0797e8d 100644 --- a/maxima-lib/src/content/zip.rs +++ b/maxima-lib/src/content/zip.rs @@ -248,7 +248,7 @@ impl EndOfCentralDirectory { } let signature = data.read_u32()?; - if signature as u32 != ZIP_EOCD_SIGNATURE { + if signature != ZIP_EOCD_SIGNATURE { return Err(EOCDError::Signature(signature)); } @@ -272,7 +272,7 @@ impl EndOfCentralDirectory { } let signature = data.read_u32()?; - if signature as u32 != ZIP64_EOCD_SIGNATURE { + if signature != ZIP64_EOCD_SIGNATURE { return Err(ZipError::Signature(signature)); } @@ -284,12 +284,12 @@ impl EndOfCentralDirectory { data.read_u16()?; data.read_u16()?; - self.disk_number = data.read_u32()? as u32; - self.disk_number_with_cd = data.read_u32()? as u32; + self.disk_number = data.read_u32()?; + self.disk_number_with_cd = data.read_u32()?; self.disk_entries = data.read_i64()? as u64; self.total_entries = data.read_i64()? as u64; self.cd_size = data.read_i64()? as u64; - self.cd_offset = data.read_i64()? as i64; + self.cd_offset = data.read_i64()?; self.comment_length = 0; Ok(()) diff --git a/maxima-lib/src/core/auth/storage.rs b/maxima-lib/src/core/auth/storage.rs index 9416351e..c8c5bcfa 100644 --- a/maxima-lib/src/core/auth/storage.rs +++ b/maxima-lib/src/core/auth/storage.rs @@ -2,11 +2,9 @@ use super::{ TokenRefreshError, TokenResponse, nucleus_connect_token_refresh, token_info::NucleusTokenInfo, }; use crate::core::auth::hardware::HardwareHashError; -use crate::ooa::LicenseError; use crate::util::native::{NativeError, maxima_dir}; use log::info; use reqwest::Client; -use reqwest::header::ToStrError; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, diff --git a/maxima-lib/src/core/background_service_win.rs b/maxima-lib/src/core/background_service_win.rs index 891239a4..8f85fb5b 100644 --- a/maxima-lib/src/core/background_service_win.rs +++ b/maxima-lib/src/core/background_service_win.rs @@ -4,8 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::core::error::BackgroundServiceClientError; use crate::util::dll_injector::DllInjector; -use crate::util::native::NativeError; -use crate::util::registry::{RegistryError, set_up_registry}; +use crate::util::registry::set_up_registry; use is_elevated::is_elevated; pub const BACKGROUND_SERVICE_PORT: u16 = 13021; diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 76bcad92..0f156478 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -14,7 +14,7 @@ /// - 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, + auth::storage::LockedAuthStorage, endpoints::API_CLOUDSYNC, library::OwnedOffer, }; use crate::util::native::{NativeError, SafeParent, SafeStr}; @@ -286,7 +286,7 @@ impl<'a> CloudSyncLock<'a> { }; if !should_download { - debug!("Skipping CloudSync read {}", &path.display()); + debug!("Skipping CloudSync read {}", path.display()); continue; } @@ -409,12 +409,12 @@ impl<'a> CloudSyncLock<'a> { let md5 = calc_file_md5(file.try_clone().await?, HashMode::Hex).await?; let base62 = calc_file_md5(file.try_clone().await?, HashMode::Base62).await?; if let Some(existing) = self.manifest.file_by_md5(&md5) { - debug!("Skipping CloudSync write {}", &path.display()); + debug!("Skipping CloudSync write {}", path.display()); skipped.push(existing.clone()); continue; } - let name = unsubstitute_paths(&path, Some(&self.slug))?; + let name = unsubstitute_paths(path, Some(&self.slug))?; let write_data = WriteData::File { name, file, @@ -603,7 +603,7 @@ impl CloudSyncClient { mode: CloudSyncLockMode, allowed_files: Vec, slug: &str, - ) -> Result { + ) -> Result, CloudSyncError> { let (token, user_id) = acquire_auth(&self.auth).await?; let res = self diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 8d3d80b8..4f7b07fe 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -14,10 +14,10 @@ use super::{ use crate::util::registry::{RegistryError, parse_registry_path_json}; use crate::{ gameinfo::load_game_info_from_json, - util::native::{NativeError, SafeStr, maxima_dir}, + util::native::{NativeError, SafeStr}, }; use derive_getters::Getters; -use log::{debug, info}; +use log::info; use std::{collections::HashMap, path::PathBuf, time::SystemTimeError}; use thiserror::Error; @@ -97,7 +97,7 @@ impl OwnedOffer { // This is unused pub async fn install_check_path(&self) -> Result { Ok(parse_registry_path_json( - &self + self .offer .install_check_override() .as_ref() @@ -232,11 +232,10 @@ fn group_offers(products: Vec) -> Vec { // Promote any product_map entry that has no corresponding base_products entry for (slug, products) in &product_map { - if !base_products.contains_key(slug) { - if let Some(first) = products.first() { + if !base_products.contains_key(slug) + && let Some(first) = products.first() { base_products.insert(slug.clone(), first.clone()); } - } } let mut titles = Vec::new(); @@ -451,7 +450,7 @@ impl GameLibrary { fn library_request( locale: &Locale, r#type: ServiceGameProductType, - entitlement_enabled: bool, + _entitlement_enabled: bool, page: u32, ) -> Result { Ok(ServiceGetPreloadedOwnedGamesRequestBuilder::default() diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index be474616..a73b69ea 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -4,7 +4,7 @@ pub mod pre_dip; use dip::DiPManifest; use pre_dip::PreDiPManifest; use quick_xml::DeError; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug)] @@ -91,7 +91,7 @@ fn bytes_to_string(bytes: Vec) -> Option { } let u16_bytes: Vec = bytes - .chunks_exact(2) + .as_chunks::<2>().0.iter() .map(|a| u16::from_ne_bytes([a[0], a[1]])) .collect(); diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index ce8b0f21..d536e3ee 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -227,11 +227,10 @@ impl ServiceLayerClient { *last = Some(now + wait.unwrap_or(Duration::ZERO)); wait }; - if let Some(duration) = wait { - if duration > Duration::ZERO { + if let Some(duration) = wait + && duration > Duration::ZERO { sleep(duration).await; } - } let mut last_error = None; diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index c6b05eeb..c13329ac 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -1,5 +1,5 @@ use derive_getters::Getters; -use log::{debug, error, info, warn}; +use log::{debug, error, warn}; use quick_xml::DeError; use rand::rand_core::Rng; use regex::Regex; @@ -11,7 +11,7 @@ use thiserror::Error; use tokio::net::TcpStream; use tokio::{ io::AsyncWriteExt, - sync::{MutexGuard, RwLock}, + sync::MutexGuard, }; use tokio::sync::mpsc::Sender; @@ -44,7 +44,7 @@ use super::{ }; use crate::{ core::{ - LockedMaxima, Maxima, MaximaEvent, auth::storage::TokenError, launch::ActiveGameContext, + LockedMaxima, Maxima, MaximaEvent, launch::ActiveGameContext, }, lsx::{request::LSXRequestError, types::LSXRequestType}, util::{ @@ -217,7 +217,7 @@ impl Connection { .to_owned(); pid = - get_wine_pid(&context.launch_id(), &filename, context.slug().as_deref()).await; + 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/account.rs b/maxima-lib/src/lsx/request/account.rs index dcca0d46..c353a579 100644 --- a/maxima-lib/src/lsx/request/account.rs +++ b/maxima-lib/src/lsx/request/account.rs @@ -62,5 +62,5 @@ pub async fn handle_query_entitlements_request( }); } - return make_lsx_handler_response!(Response, QueryEntitlementsResponse, { entitlement: lsx_entitlements }); + make_lsx_handler_response!(Response, QueryEntitlementsResponse, { entitlement: lsx_entitlements }) } diff --git a/maxima-lib/src/lsx/request/auth.rs b/maxima-lib/src/lsx/request/auth.rs index 6abc0f56..05952344 100644 --- a/maxima-lib/src/lsx/request/auth.rs +++ b/maxima-lib/src/lsx/request/auth.rs @@ -20,7 +20,7 @@ pub async fn handle_auth_code_request( let mut context = AuthContext::new()?; let access_token = state.access_token(); - context.set_access_token(&access_token); + context.set_access_token(access_token); let auth_res = nucleus_auth_exchange(&context, &client_id, "code").await; let auth_code = match auth_res { @@ -35,5 +35,5 @@ pub async fn handle_auth_code_request( } }; - return make_lsx_handler_response!(Response, AuthCode, { attr_value: auth_code }); + make_lsx_handler_response!(Response, AuthCode, { attr_value: auth_code }) } diff --git a/maxima-lib/src/lsx/request/challenge.rs b/maxima-lib/src/lsx/request/challenge.rs index af8b24a7..f6dcc1c5 100644 --- a/maxima-lib/src/lsx/request/challenge.rs +++ b/maxima-lib/src/lsx/request/challenge.rs @@ -40,5 +40,5 @@ pub async fn handle_challenge_response( hex::encode(encryption_key), message.attr_version ); - return make_lsx_handler_response!(Response, ChallengeAccepted, { attr_response: accept_key }); + make_lsx_handler_response!(Response, ChallengeAccepted, { attr_response: accept_key }) } diff --git a/maxima-lib/src/lsx/request/config.rs b/maxima-lib/src/lsx/request/config.rs index 35c622a7..ba7da967 100644 --- a/maxima-lib/src/lsx/request/config.rs +++ b/maxima-lib/src/lsx/request/config.rs @@ -54,5 +54,5 @@ pub async fn handle_config_request( ) -> Result, LSXRequestError> { let services = &SERVICES; - return make_lsx_handler_response!(Response, GetConfigResponse, { service: services.to_vec() }); + make_lsx_handler_response!(Response, GetConfigResponse, { service: services.to_vec() }) } diff --git a/maxima-lib/src/lsx/request/core.rs b/maxima-lib/src/lsx/request/core.rs index 302015bb..30e85ed1 100644 --- a/maxima-lib/src/lsx/request/core.rs +++ b/maxima-lib/src/lsx/request/core.rs @@ -21,7 +21,7 @@ pub async fn handle_settings_request( MaximaSetting::Environment => "production".to_string(), }; - return make_lsx_handler_response!(Response, GetSettingResponse, { attr_Setting: setting }); + make_lsx_handler_response!(Response, GetSettingResponse, { attr_Setting: setting }) } pub async fn handle_connectivity_request( @@ -29,7 +29,7 @@ pub async fn handle_connectivity_request( _: LSXGetInternetConnectedState, ) -> Result, LSXRequestError> { // TODO Actually check this - return make_lsx_handler_response!(Response, InternetConnectedState, { attr_connected: 1 }); + make_lsx_handler_response!(Response, InternetConnectedState, { attr_connected: 1 }) } pub async fn handle_set_downloader_util_request( diff --git a/maxima-lib/src/lsx/request/game.rs b/maxima-lib/src/lsx/request/game.rs index b7fd78c4..5d976b44 100644 --- a/maxima-lib/src/lsx/request/game.rs +++ b/maxima-lib/src/lsx/request/game.rs @@ -25,7 +25,7 @@ pub async fn handle_game_info_request( LSXGameInfoId::InstalledLanguage => "en_US".to_string(), }; - return make_lsx_handler_response!(Response, GetGameInfoResponse, { attr_GameInfo: game_info }); + make_lsx_handler_response!(Response, GetGameInfoResponse, { attr_GameInfo: game_info }) } // @@ -80,7 +80,7 @@ pub async fn handle_all_game_info_request( ), }; - return make_lsx_handler_response!(Response, GetAllGameInfoResponse, { + make_lsx_handler_response!(Response, GetAllGameInfoResponse, { attr_FullGamePurchased: true, attr_FullGameReleased: true, attr_InstalledVersion: installed_version, @@ -96,5 +96,5 @@ pub async fn handle_all_game_info_request( attr_InstalledLanguage: "en_US".to_string(), attr_FullGameReleaseDate: full_game_release_date, attr_SystemTime: "2023-06-22T04:00:00".to_string() - }); + }) } diff --git a/maxima-lib/src/lsx/request/license.rs b/maxima-lib/src/lsx/request/license.rs index f23c6b33..0cabf103 100644 --- a/maxima-lib/src/lsx/request/license.rs +++ b/maxima-lib/src/lsx/request/license.rs @@ -60,5 +60,5 @@ pub async fn handle_license_request( debug!("Got Denuvo Token: {}", token); - return make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }); + make_lsx_handler_response!(Response, RequestLicenseResponse, { attr_License: token.to_owned() }) } diff --git a/maxima-lib/src/lsx/request/offer.rs b/maxima-lib/src/lsx/request/offer.rs index 9b1918c6..1c3f8d9c 100644 --- a/maxima-lib/src/lsx/request/offer.rs +++ b/maxima-lib/src/lsx/request/offer.rs @@ -63,5 +63,5 @@ pub async fn handle_query_offers_request( }) } - return make_lsx_handler_response!(Response, QueryOffersResponse, { offer: rtn }); + make_lsx_handler_response!(Response, QueryOffersResponse, { offer: rtn }) } diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index 59c93d8a..bf514d5c 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -35,7 +35,7 @@ pub async fn handle_profile_request( .as_ref() .ok_or(ServiceLayerError::MissingField)?; let name = player.unique_name(); - debug!("Got profile for {} {:?}", &name, path); + debug!("Got profile for {} {:?}", name, path); return make_lsx_handler_response!(Response, GetProfileResponse, { attr_Persona: name.to_owned(), @@ -58,7 +58,7 @@ pub async fn handle_presence_request( _: &mut ConnectionState, _: LSXGetPresence, ) -> Result, LSXRequestError> { - return make_lsx_handler_response!(Response, GetPresenceResponse, { + make_lsx_handler_response!(Response, GetPresenceResponse, { attr_UserId: 1005663144213, attr_Presence: LSXPresence::Ingame, attr_Title: None, @@ -69,7 +69,7 @@ pub async fn handle_presence_request( attr_SessionId: None, attr_Group: None, attr_GroupId: None, - }); + }) } pub async fn handle_set_presence_request( @@ -82,7 +82,7 @@ pub async fn handle_set_presence_request( request .attr_RichPresence .to_owned() - .unwrap_or(String::new()) + .unwrap_or_default() ); let mut maxima = state.maxima().lock().await; @@ -107,7 +107,7 @@ pub async fn handle_set_presence_request( .await?; } - return make_lsx_handler_response!(Response, ErrorSuccess, { attr_Code: 0, attr_Description: String::new() }); + make_lsx_handler_response!(Response, ErrorSuccess, { attr_Code: 0, attr_Description: String::new() }) } pub async fn handle_query_presence_request( @@ -148,7 +148,7 @@ pub async fn handle_query_presence_request( }); } - return make_lsx_handler_response!(Response, QueryPresenceResponse, { friend: friends }); + make_lsx_handler_response!(Response, QueryPresenceResponse, { friend: friends }) } pub async fn handle_query_friends_request( @@ -210,7 +210,7 @@ pub async fn handle_query_friends_request( }); } - return make_lsx_handler_response!(Response, QueryFriendsResponse, { friend: lsx_friends }); + make_lsx_handler_response!(Response, QueryFriendsResponse, { friend: lsx_friends }) } pub async fn handle_get_block_list_request( @@ -245,7 +245,7 @@ pub async fn handle_get_block_list_request( }); } - return make_lsx_handler_response!(Response, GetBlockListResponse, { attr_Return: "Success".to_string(), User: list}); + make_lsx_handler_response!(Response, GetBlockListResponse, { attr_Return: "Success".to_string(), User: list}) } pub async fn handle_query_image_request( @@ -269,5 +269,5 @@ pub async fn handle_query_image_request( attr_ResourcePath: path.safe_str()?.to_string(), }]; - return make_lsx_handler_response!(Response, QueryImageResponse, { attr_Result: 1, image: images, }); + make_lsx_handler_response!(Response, QueryImageResponse, { attr_Result: 1, image: images, }) } diff --git a/maxima-lib/src/lsx/request/progressive_install.rs b/maxima-lib/src/lsx/request/progressive_install.rs index b5629f6e..364aebd7 100644 --- a/maxima-lib/src/lsx/request/progressive_install.rs +++ b/maxima-lib/src/lsx/request/progressive_install.rs @@ -15,19 +15,19 @@ pub async fn handle_pi_availability_request( _: &mut ConnectionState, _: LSXIsProgressiveInstallationAvailable, ) -> Result, LSXRequestError> { - return make_lsx_handler_response!(Response, IsProgressiveInstallationAvailableResponse, { + make_lsx_handler_response!(Response, IsProgressiveInstallationAvailableResponse, { attr_Available: false, attr_ItemId: "Origin.OFR.50.0001456".to_string(), - }); + }) } pub async fn handle_pi_installed_chunks_request( _: &mut ConnectionState, request: LSXAreChunksInstalled, ) -> Result, LSXRequestError> { - return make_lsx_handler_response!(Response, AreChunksInstalledResponse, { + make_lsx_handler_response!(Response, AreChunksInstalledResponse, { attr_ItemId: "Origin.OFR.50.0001456".to_string(), attr_Installed: true, chunk_ids: request.chunk_ids, - }); + }) } diff --git a/maxima-lib/src/lsx/request/voip.rs b/maxima-lib/src/lsx/request/voip.rs index bbfd4b17..534d8288 100644 --- a/maxima-lib/src/lsx/request/voip.rs +++ b/maxima-lib/src/lsx/request/voip.rs @@ -11,5 +11,5 @@ pub async fn handle_voip_status_request( _: &mut ConnectionState, _: LSXGetVoipStatus, ) -> Result, LSXRequestError> { - return make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }); + make_lsx_handler_response!(Response, GetVoipStatusResponse, { attr_Available: false, attr_Active: false }) } diff --git a/maxima-lib/src/ooa/mod.rs b/maxima-lib/src/ooa/mod.rs index c6db95d7..084238b4 100644 --- a/maxima-lib/src/ooa/mod.rs +++ b/maxima-lib/src/ooa/mod.rs @@ -363,7 +363,7 @@ pub async fn save_licenses( #[cfg(windows)] pub fn get_license_dir(_slug: Option<&str>) -> Result { - let path = format!("C:/{}", LICENSE_PATH.to_string()); + let path = format!("C:/{}", LICENSE_PATH); create_dir_all(&path)?; Ok(PathBuf::from(path)) } diff --git a/maxima-lib/src/rtm/client.rs b/maxima-lib/src/rtm/client.rs index 27fe7cd4..d56134c7 100644 --- a/maxima-lib/src/rtm/client.rs +++ b/maxima-lib/src/rtm/client.rs @@ -1,9 +1,9 @@ -use std::{io, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; use core::future::Future; use derive_builder::Builder; use derive_getters::Getters; -use log::{debug, error, info, warn}; +use log::{debug, error, info}; use moka::sync::Cache; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, mpsc}; @@ -18,7 +18,7 @@ use super::{ }, }; use crate::{ - core::auth::storage::{AuthError, LockedAuthStorage, TokenError}, + core::auth::storage::{AuthError, LockedAuthStorage}, rtm::proto::{LoginRequestV3, PlatformV1, PresenceSubscribeV1, PresenceV1, UserType}, }; @@ -291,7 +291,7 @@ impl RtmClient { /// Subscribe to a list of user IDs' presences pub async fn subscribe( &mut self, - persona: Vec, + _persona: Vec, players: &[String], ) -> Result<(), RtmError> { send_and_forget_rtm_request!(self.conn_man, PresenceSubscribe, PresenceSubscribeV1, { diff --git a/maxima-lib/src/util/native.rs b/maxima-lib/src/util/native.rs index 60795772..4267aeb7 100644 --- a/maxima-lib/src/util/native.rs +++ b/maxima-lib/src/util/native.rs @@ -1,5 +1,4 @@ use std::{ - env, fs::create_dir_all, num::ParseIntError, path::{Path, PathBuf}, @@ -239,6 +238,8 @@ pub fn maxima_dir() -> Result { #[cfg(unix)] pub fn maxima_dir() -> Result { + use std::env; + let home = if let Ok(home) = env::var("XDG_DATA_HOME") { home } else if let Ok(home) = env::var("HOME") { diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index b8991a28..6b2367e3 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -1,4 +1,3 @@ -use log::info; use std::path::PathBuf; use thiserror::Error; @@ -147,11 +146,10 @@ unsafe fn open_and_query_reg( fn inject_wow6432node(sub_key: &str) -> Option { // Only rewrite paths under SOFTWARE\ that aren't already redirected let prefix = "SOFTWARE\\"; - if let Some(rest) = sub_key.strip_prefix(prefix) { - if !rest.starts_with("WOW6432Node\\") { + if let Some(rest) = sub_key.strip_prefix(prefix) + && !rest.starts_with("WOW6432Node\\") { return Some(format!("SOFTWARE\\WOW6432Node\\{}", rest)); } - } None } @@ -224,10 +222,10 @@ pub async fn parse_registry_path_json( #[cfg(windows)] pub async fn parse_registry_path_regkey(key: &str) -> Result { let mut parts = key - .split(|c| c == '[' || c == ']') + .split(['[', ']']) .filter(|s| !s.is_empty()); - let path = if let (Some(first), Some(second)) = (parts.next(), parts.next()) { + 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 Err(RegistryError::InvalidInstallKey), @@ -236,14 +234,10 @@ pub async fn parse_registry_path_regkey(key: &str) -> Result Result Result { let mut parts = key - .split(|c: char| c == '[' || c == ']') + .split(['[', ']']) .filter(|s| !s.is_empty()); - let path = if let (Some(first), Some(_second)) = (parts.next(), parts.next()) { + if let (Some(first), Some(_second)) = (parts.next(), parts.next()) { let path = match read_reg_key(first, None).await? { Some(path) => path.replace("\\", "/"), None => return Err(RegistryError::InvalidInstallKey), }; - return Ok(PathBuf::from(path.to_owned())); + Ok(PathBuf::from(path.to_owned())) } else { - return Err(RegistryError::InvalidInstallKey); - }; - - Ok(path) + Err(RegistryError::InvalidInstallKey) + } } #[cfg(windows)] diff --git a/maxima-lib/src/util/service_win.rs b/maxima-lib/src/util/service_win.rs index 7d1c2c6b..17594b8a 100644 --- a/maxima-lib/src/util/service_win.rs +++ b/maxima-lib/src/util/service_win.rs @@ -144,7 +144,7 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro let result = unsafe { ConvertSecurityDescriptorToStringSecurityDescriptorW( security_descriptor, - SDDL_REVISION_1.into(), + SDDL_REVISION_1, DACL_SECURITY_INFORMATION, &mut sddl_string, &mut sddl_string_len, @@ -171,7 +171,7 @@ pub unsafe fn init_service_security() -> Result<(), BackgroundServiceControlErro let result = unsafe { ConvertStringSecurityDescriptorToSecurityDescriptorW( U16CString::from_str(amended_sddl.as_str())?.as_ptr(), - SDDL_REVISION_1.into(), + SDDL_REVISION_1, &mut amended_security_descriptor, &mut amended_security_descriptor_len, ) diff --git a/maxima-tui/src/main.rs b/maxima-tui/src/main.rs index 1b119d9c..756fe180 100644 --- a/maxima-tui/src/main.rs +++ b/maxima-tui/src/main.rs @@ -1,15 +1,13 @@ -use clap::{Parser, Subcommand}; -use inquire::Select; -use log::{debug, error, info, warn}; +use log::{error, info, warn}; use regex::Regex; use service::{BridgeThread, MaximaLibRequest, MaximaLibResponse}; use tokio_stream::StreamExt; use std::{ io::stdout, - sync::{Arc, LazyLock}, - time::{Duration, Instant}, + sync::LazyLock, + time::Duration, }; #[cfg(windows)] @@ -22,41 +20,21 @@ use maxima::{ }; use maxima::{ - content::ContentService, core::{ - Maxima, MaximaEvent, - auth::{ - context::AuthContext, - login::{begin_oauth_login_flow, manual_login}, - nucleus_auth_exchange, - }, + MaximaEvent, launch::{self, LaunchOptions}, - service_layer::ServiceUserGameProduct, }, - util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, + util::{native::take_foreground_focus, registry::check_registry_validity}, }; -use maxima::{ - content::downloader::ZipDownloader, - core::{ - LockedMaxima, MaximaOptionsBuilder, - auth::{TokenResponse, nucleus_token_exchange}, - clients::JUNO_PC_CLIENT_ID, +use maxima::core::{ + LockedMaxima, launch::LaunchMode, - library::OwnedTitle, - service_layer::{ - SERVICE_REQUEST_GETBASICPLAYER, SERVICE_REQUEST_GETLEGACYCATALOGDEFS, - ServiceGetBasicPlayerRequestBuilder, ServiceGetLegacyCatalogDefsRequestBuilder, - ServiceLegacyOffer, ServicePlayer, - }, - }, - ooa, - rtm::client::BasicPresence, -}; + }; static MANUAL_LOGIN_PATTERN: LazyLock = LazyLock::new(|| Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid")); -use anyhow::{Result, bail}; +use anyhow::Result; use color_eyre::config::HookBuilder; use ratatui::{ crossterm::{ @@ -136,8 +114,8 @@ impl App { return Ok(()); } - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { + if let Event::Key(key) = event::read()? + && key.kind == KeyEventKind::Press { use KeyCode::*; match key.code { Char('l') | Right => self.next_tab(), @@ -146,7 +124,6 @@ impl App { _ => {} } } - } Ok(()) } @@ -347,7 +324,7 @@ async fn main() { if let Some(e) = result.err() { match std::env::var("RUST_BACKTRACE") { - Ok(_) => error!("{}:\n{}", e, e.backtrace().to_string()), + Ok(_) => error!("{}:\n{}", e, e.backtrace()), Err(_) => error!("{}", e), } } @@ -479,10 +456,7 @@ async fn start_game( let mut maxima = maxima_arc.lock().await; for event in maxima.consume_pending_events() { - match event { - MaximaEvent::ReceivedLSXRequest(_pid, _request) => (), - _ => {} - } + if let MaximaEvent::ReceivedLSXRequest(_pid, _request) = event { () } } maxima.update().await; diff --git a/maxima-tui/src/service.rs b/maxima-tui/src/service.rs index 0a0318d8..bacf8780 100644 --- a/maxima-tui/src/service.rs +++ b/maxima-tui/src/service.rs @@ -103,7 +103,7 @@ impl BridgeThread { continue; }; - let result: Result<()> = match request { + let _result: Result<()> = match request { MaximaLibRequest::LoginRequest => { let login = async || { let maxima = maxima_arc.lock().await; @@ -172,7 +172,7 @@ pub async fn login_flow() -> Result { let token_res = nucleus_token_exchange(&auth_context).await; if token_res.is_err() { - bail!("Login failed: {}", token_res.err().unwrap().to_string()); + bail!("Login failed: {}", token_res.err().unwrap()); } Ok(token_res?) From 8e3803183039f969f34dca34facc69b1b63edb3b Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Wed, 9 Sep 2026 02:04:31 -0500 Subject: [PATCH 56/57] cargo fmt and cargo fix --- maxima-bootstrap/src/main.rs | 4 +- maxima-cli/src/main.rs | 16 +--- maxima-lib/src/content/downloader.rs | 20 ++--- maxima-lib/src/content/manager.rs | 79 ++++++++++--------- maxima-lib/src/content/mod.rs | 6 +- maxima-lib/src/core/auth/hardware.rs | 5 +- maxima-lib/src/core/background_service_nix.rs | 3 +- maxima-lib/src/core/cloudsync.rs | 5 +- maxima-lib/src/core/launch.rs | 6 +- maxima-lib/src/core/library.rs | 15 ++-- maxima-lib/src/core/manifest/dip.rs | 10 ++- maxima-lib/src/core/manifest/mod.rs | 47 ++++++++--- maxima-lib/src/core/manifest/pre_dip.rs | 10 ++- maxima-lib/src/core/service_layer.rs | 7 +- maxima-lib/src/gameinfo/mod.rs | 4 +- maxima-lib/src/lsx/connection.rs | 37 +++------ maxima-lib/src/lsx/request/profile.rs | 5 +- maxima-lib/src/unix/wine.rs | 10 +-- maxima-lib/src/util/registry.rs | 15 ++-- maxima-service/src/service/mod.rs | 4 +- maxima-tui/src/main.rs | 33 ++++---- maxima-ui/src/bridge/get_games.rs | 2 +- maxima-ui/src/bridge/start_game.rs | 4 +- maxima-ui/src/bridge_processor.rs | 2 +- maxima-ui/src/bridge_thread.rs | 4 +- 25 files changed, 179 insertions(+), 174 deletions(-) diff --git a/maxima-bootstrap/src/main.rs b/maxima-bootstrap/src/main.rs index 7fde6ef7..abe05c67 100644 --- a/maxima-bootstrap/src/main.rs +++ b/maxima-bootstrap/src/main.rs @@ -76,7 +76,6 @@ async fn handle_launch_args() -> Result { .as_ref() .map_err(|e| { let source = e.source(); - if source.is_some() { source.unwrap().to_string() @@ -125,8 +124,7 @@ async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { async fn platform_launch(args: BootstrapLaunchArgs) -> Result<(), NativeError> { use maxima::unix::wine::CommandType; use maxima::unix::wine::run_wine_command; -use maxima::unix::wine::wine_prefix_dir; - + use maxima::unix::wine::wine_prefix_dir; run_wine_command( args.path.into(), diff --git a/maxima-cli/src/main.rs b/maxima-cli/src/main.rs index 46b90c01..140fcb5b 100644 --- a/maxima-cli/src/main.rs +++ b/maxima-cli/src/main.rs @@ -44,11 +44,7 @@ use maxima::{ }, ooa, rtm::client::BasicPresence, - util::{ - log::init_logger, - native::take_foreground_focus, - registry::check_registry_validity, - }, + util::{log::init_logger, native::take_foreground_focus, registry::check_registry_validity}, }; static MANUAL_LOGIN_PATTERN: LazyLock = @@ -182,7 +178,6 @@ pub async fn login_flow(login_override: Option) -> Result let persona = &captures[1]; let password = &captures[2]; - manual_login(persona, password).await? } else { access_token.to_owned() @@ -395,7 +390,8 @@ async fn interactive_install_game(maxima_arc: LockedMaxima) -> Result<()> { .map(|g| g.name()) .collect::>(); - let name = Select::new("What game would you like to install?", owned_games_strs).prompt()?; + let name = + Select::new("What game would you like to install?", owned_games_strs).prompt()?; owned_games.iter().find(|g| g.name() == name).unwrap() }; @@ -555,11 +551,7 @@ async fn download_specific_file( .download_single_file(ele, &output_dir, on_progress) .await?; - info!( - "Downloaded file {} from game build {}", - file, - build - ); + info!("Downloaded file {} from game build {}", file, build); Ok(()) } diff --git a/maxima-lib/src/content/downloader.rs b/maxima-lib/src/content/downloader.rs index 26f5724f..eb0c5386 100644 --- a/maxima-lib/src/content/downloader.rs +++ b/maxima-lib/src/content/downloader.rs @@ -1,3 +1,4 @@ +use crate::content::zlib::restore_zlib_state; use std::{ io, path::{Path, PathBuf}, @@ -9,7 +10,7 @@ use anyhow::{Result, bail}; use async_compression::tokio::write::DeflateDecoder; use bytes::{Bytes, BytesMut}; use futures::{Stream, StreamExt, TryStreamExt}; -use log::{debug, error, info}; +use log::{debug, error}; use reqwest::{Client, StatusCode}; use strum_macros::Display; use tokio::{ @@ -18,11 +19,7 @@ use tokio::{ }; use tokio_util::compat::FuturesAsyncReadCompatExt; -use crate::content::{ - manager::ProgressCallback, - zip::CompressionType, - zlib::{restore_zlib_state, write_zlib_state}, -}; +use crate::content::{manager::ProgressCallback, zip::CompressionType, zlib::write_zlib_state}; use super::zip::{ZipFile, ZipFileEntry}; @@ -116,6 +113,8 @@ impl RestorableDecoder for RestorableDeflateDecoder { #[cfg(windows)] fn restore_state(&mut self) -> Result<(u64, u64), DecoderRestoreError> { + use log::info; + if !self.file_path.exists() || std::fs::metadata(self.file_path.clone()).unwrap().len() == 0 { return Err(DecoderRestoreError::CacheEmpty); @@ -159,10 +158,11 @@ impl AsyncWrite for RestorableDeflateDecoder { #[cfg(windows)] if self.should_save - && let Poll::Ready(Ok(())) = Pin::new(&mut self.inner).poll_flush(cx) { - debug!("save interval reached, serializing state..."); - self.save_state(); - } + && let Poll::Ready(Ok(())) = Pin::new(&mut self.inner).poll_flush(cx) + { + debug!("save interval reached, serializing state..."); + self.save_state(); + } poll_result } diff --git a/maxima-lib/src/content/manager.rs b/maxima-lib/src/content/manager.rs index 94252b30..a1f2980d 100644 --- a/maxima-lib/src/content/manager.rs +++ b/maxima-lib/src/content/manager.rs @@ -24,17 +24,12 @@ use crate::{ zip::{CompressionType, ZipError, ZipFileEntry}, }, core::{ - MaximaEvent, - auth::storage::LockedAuthStorage, - manifest::ManifestError, + MaximaEvent, auth::storage::LockedAuthStorage, manifest::ManifestError, service_layer::ServiceLayerError, }, util::native::{NativeError, maxima_dir}, }; -#[cfg(unix)] -use crate::core::launch::mx_linux_setup; - const QUEUE_FILE: &str = "download_queue.json"; const MAX_CONCURRENT_DOWNLOADS: usize = 16; @@ -405,11 +400,12 @@ impl ContentManager { pub async fn cancel_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { // Stop the in-flight download first so nothing keeps writing to disk. if let Some(current) = &self.current - && current.offer_id() == offer_id { - current.cancel(); - current.wait().await; // let the task settle before mutating state - self.current = None; - } + && current.offer_id() == offer_id + { + current.cancel(); + current.wait().await; // let the task settle before mutating state + self.current = None; + } if self .queue @@ -432,13 +428,14 @@ impl ContentManager { /// on disk, so it can be resumed later via `install_now` with the same game. pub async fn pause_install(&mut self, offer_id: &str) -> Result<(), ContentManagerError> { if let Some(current) = &self.current - && current.offer_id() == offer_id { - current.cancel(); - current.wait().await; - self.current = None; - self.queue.paused = true; - self.queue.save().await?; - } + && current.offer_id() == offer_id + { + current.cancel(); + current.wait().await; + self.current = None; + self.queue.paused = true; + self.queue.save().await?; + } // If it's only queued (not downloading yet), there's nothing to pause. Ok(()) } @@ -482,28 +479,34 @@ impl ContentManager { let mut event = None; if let Some(current) = &self.current - && current.is_done() { - let finished = self - .queue - .current - .take() - .expect("queue.current out of sync with active download"); - self.queue.completed.push(finished); - event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); - self.current = None; - self.queue.paused = false; // Reset pause flag when done - self.queue.save().await?; - } + && current.is_done() + { + let finished = self + .queue + .current + .take() + .expect("queue.current out of sync with active download"); + self.queue.completed.push(finished); + event = Some(MaximaEvent::InstallFinished(current.offer_id.to_owned())); + self.current = None; + self.queue.paused = false; // Reset pause flag when done + self.queue.save().await?; + } - if self.current.is_none() && !self.queue.paused - && let Some(game) = self.queue.pop_next() { - self.install_direct(game).await?; - } + if self.current.is_none() + && !self.queue.paused + && let Some(game) = self.queue.pop_next() + { + self.install_direct(game).await?; + } - if self.current.is_none() && self.queue.current.is_some() && !self.queue.paused - && let Some(game) = self.queue.current.take() { - self.install_direct(game).await?; - } + if self.current.is_none() + && self.queue.current.is_some() + && !self.queue.paused + && let Some(game) = self.queue.current.take() + { + self.install_direct(game).await?; + } Ok(event) } diff --git a/maxima-lib/src/content/mod.rs b/maxima-lib/src/content/mod.rs index 8fcd1ca2..e3b8e2e1 100644 --- a/maxima-lib/src/content/mod.rs +++ b/maxima-lib/src/content/mod.rs @@ -5,9 +5,9 @@ use crate::core::{ auth::storage::LockedAuthStorage, cache::DynamicCache, service_layer::{ - SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, - ServiceDownloadUrlMetadata, ServiceDownloadUrlRequestBuilder, ServiceLayerClient, - ServiceLayerError, + SERVICE_REQUEST_AVAILABLEBUILDS, SERVICE_REQUEST_DOWNLOADURL, ServiceAvailableBuild, + ServiceAvailableBuildsRequestBuilder, ServiceDownloadType, ServiceDownloadUrlMetadata, + ServiceDownloadUrlRequestBuilder, ServiceLayerClient, ServiceLayerError, }, }; diff --git a/maxima-lib/src/core/auth/hardware.rs b/maxima-lib/src/core/auth/hardware.rs index 2cfac08f..00c4f272 100644 --- a/maxima-lib/src/core/auth/hardware.rs +++ b/maxima-lib/src/core/auth/hardware.rs @@ -476,9 +476,10 @@ impl HardwareInfo { } #[cfg(unix)] -fn get_root_creation_str(slug: Option<&str>) -> String { //Option for slug incase of PCSign +fn get_root_creation_str(slug: Option<&str>) -> String { + //Option for slug incase of PCSign use crate::unix::wine::wine_prefix_dir; - use chrono::{DateTime, TimeZone}; + use chrono::DateTime; use std::{fs, os::unix::fs::MetadataExt}; let date_str = String::from("1970010100:00:00.000000000+0000"); diff --git a/maxima-lib/src/core/background_service_nix.rs b/maxima-lib/src/core/background_service_nix.rs index 4cb769df..d657fe68 100644 --- a/maxima-lib/src/core/background_service_nix.rs +++ b/maxima-lib/src/core/background_service_nix.rs @@ -5,7 +5,8 @@ use serde::Serialize; use std::sync::LazyLock; use crate::{ - unix::wine::{CommandType, run_wine_command, wine_prefix_dir}, util::native::{NativeError, SafeParent, SafeStr, module_path}, + unix::wine::{CommandType, run_wine_command, wine_prefix_dir}, + util::native::{NativeError, SafeParent, SafeStr, module_path}, }; static PID_PATTERN: LazyLock = LazyLock::new(|| { diff --git a/maxima-lib/src/core/cloudsync.rs b/maxima-lib/src/core/cloudsync.rs index 0f156478..0846b34b 100644 --- a/maxima-lib/src/core/cloudsync.rs +++ b/maxima-lib/src/core/cloudsync.rs @@ -13,10 +13,7 @@ /// - 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, - library::OwnedOffer, -}; +use super::{auth::storage::LockedAuthStorage, endpoints::API_CLOUDSYNC, library::OwnedOffer}; use crate::util::native::{NativeError, SafeParent, SafeStr}; use derive_getters::Getters; use log::{debug, error}; diff --git a/maxima-lib/src/core/launch.rs b/maxima-lib/src/core/launch.rs index 21aed3e9..3bcece06 100644 --- a/maxima-lib/src/core/launch.rs +++ b/maxima-lib/src/core/launch.rs @@ -31,11 +31,7 @@ use crate::{ use thiserror::Error; #[cfg(unix)] -use crate::{ - core::manifest::{self, MANIFEST_RELATIVE_PATH}, - gameinfo::load_game_info_from_json, - unix::fs::case_insensitive_path, -}; +use crate::unix::fs::case_insensitive_path; use serde::{Deserialize, Serialize}; diff --git a/maxima-lib/src/core/library.rs b/maxima-lib/src/core/library.rs index 4f7b07fe..23654b7a 100644 --- a/maxima-lib/src/core/library.rs +++ b/maxima-lib/src/core/library.rs @@ -78,7 +78,6 @@ impl OwnedOffer { } pub async fn is_installed(&self) -> bool { - let gameinfo = match load_game_info_from_json(&self.slug) { Ok(info) => info, Err(_) => return false, @@ -88,17 +87,16 @@ impl OwnedOffer { } else { #[cfg(windows)] return self.check_install_win_registry().await; - + #[cfg(not(windows))] - return false; + return false; } } // This is unused pub async fn install_check_path(&self) -> Result { Ok(parse_registry_path_json( - self - .offer + self.offer .install_check_override() .as_ref() .ok_or(ManifestError::NoInstallPath(self.slug.clone()))?, @@ -233,9 +231,10 @@ fn group_offers(products: Vec) -> Vec { // Promote any product_map entry that has no corresponding base_products entry for (slug, products) in &product_map { if !base_products.contains_key(slug) - && let Some(first) = products.first() { - base_products.insert(slug.clone(), first.clone()); - } + && let Some(first) = products.first() + { + base_products.insert(slug.clone(), first.clone()); + } } let mut titles = Vec::new(); diff --git a/maxima-lib/src/core/manifest/dip.rs b/maxima-lib/src/core/manifest/dip.rs index 9e6af175..5a907a84 100644 --- a/maxima-lib/src/core/manifest/dip.rs +++ b/maxima-lib/src/core/manifest/dip.rs @@ -332,7 +332,15 @@ impl DiPManifest { let args = self.collect_touchup_args(&install_path, self.locale())?; let path = install_path.join(&self.touchup.path()); let path = case_insensitive_path(path).to_string_lossy().to_string(); - run_wine_command(path.into(), Some(args), None, true, CommandType::Run, &wine_prefix_path.unwrap()).await?; + run_wine_command( + path.into(), + Some(args), + None, + true, + CommandType::Run, + &wine_prefix_path.unwrap(), + ) + .await?; invalidate_mx_wine_registry().await; Ok(()) } diff --git a/maxima-lib/src/core/manifest/mod.rs b/maxima-lib/src/core/manifest/mod.rs index a73b69ea..cacfe9a0 100644 --- a/maxima-lib/src/core/manifest/mod.rs +++ b/maxima-lib/src/core/manifest/mod.rs @@ -37,14 +37,22 @@ 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, wine_prefix_path: Option) -> Result<(), ManifestError>; + async fn run_touchup( + &self, + install_path: &PathBuf, + wine_prefix_path: Option, + ) -> Result<(), ManifestError>; fn execute_path(&self, trial: bool) -> Option; fn version(&self) -> Option; fn needs_touchup_on_locate(&self) -> bool; } #[async_trait::async_trait] impl GameManifest for DiPManifest { - async fn run_touchup(&self, install_path: &PathBuf, wine_prefix_path: Option) -> Result<(), ManifestError> { + async fn run_touchup( + &self, + install_path: &PathBuf, + wine_prefix_path: Option, + ) -> Result<(), ManifestError> { self.run_touchup(install_path, wine_prefix_path).await } @@ -67,7 +75,11 @@ impl GameManifest for DiPManifest { #[async_trait::async_trait] impl GameManifest for PreDiPManifest { - async fn run_touchup(&self, install_path: &PathBuf, wine_prefix_path: Option) -> Result<(), ManifestError> { + async fn run_touchup( + &self, + install_path: &PathBuf, + wine_prefix_path: Option, + ) -> Result<(), ManifestError> { self.run_touchup(install_path, wine_prefix_path).await } @@ -91,7 +103,9 @@ fn bytes_to_string(bytes: Vec) -> Option { } let u16_bytes: Vec = bytes - .as_chunks::<2>().0.iter() + .as_chunks::<2>() + .0 + .iter() .map(|a| u16::from_ne_bytes([a[0], a[1]])) .collect(); @@ -124,8 +138,15 @@ pub async fn load_manifest_from_disk( // 2 different thing for windows and unix as only windows needs elevation, #[cfg(windows)] -pub async fn handle_touchup_request(install_path: PathBuf, _wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { - use crate::{core::background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, gameinfo::GameInstallInfo}; +pub async fn handle_touchup_request( + install_path: PathBuf, + _wine_prefix_path: Option, + slug: &str, +) -> Result<(), ManifestError> { + use crate::{ + core::background_service::{BACKGROUND_SERVICE_PORT, ServiceTouchupRequest}, + gameinfo::GameInstallInfo, + }; let client = reqwest::Client::new(); let _ = client @@ -144,11 +165,19 @@ pub async fn handle_touchup_request(install_path: PathBuf, _wine_prefix_path: Op } #[cfg(unix)] -pub async fn handle_touchup_request(install_path: PathBuf, wine_prefix_path: Option, slug: &str) -> Result<(), ManifestError> { +pub async fn handle_touchup_request( + install_path: PathBuf, + wine_prefix_path: Option, + slug: &str, +) -> Result<(), ManifestError> { use crate::gameinfo::GameInstallInfo; - let manifest = load_manifest_from_disk(install_path.join(MANIFEST_RELATIVE_PATH)).await.unwrap(); - manifest.run_touchup(&install_path, wine_prefix_path.clone()).await?; + let manifest = load_manifest_from_disk(install_path.join(MANIFEST_RELATIVE_PATH)) + .await + .unwrap(); + manifest + .run_touchup(&install_path, wine_prefix_path.clone()) + .await?; let game_install_info = GameInstallInfo::new(install_path, wine_prefix_path); game_install_info.save_to_json(slug); Ok(()) diff --git a/maxima-lib/src/core/manifest/pre_dip.rs b/maxima-lib/src/core/manifest/pre_dip.rs index 13fcbd3a..94fcb3b9 100644 --- a/maxima-lib/src/core/manifest/pre_dip.rs +++ b/maxima-lib/src/core/manifest/pre_dip.rs @@ -263,7 +263,15 @@ impl PreDiPManifest { let path = install_path.join(remove_leading_slash(&self.executable.filePath)); let path = case_insensitive_path(path).to_string_lossy().to_string(); - run_wine_command(path.into(), Some(args), None, true, CommandType::Run, &wine_prefix_path.unwrap()).await?; + run_wine_command( + path.into(), + Some(args), + None, + true, + CommandType::Run, + &wine_prefix_path.unwrap(), + ) + .await?; invalidate_mx_wine_registry().await; Ok(()) diff --git a/maxima-lib/src/core/service_layer.rs b/maxima-lib/src/core/service_layer.rs index d536e3ee..77496b62 100644 --- a/maxima-lib/src/core/service_layer.rs +++ b/maxima-lib/src/core/service_layer.rs @@ -228,9 +228,10 @@ impl ServiceLayerClient { wait }; if let Some(duration) = wait - && duration > Duration::ZERO { - sleep(duration).await; - } + && duration > Duration::ZERO + { + sleep(duration).await; + } let mut last_error = None; diff --git a/maxima-lib/src/gameinfo/mod.rs b/maxima-lib/src/gameinfo/mod.rs index b0bbeec7..9e927687 100644 --- a/maxima-lib/src/gameinfo/mod.rs +++ b/maxima-lib/src/gameinfo/mod.rs @@ -77,7 +77,9 @@ impl GameInstallInfo { } pub fn load_game_info_from_json(slug: &str) -> Result { - let path = maxima_dir()?.join("gameinfo").join(format!("{}.json", slug)); + let path = maxima_dir()? + .join("gameinfo") + .join(format!("{}.json", slug)); let json = fs::read_to_string(path)?; let game_install_info: GameInstallInfo = serde_json::from_str(&json)?; Ok(game_install_info) diff --git a/maxima-lib/src/lsx/connection.rs b/maxima-lib/src/lsx/connection.rs index c13329ac..2b0766d9 100644 --- a/maxima-lib/src/lsx/connection.rs +++ b/maxima-lib/src/lsx/connection.rs @@ -3,17 +3,12 @@ use log::{debug, error, warn}; use quick_xml::DeError; use rand::rand_core::Rng; use regex::Regex; -use std::{ - io::{ErrorKind}, path::PathBuf, sync::{LazyLock}, -}; +use std::{io::ErrorKind, path::PathBuf, sync::LazyLock}; use sysinfo::{Pid, System}; use thiserror::Error; use tokio::net::TcpStream; -use tokio::{ - io::AsyncWriteExt, - sync::MutexGuard, -}; use tokio::sync::mpsc::Sender; +use tokio::{io::AsyncWriteExt, sync::MutexGuard}; use super::{ request::{ @@ -43,9 +38,7 @@ use super::{ }, }; use crate::{ - core::{ - LockedMaxima, Maxima, MaximaEvent, launch::ActiveGameContext, - }, + core::{LockedMaxima, Maxima, MaximaEvent, launch::ActiveGameContext}, lsx::{request::LSXRequestError, types::LSXRequestType}, util::{ native::NativeError, @@ -117,7 +110,6 @@ impl ConnectionState { pub fn enable_encryption(&mut self, encryption_key: [u8; 16]) { self.encryption = EncryptionState::Ready(encryption_key); } - } pub fn get_os_pid(context: &ActiveGameContext) -> Result { @@ -216,8 +208,7 @@ impl Connection { .ok_or(NativeError::Stringify)? .to_owned(); - pid = - get_wine_pid(context.launch_id(), &filename, context.slug().as_deref()).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 {}", @@ -249,11 +240,7 @@ impl Connection { pid: pid.unwrap_or(0), }; - Ok(Self { - stream, - state, - tx, - }) + Ok(Self { stream, state, tx }) } // Initialization @@ -328,14 +315,14 @@ impl Connection { let lsx_message: LSX = quick_xml::de::from_str(message.as_str())?; let reply = match lsx_message.value { - LSXMessageType::Request(msg) => { - self.process_request_message(msg).await? - } + LSXMessageType::Request(msg) => self.process_request_message(msg).await?, LSXMessageType::Event(_) => { None // Blank for now } LSXMessageType::Response(_) => { - warn!("Unexpected LSX Response message received, ignoring (server message type sent to server)"); + warn!( + "Unexpected LSX Response message received, ignoring (server message type sent to server)" + ); None } }; @@ -356,9 +343,7 @@ impl Connection { message: LSXRequest, ) -> Result, LSXConnectionError> { { - let (maxima_arc, pid) = { - (self.state.maxima.clone(), *self.state.pid()) - }; + let (maxima_arc, pid) = { (self.state.maxima.clone(), *self.state.pid()) }; maxima_arc .lock() @@ -367,7 +352,7 @@ impl Connection { } let result = lsx_message_matcher!( - + &mut self.state, message.value, LSXRequestType; ChallengeResponse handle_challenge_response, diff --git a/maxima-lib/src/lsx/request/profile.rs b/maxima-lib/src/lsx/request/profile.rs index bf514d5c..71d751c8 100644 --- a/maxima-lib/src/lsx/request/profile.rs +++ b/maxima-lib/src/lsx/request/profile.rs @@ -79,10 +79,7 @@ pub async fn handle_set_presence_request( info!( "Setting Presence to {:?}: {}", request.attr_Presence, - request - .attr_RichPresence - .to_owned() - .unwrap_or_default() + request.attr_RichPresence.to_owned().unwrap_or_default() ); let mut maxima = state.maxima().lock().await; diff --git a/maxima-lib/src/unix/wine.rs b/maxima-lib/src/unix/wine.rs index 9fe90d66..08f6e9e5 100644 --- a/maxima-lib/src/unix/wine.rs +++ b/maxima-lib/src/unix/wine.rs @@ -7,7 +7,7 @@ use std::{ collections::HashMap, env, ffi::OsStr, - fs::{File, remove_dir_all, remove_file}, + fs::remove_dir_all, io::Read, process::{ExitStatus, Stdio}, }; @@ -25,13 +25,7 @@ use xz2::read::XzDecoder; use crate::{ gameinfo::load_game_info_from_json, - util::{ - github::{ - GithubRelease, fetch_github_release, fetch_github_releases, github_download_asset, - }, - native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, - registry::RegistryError, - }, + util::native::{DownloadError, NativeError, SafeParent, SafeStr, WineError, maxima_dir}, }; static PROTON_PATTERN: LazyLock = LazyLock::new(|| { diff --git a/maxima-lib/src/util/registry.rs b/maxima-lib/src/util/registry.rs index 6b2367e3..7445dd69 100644 --- a/maxima-lib/src/util/registry.rs +++ b/maxima-lib/src/util/registry.rs @@ -147,9 +147,10 @@ fn inject_wow6432node(sub_key: &str) -> Option { // Only rewrite paths under SOFTWARE\ that aren't already redirected let prefix = "SOFTWARE\\"; if let Some(rest) = sub_key.strip_prefix(prefix) - && !rest.starts_with("WOW6432Node\\") { - return Some(format!("SOFTWARE\\WOW6432Node\\{}", rest)); - } + && !rest.starts_with("WOW6432Node\\") + { + return Some(format!("SOFTWARE\\WOW6432Node\\{}", rest)); + } None } @@ -221,9 +222,7 @@ pub async fn parse_registry_path_json( #[cfg(windows)] pub async fn parse_registry_path_regkey(key: &str) -> Result { - let mut parts = key - .split(['[', ']']) - .filter(|s| !s.is_empty()); + let mut parts = key.split(['[', ']']).filter(|s| !s.is_empty()); if let (Some(first), Some(second)) = (parts.next(), parts.next()) { let path = match read_reg_key(first, None).await? { @@ -244,9 +243,7 @@ pub async fn parse_registry_path_regkey(key: &str) -> Result Result { - let mut parts = key - .split(['[', ']']) - .filter(|s| !s.is_empty()); + let mut parts = key.split(['[', ']']).filter(|s| !s.is_empty()); if let (Some(first), Some(_second)) = (parts.next(), parts.next()) { let path = match read_reg_key(first, None).await? { diff --git a/maxima-service/src/service/mod.rs b/maxima-service/src/service/mod.rs index df7389b9..5f2c9468 100644 --- a/maxima-service/src/service/mod.rs +++ b/maxima-service/src/service/mod.rs @@ -291,7 +291,9 @@ async fn req_touchup(body: web::Bytes) -> Result { Path::new(&request.output_dir).join(maxima::core::manifest::MANIFEST_RELATIVE_PATH), ) .await?; - manifest.run_touchup(&PathBuf::from(request.output_dir), None).await?; + manifest + .run_touchup(&PathBuf::from(request.output_dir), None) + .await?; Ok(HttpResponse::Ok().body("Done")) } diff --git a/maxima-tui/src/main.rs b/maxima-tui/src/main.rs index 756fe180..c5428490 100644 --- a/maxima-tui/src/main.rs +++ b/maxima-tui/src/main.rs @@ -1,14 +1,9 @@ - use log::{error, info, warn}; use regex::Regex; use service::{BridgeThread, MaximaLibRequest, MaximaLibResponse}; use tokio_stream::StreamExt; -use std::{ - io::stdout, - sync::LazyLock, - time::Duration, -}; +use std::{io::stdout, sync::LazyLock, time::Duration}; #[cfg(windows)] use is_elevated::is_elevated; @@ -19,6 +14,7 @@ use maxima::{ util::service::{is_service_running, is_service_valid, register_service_user, start_service}, }; +use maxima::core::{LockedMaxima, launch::LaunchMode}; use maxima::{ core::{ MaximaEvent, @@ -26,10 +22,6 @@ use maxima::{ }, util::{native::take_foreground_focus, registry::check_registry_validity}, }; -use maxima::core::{ - LockedMaxima, - launch::LaunchMode, - }; static MANUAL_LOGIN_PATTERN: LazyLock = LazyLock::new(|| Regex::new(r"^(.*):(.*)$").expect("manual login regex should be valid")); @@ -115,15 +107,16 @@ impl App { } if let Event::Key(key) = event::read()? - && key.kind == KeyEventKind::Press { - use KeyCode::*; - match key.code { - Char('l') | Right => self.next_tab(), - Char('h') | Left => self.previous_tab(), - Char('q') | Esc => self.quit(), - _ => {} - } + && key.kind == KeyEventKind::Press + { + use KeyCode::*; + match key.code { + Char('l') | Right => self.next_tab(), + Char('h') | Left => self.previous_tab(), + Char('q') | Esc => self.quit(), + _ => {} } + } Ok(()) } @@ -456,7 +449,9 @@ async fn start_game( let mut maxima = maxima_arc.lock().await; for event in maxima.consume_pending_events() { - if let MaximaEvent::ReceivedLSXRequest(_pid, _request) = event { () } + if let MaximaEvent::ReceivedLSXRequest(_pid, _request) = event { + () + } } maxima.update().await; diff --git a/maxima-ui/src/bridge/get_games.rs b/maxima-ui/src/bridge/get_games.rs index f6ec9cf5..c89d3e23 100644 --- a/maxima-ui/src/bridge/get_games.rs +++ b/maxima-ui/src/bridge/get_games.rs @@ -10,7 +10,7 @@ use maxima::{ LockedMaxima, service_layer::{ SERVICE_REQUEST_GAMEIMAGES, SERVICE_REQUEST_GETHEROBACKGROUNDIMAGE, ServiceGame, - ServiceGameHub, ServiceGameHubCollection, ServiceGameImagesRequestBuilder, + ServiceGameHubCollection, ServiceGameImagesRequestBuilder, ServiceHeroBackgroundImageRequestBuilder, ServiceLayerClient, }, }, diff --git a/maxima-ui/src/bridge/start_game.rs b/maxima-ui/src/bridge/start_game.rs index 91f8a5c5..25dec6cc 100644 --- a/maxima-ui/src/bridge/start_game.rs +++ b/maxima-ui/src/bridge/start_game.rs @@ -1,5 +1,5 @@ -use crate::{GameInfo, GameSettings, bridge_thread::BackendError}; -use log::{debug, error, info}; +use crate::{GameInfo, GameSettings}; +use log::{debug, info}; use maxima::core::{ LockedMaxima, launch::{self, LaunchError, LaunchMode, LaunchOptions}, diff --git a/maxima-ui/src/bridge_processor.rs b/maxima-ui/src/bridge_processor.rs index 2d226baa..41b572f5 100644 --- a/maxima-ui/src/bridge_processor.rs +++ b/maxima-ui/src/bridge_processor.rs @@ -3,7 +3,7 @@ use crate::{ bridge_thread::{self, BackendError}, views::downloads_view::QueuedDownload, }; -use log::{error, info, warn}; +use log::{info, warn}; use tokio::sync::mpsc::error::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 e639a796..41f65e87 100644 --- a/maxima-ui/src/bridge_thread.rs +++ b/maxima-ui/src/bridge_thread.rs @@ -1,4 +1,4 @@ -use egui::{Context, Key::W}; +use egui::Context; use log::{error, info, warn}; use maxima::core::manifest::handle_touchup_request; use thiserror::Error; @@ -36,7 +36,7 @@ use maxima::{ lsx::service::LSXServerError, rtm::RtmError, util::{ - native::{NativeError, maxima_dir}, + native::NativeError, registry::{RegistryError, check_registry_validity, set_up_registry}, }, }; From 0964d9ba8634f1e4775b53ec2092d8b4f489d426 Mon Sep 17 00:00:00 2001 From: sjp761 <171447425+sjp761@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:00:24 -0500 Subject: [PATCH 57/57] Ignore Cargo.lock (maintainer request) --- .gitignore | 1 + Cargo.lock | 8229 ---------------------------------------------------- 2 files changed, 1 insertion(+), 8229 deletions(-) delete mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 781a6771..a3d2ea4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /.vscode /.idea +Cargo.lock \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 2a556528..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,8229 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "accesskit" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" -dependencies = [ - "enumn", - "serde", - "uuid", -] - -[[package]] -name = "actix-codec" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31404e1443b7b7bcaa311c1af456775cc3f668e34573f1503d7ce4844327629e" -dependencies = [ - "bitflags 2.13.1", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d62d1a48894ec9450bcde7ef1e3681205771ff6ea9c61cc5ae031145192787" -dependencies = [ - "actix-codec", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags 2.13.1", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash 0.2.0", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.10.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a16bf2f19c2ad84842bdfe6f3665620e93197d5c607889bfa3aaac45e762fd1" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d44ae8a6516f4ac7bfc7b61aabcd286104e96b4b24c747ce220832a016056d9" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbacab3593b6b4f7be815076fc52d60a83c873426824675417e2abdd229e2e36" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash 0.2.0", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aes" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" -dependencies = [ - "cipher", - "cpubits", - "cpufeatures 0.3.1", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.13.1", - "cc", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 2.0.20", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "async-compression" -version = "0.4.42" -source = "git+https://github.com/sjp761/async-compression.git?rev=a226c72f754f942ad187cc65cb1f03df5ae26b7b#a226c72f754f942ad187cc65cb1f03df5ae26b7b" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey 0.1.1", - "rayon", - "thiserror 2.0.20", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom 8.0.0", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "aws-lc-rs" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide 0.8.9", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - -[[package]] -name = "bit-set" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" -dependencies = [ - "bit-vec 0.9.1", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bit-vec" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitstream-io" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" -dependencies = [ - "no_std_io2", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", - "zeroize", -] - -[[package]] -name = "block-padding" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.4", -] - -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" -dependencies = [ - "memchr", - "serde_core", -] - -[[package]] -name = "built" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - -[[package]] -name = "bytebuffer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2618ff1f07e000264e355904bff5c0527091fcbad883ff7f71383c5428915f4b" -dependencies = [ - "byteorder", -] - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "bytestring" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" -dependencies = [ - "bytes", -] - -[[package]] -name = "cacao" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5952f0958672e4aa8fc706d01905c56af57759e078c53a6fddf4a13361943e7a" -dependencies = [ - "block", - "core-foundation 0.9.4", - "core-graphics 0.22.3", - "dispatch", - "lazy_static", - "libc", - "objc", - "objc_id", - "os_info", - "url", -] - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.13.1", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" -dependencies = [ - "bitflags 2.13.1", - "polling", - "rustix 1.1.4", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.4", - "rustix 1.1.4", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cbc" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - -[[package]] -name = "chacha20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "rand_core 0.10.1", -] - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" -dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", - "inout", - "zeroize", -] - -[[package]] -name = "clap" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "codespan-reporting" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "color" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compact_str" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "compression-codecs" -version = "0.4.38" -source = "git+https://github.com/sjp761/async-compression.git?rev=a226c72f754f942ad187cc65cb1f03df5ae26b7b#a226c72f754f942ad187cc65cb1f03df5ae26b7b" -dependencies = [ - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.32" -source = "git+https://github.com/sjp761/async-compression.git?rev=a226c72f754f942ad187cc65cb1f03df5ae26b7b#a226c72f754f942ad187cc65cb1f03df5ae26b7b" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types", - "foreign-types 0.3.2", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.13.1", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.1.4", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf", -] - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" -dependencies = [ - "darling_core 0.24.1", - "darling_macro 0.24.1", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] - -[[package]] -name = "darling_core" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 3.0.4", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "darling_macro" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" -dependencies = [ - "darling_core 0.24.1", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "dataview" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" -dependencies = [ - "derive_pod", -] - -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "derive-getters" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.119", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", - "unicode-xid", -] - -[[package]] -name = "derive_pod" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "const-oid", - "crypto-common 0.2.2", -] - -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.4", -] - -[[package]] -name = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecb" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac" -dependencies = [ - "cipher", -] - -[[package]] -name = "ecolor" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc883f505d446814c0226bef4b64564b9adb9966fc00c88c57ce6d827dd152d6" -dependencies = [ - "bytemuck", - "emath", - "serde", -] - -[[package]] -name = "eframe" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2afc0cbcdb6896b7bfb1dbbebaf7b9af9635ff38fd01e89bdd0174c1717b1857" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "egui-wgpu", - "egui-winit", - "egui_glow", - "glow", - "glutin", - "glutin-winit", - "image", - "js-sys", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "profiling", - "raw-window-handle", - "ron", - "serde", - "static_assertions", - "wasm-bindgen", - "web-sys", - "web-time", - "windows-sys 0.61.2", - "winit", -] - -[[package]] -name = "egui" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c977ac91dfaa651633fd9722e4ce9ccb32cda4c748b89a5cb57e504036e37c13" -dependencies = [ - "accesskit", - "ahash", - "bitflags 2.13.1", - "emath", - "epaint", - "itertools 0.15.0", - "log", - "nohash-hasher", - "profiling", - "ron", - "serde", - "smallvec", - "unicode-segmentation", - "web-sys", -] - -[[package]] -name = "egui-wgpu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc362cc6bc8c1d7169c9cb0e7741ff1e03063681373d6e55cc3b25fd21213f3" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 2.0.20", - "type-map", - "web-time", - "wgpu", - "winit", -] - -[[package]] -name = "egui-winit" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9327fc8edef2c57db9bcbcacc82c8a4b1e8cc64cd41a67db4419dcf643d88c83" -dependencies = [ - "arboard", - "bytemuck", - "egui", - "log", - "objc2 0.6.4", - "objc2-foundation 0.3.2", - "objc2-ui-kit 0.3.2", - "profiling", - "raw-window-handle", - "serde", - "smithay-clipboard", - "web-time", - "winit", -] - -[[package]] -name = "egui_extras" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e651e08ddf6bb8bfd964ad70e33bc721c093f2b77ab59580e7140c008dab68c" -dependencies = [ - "ahash", - "egui", - "ehttp", - "enum-map", - "image", - "itertools 0.15.0", - "log", - "mime_guess2", - "profiling", - "resvg", -] - -[[package]] -name = "egui_glow" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b65e45f8d8d7c6d848b3ec41f642b54ad828033f1511d4b4c9adc09040166a" -dependencies = [ - "bytemuck", - "egui", - "glow", - "log", - "profiling", - "winit", -] - -[[package]] -name = "ehttp" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f1b93eb2e039aaff63ce07cca59bd1dca02f2ce30075a17b619d2c42f56efc" -dependencies = [ - "document-features", - "js-sys", - "ureq", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "either" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" - -[[package]] -name = "emath" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce03cf1604f74515830012c29991beb0b58f69e4e43f5afe22fa7afed65ccd5" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "enable-ansi-support" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7457668b3da8a4b702f3d79e131aa3e81cd7e81cc95fb2d54fce9f182ecc77" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-map" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" -dependencies = [ - "enum-map-derive", -] - -[[package]] -name = "enum-map-derive" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "enumn" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "epaint" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11863a6b2b7823010c1090ddf4706947bdda46766742def0673195cbf7ff4a73" -dependencies = [ - "ahash", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "font-types", - "harfrust", - "log", - "nohash-hasher", - "parking_lot", - "profiling", - "self_cell", - "serde", - "skrifa", - "smallvec", - "unicode-general-category", - "unicode-segmentation", - "vello_cpu", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dee69613aac468922cf28a32025eb7d7ed6985b61f73245848e58f37876c98" - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "exr" -version = "1.74.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide 0.8.9", - "num-complex", - "pulp", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "eyre" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3" -dependencies = [ - "autocfg", - "indenter", - "once_cell", -] - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set 0.5.3", - "regex", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fearless_simd" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" - -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.10" -source = "git+https://github.com/sjp761/flate2-rs.git?rev=45fc9f6a56a97513b285029fee14213a2d0fe2e5#45fc9f6a56a97513b285029fee14213a2d0fe2e5" -dependencies = [ - "crc32fast", - "libz-sys", - "miniz_oxide 0.9.1", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "font-types" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" -dependencies = [ - "bytemuck", - "serde", -] - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-executor" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasm-bindgen", -] - -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glifo" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" -dependencies = [ - "bytemuck", - "foldhash 0.2.0", - "hashbrown 0.17.1", - "log", - "peniko", - "skrifa", - "smallvec", - "vello_common", -] - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "globset" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "glow" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" -dependencies = [ - "bitflags 2.13.1", - "cfg_aliases", - "cgl", - "dispatch2", - "glutin_egl_sys", - "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "once_cell", - "raw-window-handle", - "wayland-sys", - "windows-sys 0.52.0", - "x11-dl", -] - -[[package]] -name = "glutin-winit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" -dependencies = [ - "cfg_aliases", - "glutin", - "raw-window-handle", - "winit", -] - -[[package]] -name = "glutin_egl_sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" -dependencies = [ - "gl_generator", - "windows-sys 0.52.0", -] - -[[package]] -name = "glutin_glx_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" -dependencies = [ - "gl_generator", - "x11-dl", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "guillotiere" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" -dependencies = [ - "euclid", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "harfrust" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "read-fonts", - "smallvec", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http 1.5.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" -dependencies = [ - "bytes", - "futures-core", - "http 1.5.0", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humansize" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" -dependencies = [ - "libm", -] - -[[package]] -name = "hybrid-array" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" -dependencies = [ - "typenum", - "zeroize", -] - -[[package]] -name = "hyper" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http 1.5.0", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http 1.5.0", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http 1.5.0", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.1", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - -[[package]] -name = "imgref" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" - -[[package]] -name = "impl-more" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277ff51754a3f68f12f58446c5d006aa8baa4914ea273cce24a599cfaff33d4f" - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "2.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "block-padding", - "hybrid-array", -] - -[[package]] -name = "inquire" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" -dependencies = [ - "bitflags 2.13.1", - "crossterm", - "dyn-clone", - "fuzzy-matcher", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "instability" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" -dependencies = [ - "darling 0.24.1", - "indoc", - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "io-kit-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d3a048d09fbb6597dbf7c69f40d14df4a49487db1487191618c893fc3b1c26" -dependencies = [ - "core-foundation-sys", - "mach2 0.5.0", -] - -[[package]] -name = "ipnet" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "is_elevated" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5299060ff5db63e788015dcb9525ad9b84f4fd9717ed2cbdeba5018cbf42f9b5" -dependencies = [ - "winapi", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.20", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.119", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.20", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - -[[package]] -name = "kurbo" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" -dependencies = [ - "arrayvec", - "euclid", - "polycool", - "smallvec", -] - -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libfuzzer-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" -dependencies = [ - "bitflags 2.13.1", - "libc", - "plain", - "redox_syscall 0.9.3", -] - -[[package]] -name = "libz-sys" -version = "1.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "line-clipping" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "linebender_resource_handle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" -dependencies = [ - "serde_core", - "value-bag", -] - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lru" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" -dependencies = [ - "hashbrown 0.17.1", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4_flex" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix 0.29.0", - "winapi", -] - -[[package]] -name = "mach2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" -dependencies = [ - "libc", -] - -[[package]] -name = "mach2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maxima-bootstrap" -version = "0.1.0" -dependencies = [ - "base64 0.23.1", - "cacao", - "maxima-lib", - "maxima-resources", - "reqwest", - "serde_json", - "thiserror 2.0.20", - "tokio", - "url", - "urlencoding", -] - -[[package]] -name = "maxima-cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "inquire", - "is_elevated", - "log", - "maxima-lib", - "maxima-resources", - "regex", - "rustls", - "tokio", -] - -[[package]] -name = "maxima-lib" -version = "0.1.0" -dependencies = [ - "aes", - "anyhow", - "async-compression", - "async-trait", - "base64 0.23.1", - "bytebuffer", - "bytes", - "cbc", - "chrono", - "crc32fast", - "derive-getters", - "derive_builder", - "directories", - "dirs", - "ecb", - "enable-ansi-support", - "encoding_rs", - "futures", - "gethostname", - "glob", - "globset", - "hex", - "http 1.5.0", - "is_elevated", - "libz-sys", - "log", - "mac_address", - "md5", - "moka", - "open", - "pastey 0.2.3", - "pelite", - "prost", - "prost-build", - "quick-xml 0.42.0", - "rand 0.10.2", - "regex", - "reqwest", - "ring", - "rustls", - "rustls-pki-types", - "scopeguard", - "serde", - "serde_json", - "sha1", - "sha2 0.11.0", - "smbios-lib", - "strum_macros", - "sysinfo", - "tar", - "thiserror 2.0.20", - "tokio", - "tokio-rustls", - "tokio-stream", - "tokio-util", - "toml 1.1.5+spec-1.1.0", - "ureq", - "url", - "uuid", - "webpki-roots", - "which", - "widestring", - "windows-service", - "windows-sys 0.61.2", - "winreg", - "wmi", - "xz2", -] - -[[package]] -name = "maxima-resources" -version = "0.1.0" -dependencies = [ - "winres", -] - -[[package]] -name = "maxima-service" -version = "0.1.0" -dependencies = [ - "actix-web", - "hex", - "log", - "maxima-lib", - "maxima-resources", - "serde_json", - "sha2 0.11.0", - "structured-logger", - "thiserror 2.0.20", - "tokio", - "windows-service", - "windows-sys 0.61.2", -] - -[[package]] -name = "maxima-tui" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "color-eyre", - "inquire", - "is_elevated", - "log", - "maxima-lib", - "maxima-resources", - "ratatui", - "regex", - "strum", - "tokio", - "tokio-stream", -] - -[[package]] -name = "maxima-ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "eframe", - "egui", - "egui_extras", - "fuzzy-matcher", - "humansize", - "image", - "is_elevated", - "log", - "maxima-lib", - "maxima-resources", - "puffin", - "puffin_http", - "reqwest", - "rustls", - "serde", - "serde_json", - "strum", - "strum_macros", - "sys-locale", - "thiserror 2.0.20", - "tokio", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "md5" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess2" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" -dependencies = [ - "mime", - "phf", - "phf_shared", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "miniz_oxide" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "moka" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "naga" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" -dependencies = [ - "arrayvec", - "bit-set 0.10.0", - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.17.1", - "indexmap", - "libm", - "log", - "naga-types", - "num-traits", - "once_cell", - "rustc-hash 1.1.0", - "thiserror 2.0.20", - "unicode-ident", -] - -[[package]] -name = "naga-types" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" -dependencies = [ - "hashbrown 0.17.1", - "indexmap", - "rustc-hash 1.1.0", - "thiserror 2.0.20", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "no_std_io2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" -dependencies = [ - "memchr", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "bytemuck", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "num-integer" -version = "0.1.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "libc", - "objc2 0.5.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2 0.6.4", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "dispatch", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "block2 0.6.2", - "libc", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-open-directory" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" -dependencies = [ - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.1", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-cloud-kit 0.2.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", - "objc2-core-location 0.2.2", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core 0.2.2", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.1", - "block2 0.6.2", - "objc2 0.6.4", - "objc2-cloud-kit 0.3.2", - "objc2-core-data 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image 0.3.2", - "objc2-core-location 0.3.2", - "objc2-core-text", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", - "objc2-user-notifications 0.3.2", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.13.1", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "open" -version = "5.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c603ab8300cf18bc3b14146b19fe3dfcc4843ae5a400cd0e7a30b95aa366634" -dependencies = [ - "is-wsl", - "libc", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "orbclient" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "os_info" -version = "3.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" -dependencies = [ - "android_system_properties", - "log", - "nix 0.31.3", - "objc2 0.6.4", - "objc2-foundation 0.3.2", - "objc2-ui-kit 0.3.2", - "serde", - "windows-sys 0.61.2", -] - -[[package]] -name = "owo-colors" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" - -[[package]] -name = "palette" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" -dependencies = [ - "approx", - "libm", - "palette_derive", - "palette_math", -] - -[[package]] -name = "palette_derive" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" -dependencies = [ - "by_address", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "palette_math" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" -dependencies = [ - "libm", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "pastey" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" - -[[package]] -name = "pelite" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" -dependencies = [ - "dataview", - "libc", - "no-std-compat", - "pelite-macros", - "winapi", -] - -[[package]] -name = "pelite-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" - -[[package]] -name = "peniko" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" -dependencies = [ - "bytemuck", - "color", - "kurbo 0.13.1", - "linebender_resource_handle", - "smallvec", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pest_meta" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" -dependencies = [ - "pest", -] - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset 0.5.7", - "hashbrown 0.15.5", - "indexmap", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.8", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", - "unicase", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", - "unicase", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "polycool" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" -dependencies = [ - "heck", - "itertools 0.14.0", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.119", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "prost-types" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" -dependencies = [ - "prost", -] - -[[package]] -name = "puffin" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b514d95a258be801fde8a1ff1c974f4a4841d9750f5d1d6690fc07a5ad4049" -dependencies = [ - "anyhow", - "bincode", - "byteorder", - "cfg-if", - "itertools 0.14.0", - "lz4_flex", - "parking_lot", - "serde", -] - -[[package]] -name = "puffin_http" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f912991aab1adae69d2be9455e8db0f41e5ad3da87706ab2de64908678c2c76" -dependencies = [ - "anyhow", - "crossbeam-channel", - "log", - "parking_lot", - "puffin", -] - -[[package]] -name = "pulp" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" -dependencies = [ - "bytemuck", - "cfg-if", - "libm", - "num-complex", - "paste", - "pulp-wasm-simd-flag", - "raw-cpuid", - "reborrow", - "version_check", -] - -[[package]] -name = "pulp-wasm-simd-flag" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" - -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.3", - "rustls", - "socket2", - "thiserror 2.0.20", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash 2.1.3", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.20", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - -[[package]] -name = "ratatui" -version = "0.30.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termina", - "ratatui-termwiz", - "ratatui-widgets", - "serde", -] - -[[package]] -name = "ratatui-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" -dependencies = [ - "bitflags 2.13.1", - "compact_str", - "critical-section", - "hashbrown 0.17.1", - "itertools 0.14.0", - "kasuari", - "lru", - "palette", - "serde", - "strum", - "thiserror 2.0.20", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termina" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" -dependencies = [ - "instability", - "ratatui-core", - "termina", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.17.1", - "indoc", - "instability", - "itertools 0.14.0", - "line-clipping", - "ratatui-core", - "serde", - "strum", - "time", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.14.0", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.5", - "rand_chacha", - "simd_helpers", - "thiserror 2.0.20", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "read-fonts" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" -dependencies = [ - "bytemuck", - "font-types", - "once_cell", -] - -[[package]] -name = "reborrow" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_syscall" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.20", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.5.0", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "resvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - -[[package]] -name = "rgb" -version = "0.8.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "ron" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" -dependencies = [ - "bitflags 2.13.1", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", -] - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "self_cell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "serde_fmt" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "skrifa" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" -dependencies = [ - "bytemuck", - "read-fonts", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" -dependencies = [ - "serde", -] - -[[package]] -name = "smbios-lib" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e739c9b449374b4e9435857e0084895a59ef79b46d1c2b0f976c1c564ae7e4bc" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "getopts", - "io-kit-sys", - "libc", - "mach2 0.6.0", - "serde", - "serde_json", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.13.1", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.13.1", - "calloop 0.14.4", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.4", - "thiserror 2.0.20", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" -dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "structured-logger" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5e5db98a0b69a45a1f8604185fd4c2a5900b640118530b82c4ce74e12c5c9ab" -dependencies = [ - "log", - "parking_lot", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "sval" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81b254da21fe1fcc4e3a74fe39b46e25e3a863078f8b71c954d47f84889dbc6" - -[[package]] -name = "sval_buffer" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50be352d2822ffafb59e3e2ddac9d5ee60f2eeadbb7b5a2a951b9f3651e87a6f" -dependencies = [ - "sval", - "sval_ref", - "zerocopy", -] - -[[package]] -name = "sval_dynamic" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048ca293b998d9a45659159f94a64063791e74cdc670164943dbb434405573d" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b5888e40f80568733217f27b7317b845f463400ced36c424b1a804730e53b2" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17664d6bb6b74947afaab9d7c991caa9bf5638d4dee16fcbef637f440796049" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c059969ca5ca163ea7fef6c9661758973d17691aba92abdcf5c428f4ec122c" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d6b29ff568c85c87561807f51d2adfff4b6016c6363133f7cd1652a12548f3" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f33ec9edc42b12764d5c90ca0a1d84189c6bde81ed27507f1e661c6e4e05853" -dependencies = [ - "serde_core", - "sval", - "sval_nested", -] - -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo 0.11.3", - "siphasher", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "sys-locale" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" -dependencies = [ - "libc", -] - -[[package]] -name = "sysinfo" -version = "0.39.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "objc2-open-directory", - "windows", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "termina" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" -dependencies = [ - "bitflags 2.13.1", - "parking_lot", - "rustix 1.1.4", - "signal-hook", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom 7.1.3", - "phf", - "phf_codegen", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bitflags 2.13.1", - "fancy-regex", - "filedescriptor", - "finl_unicode", - "fixedbitset 0.4.2", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix 0.29.0", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf", - "sha2 0.10.9", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl 2.0.20", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba2077be5cd93c7408c849e45a4ab261b59f923f4bcdee2a724b8ed5a175a77" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "libc", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "async-compression", - "bitflags 2.13.1", - "bytes", - "futures-core", - "futures-util", - "http 1.5.0", - "http-body", - "http-body-util", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.3", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools 0.14.0", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" -dependencies = [ - "base64 0.23.1", - "flate2", - "log", - "percent-encoding", - "rustls", - "rustls-pki-types", - "ureq-proto", - "utf8-zero", - "webpki-roots", -] - -[[package]] -name = "ureq-proto" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" -dependencies = [ - "base64 0.23.1", - "http 1.5.0", - "httparse", - "log", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "usvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "imagesize", - "kurbo 0.11.3", - "log", - "pico-args", - "roxmltree", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "xmlwriter", -] - -[[package]] -name = "utf8-zero" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" -dependencies = [ - "atomic", - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "value-bag" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0941feceafbe7a8f59ea1096d45b97002884a41306315ad797b3684b63a81d8c" -dependencies = [ - "erased-serde", - "serde_core", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839752af8179287d27eb2b94164641b1ede9e60ab7424163388dc21ebd0508cd" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vello_common" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" -dependencies = [ - "bytemuck", - "fearless_simd", - "guillotiere", - "log", - "peniko", - "smallvec", - "thiserror 2.0.20", -] - -[[package]] -name = "vello_cpu" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" -dependencies = [ - "bytemuck", - "glifo", - "hashbrown 0.17.1", - "vello_common", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.4", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" -dependencies = [ - "bitflags 2.13.1", - "rustix 1.1.4", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.13.1", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" -dependencies = [ - "rustix 1.1.4", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" -dependencies = [ - "proc-macro2", - "quick-xml 0.41.0", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2 0.10.9", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - -[[package]] -name = "wgpu" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" -dependencies = [ - "arrayvec", - "bitflags 2.13.1", - "bytemuck", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown 0.17.1", - "js-sys", - "log", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" -dependencies = [ - "arrayvec", - "bit-set 0.10.0", - "bit-vec 0.9.1", - "bitflags 2.13.1", - "bytemuck", - "cfg_aliases", - "document-features", - "hashbrown 0.17.1", - "indexmap", - "log", - "naga", - "naga-types", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.20", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-naga-bridge", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases", - "hashbrown 0.17.1", - "libloading", - "log", - "naga", - "naga-types", - "portable-atomic", - "portable-atomic-util", - "raw-window-handle", - "renderdoc-sys", - "static_assertions", - "thiserror 2.0.20", - "wgpu-naga-bridge", - "wgpu-types", -] - -[[package]] -name = "wgpu-naga-bridge" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" -dependencies = [ - "naga", - "wgpu-types", -] - -[[package]] -name = "wgpu-types" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "js-sys", - "log", - "naga-types", - "raw-window-handle", - "static_assertions", - "web-sys", -] - -[[package]] -name = "which" -version = "8.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" -dependencies = [ - "libc", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-service" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" -dependencies = [ - "bitflags 2.13.1", - "widestring", - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winit" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" -dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.13.1", - "block2 0.5.1", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit 0.2.2", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "smithay-client-toolkit 0.19.2", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - -[[package]] -name = "winres" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" -dependencies = [ - "toml 0.5.11", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wmi" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" -dependencies = [ - "chrono", - "futures", - "log", - "serde", - "thiserror 2.0.20", - "windows", - "windows-core", -] - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.4", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.4", -] - -[[package]] -name = "xcursor" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.13.1", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - -[[package]] -name = "xml-rs" -version = "0.8.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "zune-core" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -]