From 013ec56b5660a396cd145ee5d635165733104f5e Mon Sep 17 00:00:00 2001 From: Iain Laird Date: Fri, 14 Feb 2025 19:14:15 +0000 Subject: [PATCH] wip --- Cargo.lock | 13 + common/src/fast_im_map.rs | 11 +- installer/src/error.rs | 2 +- installer/src/installer.rs | 10 +- installer/src/lib.rs | 2 +- installer/src/traits.rs | 15 +- moss/Cargo.toml | 1 + moss/src/app.rs | 31 +- moss/src/app/app_delegate.rs | 2 +- moss/src/app/controllers/app_controller.rs | 13 +- moss/src/app/controllers/async_controller.rs | 223 +++++++++++ moss/src/app/controllers/mod.rs | 2 + moss/src/app/installer_impl.rs | 18 +- moss/src/app/mod_description.rs | 38 +- moss/src/app/mod_entry.rs | 309 +++++++-------- moss/src/app/mod_entry/version_checker.rs | 117 ++++++ moss/src/app/mod_list.rs | 361 ++++++++---------- .../app/mod_list/actions/action_options.rs | 2 +- moss/src/app/overlays/duplicate.rs | 2 +- moss/src/app/overlays/mod.rs | 15 +- moss/src/app/overlays/multiple.rs | 2 +- moss/src/app/overlays/overwrite.rs | 2 +- moss/src/app/settings.rs | 72 +++- moss/src/app/tools/vmparams.rs | 3 +- moss/src/entrypoint.rs | 35 +- moss/src/main.rs | 5 +- moss/src/util.rs | 73 ++-- types/src/array_set.rs | 6 +- 28 files changed, 877 insertions(+), 508 deletions(-) create mode 100644 moss/src/app/controllers/async_controller.rs create mode 100644 moss/src/app/mod_entry/version_checker.rs diff --git a/Cargo.lock b/Cargo.lock index cc1b3ad0..21ad0452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3135,6 +3135,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-stream", "tokio-util", "types", "unrar", @@ -5420,6 +5421,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.12" diff --git a/common/src/fast_im_map.rs b/common/src/fast_im_map.rs index 276ea140..d82e5638 100644 --- a/common/src/fast_im_map.rs +++ b/common/src/fast_im_map.rs @@ -84,14 +84,11 @@ impl From> } } -impl>> From - for FastImMap +impl + From> for FastImMap { - fn from(other: O) -> Self { - let mut new = Self::new(); - new.extend(other.into().iter().map(|(k, v)| (k.clone(), v.clone()))); - - new + fn from(other: druid::im::HashMap) -> Self { + Self(other) } } diff --git a/installer/src/error.rs b/installer/src/error.rs index f34decb3..eed477d0 100644 --- a/installer/src/error.rs +++ b/installer/src/error.rs @@ -27,7 +27,7 @@ pub enum InstallError { #[error("Failed to join task/thread: {0:?}")] Join(#[from] tokio::task::JoinError), #[error("Multiple errors")] - MultipleErrors(Vec>), + MultipleErrors(Vec), #[error(transparent)] Generic(#[from] anyhow::Error), } diff --git a/installer/src/installer.rs b/installer/src/installer.rs index edcabbba..24977db0 100644 --- a/installer/src/installer.rs +++ b/installer/src/installer.rs @@ -26,6 +26,8 @@ pub enum Request { Download { remote_data: U, old_path: PathBuf }, } +pub struct EnrichedEntry(pub T); + type InstallerError = InstallError<::Entry>; pub trait InstallerExt: InstallerDelegate @@ -146,10 +148,10 @@ where move_or_copy(path.clone(), destination.clone()).await; entry - .enrich(destination) + .enrich(self.context(), destination) .await .map_err(InstallError::EntryEnrichmentError)?; - self.completed_handler(entry); + self.completed_handler(EnrichedEntry(entry)); } } else { Err(InstallError::ModSearchErrorUnknown)?; @@ -174,11 +176,11 @@ where move_or_copy(origin, old_path.clone()).await; entry - .enrich(old_path) + .enrich(self.context(), old_path) .await .map_err(InstallError::EntryEnrichmentError)?; - self.completed_handler(entry); + self.completed_handler(EnrichedEntry(entry)); Ok(()) } diff --git a/installer/src/lib.rs b/installer/src/lib.rs index a915a478..bcc706c7 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -7,5 +7,5 @@ pub(crate) mod installer; mod traits; pub use error::InstallError; -pub use installer::{HybridPath, Request, StringOrPath}; +pub use installer::{EnrichedEntry, HybridPath, Request, StringOrPath}; pub use traits::{Entry, EntryUpdate, InstallerDelegate, InstallerExt}; diff --git a/installer/src/traits.rs b/installer/src/traits.rs index 07f898c2..bd7a203b 100644 --- a/installer/src/traits.rs +++ b/installer/src/traits.rs @@ -8,25 +8,27 @@ use std::{ use futures_util::TryFutureExt as _; pub use crate::installer::InstallerExt; -use crate::{HybridPath, InstallError, StringOrPath}; +use crate::{installer::EnrichedEntry, HybridPath, InstallError, StringOrPath}; pub trait Entry: Debug + Sized + Send + 'static { type Id: Into; type EnrichmentError: Error + Send + Sync; type ParseError: Error + Send + Sync; + type Context; fn id(&self) -> Self::Id; fn parse( path: impl AsRef + Send, - ) -> impl Future::ParseError>> + Send; + ) -> impl Future> + Send; fn destination_folder(&self, parent: &Path) -> PathBuf; fn enrich( &mut self, + context: &Self::Context, path: PathBuf, - ) -> impl Future::EnrichmentError>> + Send; + ) -> impl Future> + Send; } pub(crate) trait EntryExt: Entry { @@ -48,16 +50,19 @@ pub trait EntryUpdate: Send { } pub trait InstallerDelegate: Clone + Send + Sync { - type Entry: Entry; + type Context; + type Entry: Entry; type EntryUpdate: EntryUpdate; + fn context(&self) -> &Self::Context; + fn error_handler(&self, error: E); fn multiple_handler(&self, folder: HybridPath, found: Vec); fn overwrite_handler(&self, found: StringOrPath, folder: HybridPath, entry: Self::Entry); - fn completed_handler(&self, entry: Self::Entry); + fn completed_handler(&self, entry: EnrichedEntry); fn check_conflict(&self, entry: &Self::Entry) -> impl Future + Send + 'static; } diff --git a/moss/Cargo.toml b/moss/Cargo.toml index 57aeb918..e80a8ab2 100644 --- a/moss/Cargo.toml +++ b/moss/Cargo.toml @@ -100,6 +100,7 @@ mimalloc = { workspace = true } dhat = { version = "0.3", optional = true } uuid = "1.11.0" +tokio-stream = { version = "0.1.17", features = ["full"] } [target.'cfg(windows)'.dependencies] indoc = { workspace = true } diff --git a/moss/src/app.rs b/moss/src/app.rs index f2e3aede..fa261b20 100644 --- a/moss/src/app.rs +++ b/moss/src/app.rs @@ -4,14 +4,13 @@ use common::{ controllers::{HoverController, REMOVE_POINTER}, fast_im_map::FastImMap, labels::bold_text, - lenses::LensExtExt as _, widget_ext::{WidgetExtEx as _, HOVER_STATE_CHANGE}, widgets::root_stack::RootStack, }; use druid::{ im::{HashSet, Vector}, widget::{Flex, WidgetWrapper, ZStack}, - Data, Lens, LensExt, Selector, SingleUse, Widget, WidgetExt, WidgetId, + Data, Lens, Selector, SingleUse, Widget, WidgetExt, WidgetId, }; use druid_patch::{ tabs::tab::{InitialTab, Tabs, TabsPolicy, TabsTransition}, @@ -30,10 +29,10 @@ use webview::PROJECT; use crate::{ app::{ browser::Browser, - controllers::{AppController, ModListController}, + controllers::{AppController, AsyncController, ModListController}, installer_impl::{AsyncError, Installer}, mod_description::{ModDescription, ENABLE_DEPENDENCIES}, - mod_entry::{GameVersion, ModEntry, UpdateStatus, ViewModEntry}, + mod_entry::{GameVersion, ModEntry, ViewModEntry}, mod_list::ModList, mod_repo::ModRepo, overlays::Popup, @@ -101,7 +100,6 @@ impl App { Selector::new("app.mod_list.replace"); const RESTART: Selector = Selector::new("app.update.restart"); const SELECTOR: Selector = Selector::new("app.update.commands"); - const SELF_UPDATE: Selector<()> = Selector::new("app.update.perform"); const TOGGLE_NAV_BAR: Selector = Selector::new("app.nav_bar.collapse"); const OPEN_EXTERNALLY: Selector = Selector::new("app.user_browser.open"); @@ -299,25 +297,6 @@ impl App { }) }, ) - .on_command(util::MASTER_VERSION_RECEIVED, |_ctx, (id, res), data| { - let remote = res.as_ref().ok().cloned(); - let entry_lens = App::mod_list.then(ModList::mods).deref().index(id); - - if let Some(version_checker) = entry_lens - .clone() - .then(ModEntry::version_checker.in_rc()) - .get(data) - { - entry_lens - .clone() - .then(ModEntry::remote_version.in_rc()) - .put(data, remote.clone()); - - entry_lens - .then(ModEntry::update_status.in_rc()) - .put(data, Some(UpdateStatus::from((&version_checker, &remote)))); - } - }) .on_notification(ENABLE_DEPENDENCIES, |_, id, data| { ViewModEntry::enable_dependencies(id, data); }), @@ -351,6 +330,10 @@ pub impl + 'static> W { fn env_as_shared_data(self) -> impl Widget { self.env_scope(|env, data| env.set(crate::ENV_STATE, data)) } + + fn async_controller(self, runtime: Handle) -> impl Widget { + self.controller(AsyncController::new(runtime)) + } } #[derive(Debug, Clone, Hash)] diff --git a/moss/src/app/app_delegate.rs b/moss/src/app/app_delegate.rs index 00ea4fd1..3702be65 100644 --- a/moss/src/app/app_delegate.rs +++ b/moss/src/app/app_delegate.rs @@ -199,7 +199,7 @@ impl Delegate for AppDelegate { } } else if let Some(entry) = cmd.get(App::CONFIRM_DELETE_MOD) { if remove_dir_all(&entry.path).is_ok() { - data.mod_list.mods.remove(&entry.id); + data.mod_list.mods.remove(&entry.mod_id); data.active = None; } else { eprintln!("Failed to delete mod"); diff --git a/moss/src/app/controllers/app_controller.rs b/moss/src/app/controllers/app_controller.rs index b78c1a13..87b52694 100644 --- a/moss/src/app/controllers/app_controller.rs +++ b/moss/src/app/controllers/app_controller.rs @@ -8,7 +8,7 @@ use itertools::Itertools; use crate::{ app::{ installer_impl::{InstallMessage, INSTALL}, - mod_entry::UpdateStatus, + mod_entry::ModEntry, mod_list::ModList, settings::{self, Settings, SettingsCommand}, App, @@ -72,15 +72,8 @@ impl> Controller for AppController { match payload { InstallMessage::Success(entry) => { let mut entry = entry.clone(); - if let Some(existing) = data.mod_list.mods.get(&entry.id) { + if let Some(existing) = data.mod_list.mods.get(&entry.mod_id) { entry.enabled = existing.enabled; - if let Some(remote_version_checker) = existing.remote_version.clone() { - entry.remote_version = Some(remote_version_checker.clone()); - entry.update_status = Some(UpdateStatus::from(( - entry.version_checker.as_ref().unwrap(), - &Some(remote_version_checker), - ))); - } } ctx.submit_command(ModList::INSERT_MOD.with(*entry)); ctx.request_update(); @@ -110,6 +103,8 @@ impl> Controller for AppController { .inspect_err(|err| bang!(err)); } } + } else if let Some(()) = cmd.get(ModEntry::VERSION_CHECK_COMPLETE) { + ctx.request_update(); } } else if let Event::MouseDown(_) = event { if ctx.is_disabled() { diff --git a/moss/src/app/controllers/async_controller.rs b/moss/src/app/controllers/async_controller.rs new file mode 100644 index 00000000..e254f4cc --- /dev/null +++ b/moss/src/app/controllers/async_controller.rs @@ -0,0 +1,223 @@ +use std::{collections::LinkedList, future::Future, sync::OnceLock, time::Duration}; + +use common::ExtEventSinkExt; +use druid::{ + widget::Controller, Env, Event, EventCtx, ExtEventSink, Selector, SingleUse, TimerToken, Widget, +}; +use strum_macros::EnumDiscriminants; +use tokio::{runtime::Handle, sync::oneshot}; + +use crate::{app::App, bang, match_command}; + +pub static GLOBAL_ASYNC_CONTROLLER: AsyncCoordinator = AsyncCoordinator::new(); + +#[derive(derive_more::Deref)] +pub struct AsyncCoordinator(OnceLock); + +impl AsyncCoordinator { + pub const fn new() -> Self { + Self(OnceLock::new()) + } + + pub fn get_unchecked(&self) -> &AsyncCoordinatorImpl { + self.get().unwrap() + } +} + +#[derive(EnumDiscriminants)] +#[strum_discriminants(name(Status))] +enum TypedStatus { + Pending(oneshot::Receiver), + Blocked(T), + Complete(T), + Failed, +} + +trait FutureHandle { + fn progress(&mut self, ctx: &mut EventCtx, data: &App, env: &Env) -> Status; + + fn apply_result(self: Box, ctx: &mut EventCtx, app: &mut App, env: &Env); +} + +pub struct AsyncCoordinatorImpl { + runtime: Handle, + ext_ctx: ExtEventSink, +} + +impl AsyncCoordinatorImpl { + const NEW_TASK: Selector>> = + Selector::new("async_controller.task.new"); + + pub fn new(runtime: Handle, ext_ctx: ExtEventSink) -> Self { + AsyncCoordinatorImpl { runtime, ext_ctx } + } + + pub fn add_task( + &self, + task: impl Future + Send + 'static, + handler: impl FnOnce(T, &mut EventCtx, &mut App, &Env) + Send + Sync + 'static, + ) { + self.add_task_with_optional_check( + task, + Option:: bool>::None, + handler, + ); + } + + pub fn add_task_with_check( + &self, + task: impl Future + Send + 'static, + progress_handler: impl Fn(&T, &mut EventCtx, &App, &Env) -> bool + Send + Sync + 'static, + result_handler: impl FnOnce(T, &mut EventCtx, &mut App, &Env) + Send + Sync + 'static, + ) { + self.add_task_with_optional_check(task, Some(progress_handler), result_handler); + } + + pub fn add_task_with_optional_check( + &self, + task: impl Future + Send + 'static, + progress_handler: Option< + impl Fn(&T, &mut EventCtx, &App, &Env) -> bool + Send + Sync + 'static, + >, + result_handler: impl FnOnce(T, &mut EventCtx, &mut App, &Env) + Send + Sync + 'static, + ) { + let (tx, rx) = oneshot::channel(); + + self.runtime.spawn(async move { + let res = task.await; + let _ = tx.send(res); + }); + + let handle: Box = Box::new(TypedFutureHandle { + status: TypedStatus::Pending(rx), + progress_handler, + result_handler, + }); + + let _ = self + .ext_ctx + .submit_command_global(AsyncCoordinatorImpl::NEW_TASK, SingleUse::new(handle)) + .inspect_err(|err| bang!(err)); + } +} + +pub struct AsyncController { + handles: LinkedList>, + deadline: Option, + runtime: Handle, +} + +impl AsyncController { + pub fn new(runtime: Handle) -> Self { + Self { + handles: LinkedList::new(), + deadline: None, + runtime, + } + } + + fn update_deadline(&mut self, ctx: &mut EventCtx) { + self.deadline = Some(ctx.request_timer(std::time::Duration::from_millis(50))) + } +} + +impl> Controller for AsyncController { + fn event(&mut self, _: &mut W, ctx: &mut EventCtx, event: &Event, app: &mut App, env: &Env) { + if let Event::Timer(token) = event + && let Some(requested) = self.deadline.as_ref() + && token == requested + { + let mut handles = self.handles.cursor_front_mut(); + let work = async { + while let Some(handle) = handles.current() { + match handle.progress(ctx, &app, env) { + Status::Complete => { + let completed = handles + .remove_current() + .expect("Must be `Some` if entered loop"); + completed.apply_result(ctx, app, env); + } + Status::Failed => { + handles.remove_current(); + bang!("Task channel closed") + } + Status::Pending | Status::Blocked => {} + } + + tokio::task::yield_now().await + } + }; + + if let Err(_) = self + .runtime + .block_on(tokio::time::timeout(Duration::from_millis(25), work)) + { + bang!("Didn't process all tasks in time") + } + + if !self.handles.is_empty() { + self.update_deadline(ctx); + } + } + if let Event::Command(cmd) = event { + match_command!(cmd, () => { + AsyncCoordinatorImpl::NEW_TASK(task) => { + let task = task.take().unwrap(); + self.handles.push_back(task); + if self.deadline.is_none() { + self.update_deadline(ctx); + } + } + }) + } + } +} + +struct TypedFutureHandle { + status: TypedStatus, + progress_handler: FP, + result_handler: FR, +} + +impl TypedFutureHandle bool>, FR> { + pub fn new(rx: oneshot::Receiver, result_handler: FR) -> Self { + Self { + status: TypedStatus::Pending(rx), + progress_handler: None, + result_handler, + } + } +} + +impl< + T, + FP: Fn(&T, &mut EventCtx, &App, &Env) -> bool, + FR: FnOnce(T, &mut EventCtx, &mut App, &Env), + > FutureHandle for TypedFutureHandle, FR> +{ + fn progress(&mut self, ctx: &mut EventCtx, data: &App, env: &Env) -> Status { + if let TypedStatus::Pending(ref mut rx) = &mut self.status { + match rx.try_recv() { + Ok(res) => { + if let Some(progress_handler) = &self.progress_handler { + if !(progress_handler)(&res, ctx, data, env) { + self.status = TypedStatus::Blocked(res) + } + } else { + self.status = TypedStatus::Complete(res); + } + } + Err(oneshot::error::TryRecvError::Closed) => self.status = TypedStatus::Failed, + Err(oneshot::error::TryRecvError::Empty) => {} + } + }; + + (&self.status).into() + } + + fn apply_result(self: Box, ctx: &mut EventCtx, app: &mut App, env: &Env) { + if let TypedStatus::Complete(res) = self.status { + (self.result_handler)(res, ctx, app, env) + } + } +} diff --git a/moss/src/app/controllers/mod.rs b/moss/src/app/controllers/mod.rs index a105705e..74289fff 100644 --- a/moss/src/app/controllers/mod.rs +++ b/moss/src/app/controllers/mod.rs @@ -1,5 +1,7 @@ mod app_controller; +mod async_controller; mod mod_list_controller; pub use app_controller::*; +pub use async_controller::*; pub use mod_list_controller::ModListController; diff --git a/moss/src/app/installer_impl.rs b/moss/src/app/installer_impl.rs index d8df6617..a77eba38 100644 --- a/moss/src/app/installer_impl.rs +++ b/moss/src/app/installer_impl.rs @@ -2,8 +2,9 @@ use std::{future::Future, path::PathBuf, sync::Arc}; use common::ExtEventSinkExt; use druid::{ExtEventSink, Selector, SingleUse}; -use installer::{Entry, HybridPath, InstallerDelegate, InstallerExt, Request}; +use installer::{EnrichedEntry, Entry, HybridPath, InstallerDelegate, InstallerExt, Request}; use tokio::sync::oneshot::Sender; +use web_client::WebClient; use super::{mod_entry::ModVersionMeta, overlays::Popup}; use crate::{app::mod_entry::ModEntry, bang}; @@ -27,12 +28,16 @@ pub enum InstallMessage { #[derive(Clone)] pub struct Installer { - ext_ctx: ExtEventSink, + pub ext_ctx: ExtEventSink, + pub client: Arc, } impl Installer { pub fn new(ext_ctx: ExtEventSink) -> Self { - Self { ext_ctx } + Self { + ext_ctx, + client: Arc::new(WebClient::new()), + } } pub fn install( @@ -49,9 +54,14 @@ impl Installer { pub trait AsyncError = std::error::Error + Send + Sync + 'static; impl InstallerDelegate for Installer { + type Context = Arc; type Entry = ModEntry; type EntryUpdate = ModVersionMeta; + fn context(&self) -> &Self::Context { + &self.client + } + fn error_handler(&self, error: E) { let _ = self .ext_ctx @@ -76,7 +86,7 @@ impl InstallerDelegate for Installer { .inspect_err(|err| bang!(err)); } - fn completed_handler(&self, entry: ModEntry) { + fn completed_handler(&self, EnrichedEntry(entry): EnrichedEntry) { let _ = self .ext_ctx .submit_command_global( diff --git a/moss/src/app/mod_description.rs b/moss/src/app/mod_description.rs index 19b0a340..30620f34 100644 --- a/moss/src/app/mod_description.rs +++ b/moss/src/app/mod_description.rs @@ -26,7 +26,7 @@ use itertools::Itertools; use crate::{ app::{ app_delegate::AppCommands, - mod_entry::{ModMetadata, UpdateStatus, VersionComplex}, + mod_entry::{version_checker::UpdateStatus, ModMetadata, VersionChecker, VersionComplex}, overlays::Popup, util::FnWidgetToMaybe, App, ViewModEntry as ModEntry, INFO, @@ -49,8 +49,8 @@ pub struct ModDescription> { impl ModDescription { pub fn from_entry(entry: &ModEntry) -> Self { Self { - entry: entry.id.clone(), - crumbs: vector![(entry.name.clone(), entry.id.clone())], + entry: entry.mod_id.clone(), + crumbs: vector![(entry.name.clone(), entry.mod_id.clone())], } } } @@ -88,7 +88,10 @@ impl ModDescription { }, |app, entry| { if let Some(desc) = entry { - app.mod_list.mods.insert(desc.entry.id.clone(), desc.entry); + app + .mod_list + .mods + .insert(desc.entry.mod_id.clone(), desc.entry); } }, )) @@ -128,9 +131,9 @@ impl ModDescription { } else { Vector::new() }; - crumbs.push_back((notif_entry.name.clone(), notif_entry.id.clone())); + crumbs.push_back((notif_entry.name.clone(), notif_entry.mod_id.clone())); app.active = Some(ModDescription { - entry: notif_entry.id.clone(), + entry: notif_entry.mod_id.clone(), crumbs, }); } @@ -263,11 +266,12 @@ impl ModDescription { .with_child(Label::stringify_wrapped()) .with_default_spacer() .cross_axis_alignment(druid::widget::CrossAxisAlignment::Start) + .lens(VersionChecker::update_status) }) - .lens(ModEntry::update_status.compute(|s| { + .lens(ModEntry::version_checker.compute(|s| { s.clone().filter(|s| { matches!( - s, + s.update_status, UpdateStatus::Major(_) | UpdateStatus::Minor(_) | UpdateStatus::Patch(_) @@ -521,19 +525,19 @@ fn entry_controls() -> Flex { .fix_height(42.0) .padding((0.0, 2.0)) .empty_if_not(|data: &ModEntry, _| { - data - .remote_version - .as_ref() - .is_some_and(|r| r.direct_download_url.is_some()) - && data.update_status.as_ref().is_some_and(|s| { + data.get_direct_download_url().is_some() + && data.version_checker.as_ref().is_some_and(|s| { matches!( - s, + s.update_status, UpdateStatus::Major(_) | UpdateStatus::Minor(_) | UpdateStatus::Patch(_) ) }) }) .on_click(|ctx, data, _| { - ctx.submit_command(Popup::OPEN_POPUP.with(Popup::remote_update(data))); + if let Some(remote_version) = data.version_checker.as_ref().and_then(|vc| vc.get_remote()) + { + ctx.submit_command(Popup::OPEN_POPUP.with(Popup::remote_update(data, &remote_version))); + } }) .disabled_if(|data, _| data.view_state.updating), ) @@ -570,7 +574,7 @@ fn id() -> Flex { Flex::row() .with_spacer(5.0) .with_child(h3_fixed("id: ")) - .with_child(h3().lens(ModEntry::id)) + .with_child(h3().lens(ModEntry::mod_id)) .padding((0.0, 4.5, 0.0, 0.0)), ) } @@ -582,6 +586,6 @@ pub fn notify_enabled( _: &druid::Env, ) { if data.enabled { - ctx.submit_notification(ENABLE_DEPENDENCIES.with(data.id.clone())); + ctx.submit_notification(ENABLE_DEPENDENCIES.with(data.mod_id.clone())); } } diff --git a/moss/src/app/mod_entry.rs b/moss/src/app/mod_entry.rs index 4d1fee32..71acdb5d 100644 --- a/moss/src/app/mod_entry.rs +++ b/moss/src/app/mod_entry.rs @@ -7,7 +7,7 @@ use std::{ io::{BufRead, BufReader}, path::{Path, PathBuf}, rc::Rc, - sync::LazyLock, + sync::{Arc, LazyLock}, }; use ahash::AHashMap; @@ -34,14 +34,20 @@ use json_comments::StripComments; use serde::{Deserialize, Serialize}; use serde_aux::prelude::*; use tokio::io::AsyncWriteExt; +use uuid::Uuid; + +pub mod version_checker; + +pub use version_checker::{ModVersionMeta, UpdateStatus, VersionChecker}; use web_client::WebClient; use crate::{ app::{ app_delegate::AppCommands, + controllers::GLOBAL_ASYNC_CONTROLLER, mod_description::{notify_enabled, ModDescription}, mod_list::{headings::Heading, ModList, ModMap}, - util::{self, default_true, get_master_version, parse_game_version, Tap}, + util::{self, default_true, parse_game_version, Tap}, App, SharedFromEnv, }, nav_bar::{Nav, NavLabel}, @@ -61,7 +67,12 @@ pub type GameVersion = ( #[derive(Debug, Clone, Deserialize, Data, Lens, Default, Dummy)] pub struct ModEntry { - pub id: String, + #[serde(alias = "id")] + pub mod_id: String, + #[serde(skip)] + #[data(eq)] + #[dummy(default)] + pub internal_id: Uuid, pub name: String, #[serde(default)] pub author: Option, @@ -76,7 +87,7 @@ pub struct ModEntry { pub utility: bool, #[data(eq)] #[serde(deserialize_with = "ModEntry::deserialize_dependencies", default)] - pub dependencies: std::sync::Arc>, + pub dependencies: Arc>, #[serde( alias = "totalConversion", default, @@ -86,13 +97,10 @@ pub struct ModEntry { #[serde(skip)] pub enabled: bool, #[serde(skip)] - pub version_checker: Option, + #[dummy(default)] + pub version_checker: Option, #[serde(skip)] - pub remote_version: Option, - #[serde(skip)] - pub update_status: Option, - #[serde(skip)] - #[data(same_fn = "PartialEq::eq")] + #[data(eq)] pub path: PathBuf, #[serde(skip)] #[serde(default = "default_true")] @@ -102,6 +110,9 @@ pub struct ModEntry { #[serde(skip, default)] #[data(ignore)] pub view_state: T, + + #[serde(skip, default)] + pub duplicates: Arc>>, } #[derive(Debug, Clone, PartialEq, Data, Deserialize, Dummy)] @@ -157,7 +168,7 @@ pub type ViewModEntry = ModEntry; impl ModEntry { pub fn fractal_link() -> impl Lens> { Self::version_checker.compute(|v| { - v.as_ref().map(|v| &v.fractal_id).and_then(|s| { + v.as_ref().map(|v| &v.local.fractal_id).and_then(|s| { (!s.is_empty()).then(|| format!("{}{}", ModDescription::FRACTAL_URL, s.clone())) }) }) @@ -165,7 +176,7 @@ impl ModEntry { pub fn nexus_link() -> impl Lens> { Self::version_checker.compute(|v| { - v.as_ref().map(|v| &v.nexus_id).and_then(|s| { + v.as_ref().map(|v| &v.local.nexus_id).and_then(|s| { (!s.is_empty()).then(|| format!("{}{}", ModDescription::NEXUS_URL, s.clone())) }) }) @@ -249,6 +260,59 @@ impl ModEntry { true } + + pub fn spawn_version_check(&self, client: &Arc) { + if let Some(version_checker) = self.version_checker.as_ref() { + let mod_id = self.mod_id.clone(); + let internal_id = self.internal_id; + GLOBAL_ASYNC_CONTROLLER.get_unchecked().add_task_with_check( + util::get_master_version(&client, version_checker.local.remote_url.clone()), + { + let mod_id = mod_id.clone(); + move |_, _, app, _| { + app + .mod_list + .mods + .get(&mod_id) + .into_iter() + .flat_map(|entry| entry.iter_with_dupes()) + .any(|entry| entry.internal_id == internal_id) + } + }, + move |res, _, app, _| { + if let Some(entry) = app.mod_list.mods.get_mut(&mod_id) + && let entry = Rc::make_mut(entry) + && entry.internal_id == internal_id + && let Some(version_checker) = entry.version_checker.as_mut() + { + version_checker.update_remote(res.ok()); + } + }, + ); + } + } + + pub fn get_direct_download_url(&self) -> Option<&str> { + self + .version_checker + .as_ref() + .and_then(|vc| vc.get_direct_download_url()) + } + + pub fn iter_with_dupes(&self) -> impl Iterator { + std::iter::once(self).chain(self.duplicates.iter()) + } + + pub fn iter_with_dupes_mut(&mut self, mut func: impl FnMut(&mut Self)) + where + T: Clone, + { + (func)(self); + let duplicates = Arc::make_mut(&mut self.duplicates); + for dupe in duplicates { + (func)(dupe) + } + } } impl ModEntry { @@ -256,7 +320,7 @@ impl ModEntry { let mod_info_file = std::fs::read_to_string(path.join("mod_info.json"))?; let stripped = std::io::read_to_string(StripComments::new(mod_info_file.as_bytes()))?; let mut mod_info = json5::from_str::(&stripped)?; - mod_info.version_checker = ModEntry::parse_version_checker(path, &mod_info.id); + mod_info.version_checker = ModEntry::parse_version_checker(path, &mod_info.mod_id); mod_info.path = path.to_path_buf(); mod_info.manager_metadata = manager_metadata; Ok(mod_info) @@ -267,8 +331,9 @@ impl ModEntry { pub const ASK_DELETE_MOD: Selector = Selector::new("mod_entry.delete"); pub const AUTO_UPDATE: Selector = Selector::new("mod_list.update.auto"); pub const REPLACE: Selector = Selector::new("MOD_ENTRY_REPLACE"); + pub const VERSION_CHECK_COMPLETE: Selector = Selector::new("mod_entry.version_check.complete"); - fn parse_version_checker(path: &Path, id: &str) -> Option { + fn parse_version_checker(path: &Path, id: &str) -> Option { static VC_LOCATION_PATH: LazyLock<&'static Path> = LazyLock::new(|| Path::new("data/config/version/version_files.csv")); @@ -281,7 +346,7 @@ impl ModEntry { && let Ok(mut version) = json5::from_str::(&normalized) { version.id = id.to_string(); - Some(version) + Some(VersionChecker::new(version)) } else { None } @@ -296,9 +361,7 @@ impl ModEntry { Ok(parse_game_version(&buf)) } - fn deserialize_dependencies<'de, D>( - deserializer: D, - ) -> Result>, D::Error> + fn deserialize_dependencies<'de, D>(deserializer: D) -> Result>, D::Error> where D: serde::Deserializer<'de>, { @@ -311,7 +374,7 @@ impl ModEntry { let dependencies = Vec::::deserialize(deserializer)?; - Ok(std::sync::Arc::new( + Ok(Arc::new( dependencies .into_iter() .filter_map(|RawDependency { id, name, version }| { @@ -326,9 +389,10 @@ impl installer::Entry for ModEntry { type Id = String; type ParseError = ModEntryError; type EnrichmentError = ModEntryError; + type Context = Arc; fn id(&self) -> Self::Id { - self.id.clone() + self.mod_id.clone() } fn destination_folder(&self, parent: &Path) -> PathBuf { @@ -342,21 +406,11 @@ impl installer::Entry for ModEntry { ModEntry::from_file(path, metadata) } - async fn enrich(&mut self, path: PathBuf) -> Result<(), ModEntryError> { + async fn enrich(&mut self, context: &Self::Context, path: PathBuf) -> Result<(), ModEntryError> { self.manager_metadata.save(&path).await?; self.set_path(path); - if let Some(version_checker) = self.version_checker.as_ref() { - let client = WebClient::new(); - self.remote_version = get_master_version( - &client, - None, - version_checker.remote_url.clone(), - version_checker.id.clone(), - ) - .await; - self.update_status = Some(UpdateStatus::from((version_checker, &self.remote_version))); - } + self.spawn_version_check(context); Ok(()) } @@ -380,7 +434,7 @@ impl ViewModEntry { header @ (Heading::ID | Heading::Name | Heading::Author) => { let label = Label::wrapped_func(|text: &String, _| text.to_string()); match header { - Heading::ID => label.lens(ViewModEntry::id).padding(5.).expand_width(), + Heading::ID => label.lens(ViewModEntry::mod_id).padding(5.).expand_width(), Heading::Name => label.lens(ViewModEntry::name).padding(5.).expand_width(), Heading::Author => label .lens( @@ -402,26 +456,20 @@ impl ViewModEntry { .boxed(), Heading::Version => ViewModEntry::version_cell(), Heading::AutoUpdateSupport => Either::new( - |entry: &ViewModEntry, _| { - entry - .remote_version - .as_ref() - .and_then(|r| r.direct_download_url.as_ref()) - .is_some() - }, + |entry: &ViewModEntry, _| entry.get_direct_download_url().is_some(), Either::new( |entry: &ViewModEntry, _| { entry - .update_status + .version_checker .as_ref() - .is_some_and(|status| status != &UpdateStatus::Error) + .is_some_and(|vc| vc.update_status != UpdateStatus::Error) }, Either::new( |entry: &ViewModEntry, _| { - entry.update_status.as_ref().is_some_and(|status| { + entry.version_checker.as_ref().is_some_and(|vc| { !matches!( - status, - &UpdateStatus::UpToDate | &UpdateStatus::Discrepancy(_) + vc.update_status, + UpdateStatus::UpToDate | UpdateStatus::Discrepancy(_) ) }) }, @@ -558,10 +606,10 @@ impl ViewModEntry { ), Label::dynamic(|data: &(Option, Version), _| data.1.to_string()), ) - .lens( - lens::Identity - .compute(|entry: &ViewModEntry| (entry.update_status.clone(), entry.version.clone())), - ) + .lens(( + ViewModEntry::version_checker.compute(|vc| vc.as_ref().map(|vc| vc.update_status.clone())), + ViewModEntry::version, + )) .padding(5.) .expand_width() .boxed() @@ -604,7 +652,7 @@ impl ViewModEntry { impl Hash for ModEntry { fn hash(&self, state: &mut H) { - self.id.hash(state); + self.mod_id.hash(state); self.name.hash(state); self.author.hash(state); self.version.hash(state); @@ -612,8 +660,6 @@ impl Hash for ModEntry { self.game_version.hash(state); self.enabled.hash(state); self.version_checker.hash(state); - self.remote_version.hash(state); - self.update_status.hash(state); self.path.hash(state); self.manager_metadata.hash(state); } @@ -621,7 +667,7 @@ impl Hash for ModEntry { impl PartialEq for ModEntry { fn eq(&self, other: &Self) -> bool { - self.id == other.id + self.mod_id == other.mod_id && self.name == other.name && self.author == other.author && self.version == other.version @@ -629,8 +675,6 @@ impl PartialEq for ModEntry { && self.game_version == other.game_version && self.enabled == other.enabled && self.version_checker == other.version_checker - && self.remote_version == other.remote_version - && self.update_status == other.update_status && self.path == other.path && self.display == other.display && self.manager_metadata == other.manager_metadata @@ -642,7 +686,8 @@ impl Eq for ModEntry {} impl From for ViewModEntry { fn from( ModEntry { - id, + mod_id, + internal_id, name, author, version, @@ -653,16 +698,16 @@ impl From for ViewModEntry { total_conversion, enabled, version_checker, - remote_version, - update_status, path, display, manager_metadata, view_state: (), + duplicates, }: ModEntry, ) -> Self { ViewModEntry { - id, + mod_id, + internal_id, name, author, version, @@ -673,26 +718,25 @@ impl From for ViewModEntry { total_conversion, enabled, version_checker, - remote_version, - update_status, path, display, manager_metadata, view_state: ViewState::new(), + duplicates: Arc::new( + Arc::unwrap_or_clone(duplicates) + .into_iter() + .map(|dup| dup.into()) + .collect(), + ), } } } -impl<'a> From<&'a ModEntry> for ViewModEntry { - fn from(value: &'a ModEntry) -> Self { - value.clone().into() - } -} - impl From for ModEntry { fn from( ViewModEntry { - id, + mod_id, + internal_id, name, author, version, @@ -703,16 +747,16 @@ impl From for ModEntry { total_conversion, enabled, version_checker, - remote_version, - update_status, path, display, manager_metadata, view_state: _, + duplicates, }: ViewModEntry, ) -> Self { ModEntry { - id, + mod_id, + internal_id, name, author, version, @@ -723,12 +767,16 @@ impl From for ModEntry { total_conversion, enabled, version_checker, - remote_version, - update_status, path, display, manager_metadata, view_state: (), + duplicates: Arc::new( + Arc::unwrap_or_clone(duplicates) + .into_iter() + .map(|dup| dup.into()) + .collect(), + ), } } } @@ -744,7 +792,7 @@ impl RowData for ViewModEntry { type Id = String; fn id(&self) -> String { - self.id.clone() + self.mod_id.clone() } fn cell(&self, column: &Self::Column) -> Box> { @@ -800,58 +848,8 @@ pub enum ModEntryError { JsonError(#[from] json5::Error), #[error("I/O error")] IoError(#[from] std::io::Error), -} - -#[allow(clippy::derived_hash_with_manual_eq)] -#[derive(Debug, Clone, Deserialize, Eq, Data, Lens, Hash, Dummy)] -pub struct ModVersionMeta { - #[serde(alias = "masterVersionFile")] - pub remote_url: String, - #[serde(alias = "directDownloadURL")] - #[serde(default)] - pub direct_download_url: Option, - #[serde(alias = "modName")] - pub id: String, - #[serde(alias = "modThreadId")] - #[serde(deserialize_with = "deserialize_string_from_number")] - #[serde(default)] - pub fractal_id: String, - #[serde(alias = "modNexusId")] - #[serde(deserialize_with = "deserialize_string_from_number")] - #[serde(default)] - pub nexus_id: String, - #[serde(alias = "modVersion")] - pub version: VersionComplex, -} - -impl installer::EntryUpdate for ModVersionMeta { - type Entry = ModEntry; - - fn url(&self) -> String { - self.direct_download_url.clone().unwrap() - } - - fn matches(&self, entry: &ModEntry) -> bool { - entry.version_checker.as_ref().unwrap().version == self.version - } -} - -impl PartialEq for ModVersionMeta { - fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.version == other.version - } -} - -impl PartialOrd for ModVersionMeta { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.version.cmp(&other.version)) - } -} - -impl Ord for ModVersionMeta { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other).unwrap() - } + #[error("VC error: {0}")] + VersionCheckError(Arc), } #[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord, Data, Lens, Hash, Dummy)] @@ -883,16 +881,6 @@ impl Display for VersionComplex { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Data, Hash, Dummy)] -pub enum UpdateStatus { - Error, - UpToDate, - Discrepancy(VersionComplex), - Patch(VersionComplex), - Minor(VersionComplex), - Major(VersionComplex), -} - impl Display for UpdateStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match self { @@ -1047,7 +1035,7 @@ mod test { })), }; let dep_entry = ViewModEntry { - id: "Dep".to_owned(), + mod_id: "Dep".to_owned(), version: Version::Complex(VersionComplex { major: 1, minor: 0, @@ -1056,18 +1044,16 @@ mod test { ..Default::default() }; let entry = ViewModEntry { - id: "Entry".to_owned(), + mod_id: "Entry".to_owned(), dependencies: vec![dep].into(), ..Default::default() }; - let mut mods = ModMap::from( - [ - ("Dep".to_owned(), dep_entry.into()), - ("Entry".to_owned(), entry.into()), - ] - .as_slice(), - ); + let mut mods = ModMap::new(); + mods.extend([ + ("Dep".to_owned(), dep_entry.into()), + ("Entry".to_owned(), entry.into()), + ]); // Assert assert!(!mods["Entry"].enabled); @@ -1083,7 +1069,7 @@ mod test { fn enable_dependencies() { // Setup let sub_dep_entry = ViewModEntry { - id: "subdep".to_owned(), + mod_id: "subdep".to_owned(), version: Version::Complex(VersionComplex { major: 0, minor: 2, @@ -1092,7 +1078,7 @@ mod test { ..Default::default() }; let dep_entry = ViewModEntry { - id: "Dep".to_owned(), + mod_id: "Dep".to_owned(), version: Version::Complex(VersionComplex { major: 1, minor: 0, @@ -1111,7 +1097,7 @@ mod test { ..Default::default() }; let entry = ViewModEntry { - id: "Entry".to_owned(), + mod_id: "Entry".to_owned(), dependencies: vec![Dependency { id: "Dep".to_owned(), name: None, @@ -1126,24 +1112,22 @@ mod test { }; let unused_entry_a = ViewModEntry { - id: "Unused A".to_owned(), + mod_id: "Unused A".to_owned(), ..Default::default() }; let unused_entry_b = ViewModEntry { - id: "Unused B".to_owned(), + mod_id: "Unused B".to_owned(), ..Default::default() }; - let mut mods = ModMap::from( - [ - ("Dep".to_owned(), dep_entry.into()), - ("Entry".to_owned(), entry.into()), - ("subdep".to_owned(), sub_dep_entry.into()), - ("Unused A".to_owned(), unused_entry_a.into()), - ("Unused B".to_owned(), unused_entry_b.into()), - ] - .as_slice(), - ); + let mut mods = ModMap::new(); + mods.extend([ + ("Dep".to_owned(), dep_entry.into()), + ("Entry".to_owned(), entry.into()), + ("subdep".to_owned(), sub_dep_entry.into()), + ("Unused A".to_owned(), unused_entry_a.into()), + ("Unused B".to_owned(), unused_entry_b.into()), + ]); // Assert assert!(mods.values().all(|entry| !entry.enabled)); @@ -1160,7 +1144,7 @@ mod test { #[test] fn missing_dependency() { let entry = ViewModEntry { - id: "entry".to_owned(), + mod_id: "entry".to_owned(), dependencies: vec![Dependency { id: "doesn't exist".to_owned(), name: None, @@ -1170,7 +1154,8 @@ mod test { ..Default::default() }; - let mut mods = ModMap::from([("entry".to_owned(), entry.into())].as_slice()); + let mut mods = ModMap::new(); + mods.extend([("entry".to_owned(), entry.into())]); assert!(!ViewModEntry::enable_all_dependencies("entry", &mut mods)); assert!(!mods["entry"].enabled); diff --git a/moss/src/app/mod_entry/version_checker.rs b/moss/src/app/mod_entry/version_checker.rs new file mode 100644 index 00000000..38627460 --- /dev/null +++ b/moss/src/app/mod_entry/version_checker.rs @@ -0,0 +1,117 @@ +use std::{hash::Hash, sync::Arc}; + +use druid::{Data, Lens}; +use fake::Dummy; +use serde::Deserialize; +use serde_aux::prelude::deserialize_string_from_number; + +use crate::app::mod_entry::{ModEntry, VersionComplex}; + +pub(crate) type AsyncModVersionMetaRes = Result>; + +#[derive(Debug, Clone, Data, Lens, Hash, derive_more::Deref)] +pub struct VersionChecker { + #[deref] + pub local: ModVersionMeta, + remote: Option, + pub update_status: UpdateStatus, +} + +impl VersionChecker { + pub fn new(local: ModVersionMeta) -> Self { + Self { + local, + remote: None, + update_status: UpdateStatus::Error, + } + } + + pub fn update_remote(&mut self, remote: Option) { + self.remote = remote; + self.update_status = UpdateStatus::from((&self.local, &self.remote)) + } + + pub fn get_remote(&self) -> Option<&ModVersionMeta> { + self.remote.as_ref() + } + + pub fn get_direct_download_url(&self) -> Option<&str> { + self + .get_remote() + .and_then(|remote| remote.direct_download_url.as_deref()) + } +} + +impl PartialEq for VersionChecker { + fn eq(&self, other: &Self) -> bool { + self.local == other.local + && self.remote == other.remote + && self.update_status == other.update_status + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Data, Hash, Dummy)] +pub enum UpdateStatus { + Error, + UpToDate, + Discrepancy(VersionComplex), + Patch(VersionComplex), + Minor(VersionComplex), + Major(VersionComplex), +} + +#[allow(clippy::derived_hash_with_manual_eq)] +#[derive(Debug, Clone, Deserialize, Eq, Data, Lens, Hash, Dummy)] +pub struct ModVersionMeta { + #[serde(alias = "masterVersionFile")] + pub remote_url: String, + #[serde(alias = "directDownloadURL")] + #[serde(default)] + pub direct_download_url: Option, + #[serde(alias = "modName")] + pub id: String, + #[serde(alias = "modThreadId")] + #[serde(deserialize_with = "deserialize_string_from_number")] + #[serde(default)] + pub fractal_id: String, + #[serde(alias = "modNexusId")] + #[serde(deserialize_with = "deserialize_string_from_number")] + #[serde(default)] + pub nexus_id: String, + #[serde(alias = "modVersion")] + pub version: VersionComplex, +} + +impl installer::EntryUpdate for ModVersionMeta { + type Entry = ModEntry; + + fn url(&self) -> String { + self.direct_download_url.clone().unwrap() + } + + fn matches(&self, entry: &ModEntry) -> bool { + entry + .version_checker + .as_ref() + .map(|vc| vc.local.version == self.version) + .unwrap_or_default() + } +} + +impl PartialEq for ModVersionMeta { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.version == other.version + } +} + +impl PartialOrd for ModVersionMeta { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.version.cmp(&other.version)) + } +} + +impl Ord for ModVersionMeta { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} diff --git a/moss/src/app/mod_list.rs b/moss/src/app/mod_list.rs index c3a088c6..1d165bda 100644 --- a/moss/src/app/mod_list.rs +++ b/moss/src/app/mod_list.rs @@ -1,12 +1,15 @@ use std::{ - collections::HashMap, + cell::Cell, hash::Hash, + num::NonZero, ops::{Deref, Index}, path::{Path, PathBuf}, rc::Rc, sync::Arc, }; +use ahash::{HashSet, HashSetExt}; +use anyhow::Context; use comemo::memoize; use common::{ controllers::ExtensibleController, @@ -30,9 +33,9 @@ use druid_patch::table::{ use druid_widget_nursery::{ Stack, StackChildParams, StackChildPosition, WidgetExt as WidgetExtNursery, }; +use futures_util::{StreamExt, TryStreamExt}; use installer::HybridPath; -use rand::Rng; -use rayon::prelude::*; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumCount, EnumIter}; use sublime_fuzzy::best_match; @@ -40,15 +43,14 @@ use web_client::WebClient; use super::{ mod_entry::{ - GameVersion, ModEntry as RawModEntry, ModMetadata, ModVersionMeta, UpdateStatus, + version_checker::UpdateStatus, GameVersion, ModEntry as RawModEntry, ModMetadata, ViewModEntry as ModEntry, }, - util::{self, SaveError}, + util::SaveError, App, }; -use crate::app::{ - mod_list::actions::{action_button::ActionsButton, action_options::ActionsOptions, ActionsState}, - util::LoadBalancer, +use crate::app::mod_list::actions::{ + action_button::ActionsButton, action_options::ActionsOptions, ActionsState, }; mod actions; @@ -101,8 +103,9 @@ impl ModList { pub const UPDATE_COLUMN_WIDTH: Selector<(usize, f64)> = Selector::new("mod_list.column.update_width"); const UPDATE_TABLE_SORT: Selector = Selector::new("mod_list.table.update_sorting"); - const UPDATE_VERSION_CHECKER: Selector>> = - Selector::new("mod_list.mods.update_version_checker"); + // const UPDATE_VERSION_CHECKER: Selector< + // HashMap<(String, VersionComplex), Option>, + // > = Selector::new("mod_list.mods.update_version_checker"); pub fn new(headings: Vector) -> Self { Self { @@ -233,27 +236,10 @@ impl ModList { .on_command(Self::INSERT_MOD, |_, ctx, entry, data| { data .mods - .insert(entry.id.clone(), Rc::new(entry.clone().into())); + .insert(entry.mod_id.clone(), Rc::new(entry.clone().into())); ctx.request_update(); true }) - .on_command(Self::UPDATE_VERSION_CHECKER, |_, _, payload, data| { - for (id, remote_version) in payload { - if let Some(entry) = data.mods.get(id) { - let mut entry = entry.clone(); - let entry_ref = Rc::make_mut(&mut entry); - entry_ref.remote_version.clone_from(remote_version); - entry_ref.update_status = Some(UpdateStatus::from(( - entry_ref.version_checker.as_ref().unwrap(), - &entry_ref.remote_version, - ))); - - data.mods[id] = entry; - } - } - - true - }) .on_command(App::ENABLE, |_, ctx, _, _| { ctx.children_changed(); true @@ -407,7 +393,7 @@ impl ModList { .mod_list .mods .values() - .filter_map(|v| v.enabled.then_some(v.id.clone())) + .filter_map(|v| v.enabled.then_some(v.mod_id.clone())) .collect(); if let Err(err) = EnabledMods::from(enabled).save(install_dir) { eprintln!("{err:?}"); @@ -447,18 +433,8 @@ impl ModList { true } - pub fn parse_mod_folder( - root_dir: &Path, - ext_ctx: &ExtEventSink, - ) -> Result>)> { - type VersionCheckBalancer = LoadBalancer< - (String, Option), - HashMap>, - HashMap>, - >; - static BALANCER: VersionCheckBalancer = LoadBalancer::new(ModList::UPDATE_VERSION_CHECKER); - - let handle = tokio::runtime::Handle::current(); + pub async fn parse_mod_folder_inner(root_dir: &Path) -> anyhow::Result<(RawModMap, HashSet)> { + let client = Arc::new(WebClient::new()); let mod_dir = root_dir.join("mods"); let enabled_mods_filename = mod_dir.join("enabled_mods.json"); @@ -468,122 +444,95 @@ impl ModList { && let Ok(EnabledMods { enabled_mods }) = serde_json::from_str::(&enabled_mods_text) { - enabled_mods + Arc::new(enabled_mods) } else { - vec![] - }; - - let Ok(dir_iter) = std::fs::read_dir(mod_dir) else { - return Ok(FastImMap::default()); + Default::default() }; - let enabled_mods_iter = enabled_mods.par_iter(); - let client = Arc::new(WebClient::new()); - let barrier = Arc::new(tokio::sync::Semaphore::new(0)); - let mods = dir_iter - .par_bridge() - .filter_map(std::result::Result::ok) - .filter(|entry| { - if let Ok(file_type) = entry.file_type() { - file_type.is_dir() - } else { - false - } - }) - .filter_map( - |entry| match RawModEntry::from_file(&entry.path(), ModMetadata::default()) { - Ok(mut entry) => { - entry.set_enabled( - enabled_mods_iter - .clone() - .find_any(|id| entry.id.clone().eq(*id)) - .is_some(), - ); - - if let Some(version) = entry.version_checker.as_ref() { - let client = client.clone(); - let remote_url = version.remote_url.clone(); - let id = version.id.clone(); - let tx = { - let _handle = handle.enter(); - BALANCER.sender(ext_ctx.clone()) - }; - let barrier = barrier.clone(); - handle.spawn(async move { - let remote_version = - util::get_master_version(client.as_ref(), None, remote_url, id.clone()).await; - let _ = barrier.acquire().await; - let _ = tx.send((id, remote_version)); - }); - } - if ModMetadata::path(&entry.path).exists() { - if let Some(mod_metadata) = handle.block_on(ModMetadata::parse_and_send( - entry.id.clone(), - entry.path.clone(), - None, - )) { - entry.manager_metadata = mod_metadata; + tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(mod_dir).await?) + .map(|entry| { + let enabled_mods = enabled_mods.clone(); + let client = client.clone(); + tokio::spawn(async move { + let Ok(entry) = entry else { + return None; + }; + + if tokio::fs::metadata(entry.path()) + .await + .map(|meta| meta.is_dir()) + .unwrap_or_default() + { + match RawModEntry::from_file(&entry.path(), ModMetadata::default()) { + Ok(mut entry) => { + entry.set_enabled(enabled_mods.contains(&entry.mod_id)); + + entry.spawn_version_check(&client); + + if ModMetadata::path(&entry.path).exists() { + if let Some(mod_metadata) = + ModMetadata::parse_and_send(entry.mod_id.clone(), entry.path.clone(), None) + .await + { + entry.manager_metadata = mod_metadata; + } + } + Some(entry) + } + Err(err) => { + eprintln!("Failed to get mod info for mod at: {:?}", entry.path()); + eprintln!("With err: {err:?}"); + None } } - Some(entry) - } - Err(err) => { - eprintln!("Failed to get mod info for mod at: {:?}", entry.path()); - eprintln!("With err: {err:?}"); + } else { None } + }) + }) + .buffer_unordered( + std::thread::available_parallelism() + .map(NonZero::get) + .unwrap_or(8), + ) + .try_fold( + (FastImMap::new().inner(), HashSet::new()), + |(mods, mut dupes), entry| async move { + let Some(entry) = entry else { + return Ok((mods, dupes)); + }; + + Ok(( + mods.update_with(entry.mod_id.clone(), entry, |mut old, new| { + dupes.insert(old.mod_id.clone()); + + let dupes = Arc::make_mut(&mut old.duplicates); + dupes.push(new); + + old + }), + dupes, + )) }, ) - .collect::>(); - - let mut bucket_map: HashMap> = HashMap::new(); - - for entry in mods { - if let Some(bucket) = bucket_map.get_mut(&entry.id) { - bucket.push(entry); - } else { - bucket_map.insert(entry.id.clone(), vec![entry]); - } - } - - let (map, duplicates): (Vec<_>, _) = bucket_map - .into_iter() - .partition(|(_, bucket)| bucket.len() == 1); - - let mut out = FastImMap::new(); - *out = map - .into_iter() - .map(|(id, mut bucket)| (id, bucket.swap_remove(0))) - .collect(); - - if duplicates.is_empty() { - barrier.close(); - - Ok(out) - } else { - let duplicates = duplicates - .into_iter() - .map(|(_, bucket)| bucket) - .inspect(|bucket| { - let pick = bucket[rand::thread_rng().gen_range(0..bucket.len())].clone(); - out.insert(pick.id.clone(), pick); - }) - .collect(); - - barrier.close(); - - Err((out, duplicates)) - } + .await + .map(|(map, dupes)| (map.into(), dupes)) + .context("Join err") } pub async fn parse_mod_folder_async(root_dir: PathBuf, ext_ctx: ExtEventSink) { - let ext_ctx_tmp = ext_ctx.clone(); - let map = - tokio::task::spawn_blocking(move || Self::parse_mod_folder(&root_dir, &ext_ctx_tmp)).await; - - let (mods, duplicates) = match map { - Ok(Ok(mods)) => (mods, None), - Ok(Err((mods, duplicates))) => (mods, Some(duplicates)), + let map = Self::parse_mod_folder_inner(&root_dir).await; + + let (duplicates, mods) = match map { + Ok(mods) => ( + mods + .iter() + .filter_map(|(id, entry)| { + (!entry.duplicates.is_empty()).then(|| super::Popup::duplicate(id.clone())) + }) + .collect_vec(), + mods, + ), Err(err) => { eprintln!("{} | Failed to parse mod folder async: {err}", line!()); return; @@ -593,14 +542,8 @@ impl ModList { { eprintln!("{} | {err}", line!()); } - if let Some(duplicates) = duplicates { - let _ = ext_ctx.submit_command_global( - super::Popup::DELAYED_POPUP, - duplicates - .into_iter() - .map(|dupes| super::Popup::duplicate(dupes.into())) - .collect::>(), - ); + if !duplicates.is_empty() { + let _ = ext_ctx.submit_command_global(super::Popup::DELAYED_POPUP, duplicates); } } @@ -636,7 +579,7 @@ impl ModList { .filter_map(|entry| { let search = if let Heading::Score = header.sort_by.0 { search_text.is_empty() || { - let id_score = best_match(search_text, &entry.id).map(|m| m.score()); + let id_score = best_match(search_text, &entry.mod_id).map(|m| m.score()); let name_score = best_match(search_text, &entry.name).map(|m| m.score()); let author_score = best_match(search_text, entry.author.as_deref().unwrap_or_default()) .map(|m| m.score()); @@ -657,7 +600,7 @@ impl ModList { .any(|f| f.as_fn()(entry)); let passes_filters = matches_all && (filters.is_empty() || matches_any); - (search && passes_filters).then(|| entry.id.clone()) + (search && passes_filters).then(|| entry.mod_id.clone()) }) .collect(); @@ -668,7 +611,22 @@ impl ModList { &entry.$field }); }}; - ($ids:ident, $e:expr) => {{ + ($ids:ident, $i:ident $t:ty $e:block) => {{ + $ids.sort_unstable_by(|id, other| { + fn map($i: &ModEntry) -> $t $e + + let entry: &ModEntry = &mods[id]; + let other: &ModEntry = &mods[other]; + map(entry).cmp(&map(other)) + }) + }}; + ($ids:ident, key $e:expr) => {{ + $ids.sort_unstable_by_key(|id| { + let entry: &ModEntry = &mods[id]; + $e(entry) + }) + }}; + ($ids:ident, cached $e:expr) => {{ $ids.sort_by_cached_key(|id| { let entry: &ModEntry = &mods[id]; $e(entry) @@ -682,14 +640,11 @@ impl ModList { Heading::Author => sort!(ids, author), Heading::GameVersion => sort!(ids, game_version), Heading::Enabled => sort!(ids, enabled), - Heading::Version => sort!(ids, |entry: &ModEntry| { - entry - .update_status - .clone() - .ok_or_else(|| entry.name.clone()) + Heading::Version => sort!(ids, entry Result<&String, &String> { + entry.version_checker.as_ref().map_or_else(|| Ok(&entry.name), |_| Err(&entry.name)) }), - Heading::Score => sort!(ids, |entry: &ModEntry| { - let id_score = best_match(search_text, &entry.id).map(|m| m.score()); + Heading::Score => sort!(ids, cached |entry: &ModEntry| { + let id_score = best_match(search_text, &entry.mod_id).map(|m| m.score()); let name_score = best_match(search_text, &entry.name).map(|m| m.score()); let author_score = best_match(search_text, entry.author.as_deref().unwrap_or_default()).map(|m| m.score()); @@ -699,15 +654,13 @@ impl ModList { .max(author_score) .ok_or_else(|| entry.name.clone()) }), - Heading::AutoUpdateSupport => sort!(ids, |entry: &ModEntry| { - entry - .remote_version - .clone() - .and_then(|r| r.direct_download_url.clone()) - .ok_or_else(|| entry.name.clone()) + Heading::AutoUpdateSupport => sort!(ids, entry Result<&UpdateStatus, &String> { + entry.get_direct_download_url().and(entry.version_checker.as_ref().map(|vc| &vc.update_status)).ok_or_else(|| &entry.name) }), - Heading::InstallDate => sort!(ids, |entry: &ModEntry| entry.manager_metadata.install_date), - Heading::Type => sort!(ids, |entry: &ModEntry| { + Heading::InstallDate => { + sort!(ids, key |entry: &ModEntry| entry.manager_metadata.install_date) + } + Heading::Type => sort!(ids, key |entry: &ModEntry| { if entry.total_conversion { 3 } else if entry.utility { @@ -786,7 +739,7 @@ impl TableData for ModList { #[derive(Serialize, Deserialize)] pub struct EnabledMods { #[serde(rename = "enabledMods")] - enabled_mods: Vec, + enabled_mods: HashSet, } impl EnabledMods { @@ -807,14 +760,16 @@ impl EnabledMods { impl From>> for EnabledMods { fn from(from: Vec>) -> Self { Self { - enabled_mods: from.iter().map(|v| v.id.clone()).collect(), + enabled_mods: from.iter().map(|v| v.mod_id.clone()).collect(), } } } impl From> for EnabledMods { fn from(enabled_mods: Vec) -> Self { - Self { enabled_mods } + Self { + enabled_mods: enabled_mods.into_iter().collect(), + } } } @@ -856,34 +811,40 @@ impl Filters { Filters::Enabled => |entry: &ModEntry| !entry.enabled, Filters::Disabled => |entry: &ModEntry| entry.enabled, Filters::Unimplemented => |entry: &ModEntry| entry.version_checker.is_none(), - Filters::Error => |entry: &ModEntry| entry.update_status == Some(UpdateStatus::Error), - Filters::UpToDate => |entry: &ModEntry| entry.update_status == Some(UpdateStatus::UpToDate), - Filters::Discrepancy => { - |entry: &ModEntry| matches!(entry.update_status, Some(UpdateStatus::Discrepancy(_))) - } - Filters::Patch => { - |entry: &ModEntry| matches!(entry.update_status, Some(UpdateStatus::Patch(_))) - } - Filters::Minor => { - |entry: &ModEntry| matches!(entry.update_status, Some(UpdateStatus::Minor(_))) - } - Filters::Major => { - |entry: &ModEntry| matches!(entry.update_status, Some(UpdateStatus::Major(_))) - } - Filters::AutoUpdateAvailable => |entry: &ModEntry| { - entry - .remote_version - .as_ref() - .and_then(|r| r.direct_download_url.as_ref()) - .is_some() + Filters::Error => |entry: &ModEntry| { + entry.version_checker.as_ref().map(|vc| &vc.update_status) == Some(&UpdateStatus::Error) + }, + Filters::UpToDate => |entry: &ModEntry| { + entry.version_checker.as_ref().map(|vc| &vc.update_status) == Some(&UpdateStatus::UpToDate) + }, + Filters::Discrepancy => |entry: &ModEntry| { + matches!( + entry.version_checker.as_ref().map(|vc| &vc.update_status), + Some(UpdateStatus::Discrepancy(_)) + ) }, - Filters::AutoUpdateUnsupported => |entry: &ModEntry| { - entry - .remote_version - .as_ref() - .and_then(|r| r.direct_download_url.as_ref()) - .is_none() + Filters::Patch => |entry: &ModEntry| { + matches!( + entry.version_checker.as_ref().map(|vc| &vc.update_status), + Some(UpdateStatus::Patch(_)) + ) }, + Filters::Minor => |entry: &ModEntry| { + matches!( + entry.version_checker.as_ref().map(|vc| &vc.update_status), + Some(UpdateStatus::Minor(_)) + ) + }, + Filters::Major => |entry: &ModEntry| { + matches!( + entry.version_checker.as_ref().map(|vc| &vc.update_status), + Some(UpdateStatus::Major(_)) + ) + }, + Filters::AutoUpdateAvailable => |entry: &ModEntry| entry.get_direct_download_url().is_some(), + Filters::AutoUpdateUnsupported => { + |entry: &ModEntry| entry.get_direct_download_url().is_none() + } } } } diff --git a/moss/src/app/mod_list/actions/action_options.rs b/moss/src/app/mod_list/actions/action_options.rs index ead32344..7d6bafaa 100644 --- a/moss/src/app/mod_list/actions/action_options.rs +++ b/moss/src/app/mod_list/actions/action_options.rs @@ -42,7 +42,7 @@ impl ActionsOptions { data.actions_state.open = false; for (_, entry) in data.mods.iter_mut() { let entry = Rc::make_mut(entry); - + entry.enabled = true; } }), diff --git a/moss/src/app/overlays/duplicate.rs b/moss/src/app/overlays/duplicate.rs index f3fab2b8..847bc9c2 100644 --- a/moss/src/app/overlays/duplicate.rs +++ b/moss/src/app/overlays/duplicate.rs @@ -37,7 +37,7 @@ impl Duplicate { .with_child( h2_fixed(&format!( r#"Multiple mods with ID "{}" installed."#, - &duplicates.front().unwrap().id + &duplicates.front().unwrap().mod_id )) .halign_centre(), ) diff --git a/moss/src/app/overlays/mod.rs b/moss/src/app/overlays/mod.rs index 94651a00..18d91925 100644 --- a/moss/src/app/overlays/mod.rs +++ b/moss/src/app/overlays/mod.rs @@ -28,7 +28,12 @@ use overwrite::Overwrite; use remote_update::RemoteUpdate; use select_install::SelectInstall; -use crate::app::{mod_entry::ModEntry, overlays::self_update::StatusPopup, util::DataTimer, App}; +use crate::app::{ + mod_entry::{version_checker::ModVersionMeta, ModEntry}, + overlays::self_update::StatusPopup, + util::DataTimer, + App, +}; #[derive(Clone, Data)] pub enum Popup { @@ -134,7 +139,7 @@ impl Popup { Popup::Ovewrite(Overwrite::new(conflict, to_install, entry)) } - pub fn duplicate(duplicates: Vector) -> Popup { + pub fn duplicate(duplicate: String) -> Popup { Popup::Duplicate(Duplicate::new(duplicates)) } @@ -142,12 +147,12 @@ impl Popup { Popup::FoundMultiple(Multiple::new(source, found.into())) } - pub fn remote_update(mod_entry: &ModEntry) -> Popup { + pub fn remote_update(mod_entry: &ModEntry, remote_version: &ModVersionMeta) -> Popup { Popup::RemoteUpdate(RemoteUpdate::new( - mod_entry.id.clone(), + mod_entry.mod_id.clone(), mod_entry.name.clone(), mod_entry.version.clone(), - mod_entry.remote_version.clone().unwrap(), + remote_version.clone(), )) } diff --git a/moss/src/app/overlays/multiple.rs b/moss/src/app/overlays/multiple.rs index e486855d..1eb06d47 100644 --- a/moss/src/app/overlays/multiple.rs +++ b/moss/src/app/overlays/multiple.rs @@ -226,7 +226,7 @@ fn row(entry: &ModEntry) -> impl Widget { .with_row( TableRow::new() .with_child(bolded("ID:").align_right()) - .with_child(Label::new(entry.id.clone())), + .with_child(Label::new(entry.mod_id.clone())), ) .with_row( TableRow::new() diff --git a/moss/src/app/overlays/overwrite.rs b/moss/src/app/overlays/overwrite.rs index e309092c..9f725432 100644 --- a/moss/src/app/overlays/overwrite.rs +++ b/moss/src/app/overlays/overwrite.rs @@ -53,7 +53,7 @@ impl Overwrite { StringOrPath::Path(path) => format!( "Found a folder at the path {} when trying to install {}.", path.to_string_lossy(), - entry.id + entry.mod_id ), })) .with_child(Label::new("This action is permanent and cannot be undone.")) diff --git a/moss/src/app/settings.rs b/moss/src/app/settings.rs index ef5c41ef..e31cf654 100644 --- a/moss/src/app/settings.rs +++ b/moss/src/app/settings.rs @@ -37,7 +37,11 @@ use super::{ App, }; use crate::{ - app::{updater::get_update_status_handler, PROJECT}, + app::{ + settings::version_tag::{VersionTag, SETTINGS_VERSION_TAG}, + updater::get_update_status_handler, + PROJECT, + }, nav_bar::Nav, theme::{Theme, Themes, GREEN_KEY, ON_GREEN_KEY}, widgets::RootStack, @@ -47,8 +51,12 @@ mod theme_editor; pub use theme_editor::*; -#[derive(Clone, Data, Lens, Serialize, Deserialize, Default, Debug)] +const SETTINGS_VERSION: u8 = 0; + +#[derive(Clone, Data, Lens, Serialize, Deserialize, Debug)] pub struct Settings { + version: VersionTag, + #[serde(skip)] pub dirty: bool, #[data(eq)] @@ -83,6 +91,48 @@ pub struct Settings { pub jre_23: bool, } +mod version_tag { + use druid::Data; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use super::SETTINGS_VERSION; + + pub(super) const SETTINGS_VERSION_TAG: VersionTag = VersionTag(Some(SETTINGS_VERSION)); + + #[derive(Debug, Clone, Data)] + pub(super) struct VersionTag(Option); + + impl Serialize for VersionTag { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Some(SETTINGS_VERSION).serialize(serializer) + } + } + + #[derive(thiserror::Error, Debug)] + pub(crate) enum TagError { + #[error("Incompatible config version: {0} | Current: {SETTINGS_VERSION}")] + Incompatible(u8), + } + + impl<'de> Deserialize<'de> for VersionTag { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let version = >::deserialize(deserializer)?.unwrap_or(0); + + if version == SETTINGS_VERSION { + Ok(Self(Some(version))) + } else { + Err(serde::de::Error::custom(TagError::Incompatible(version))) + } + } + } +} + fn default_headers() -> Vector { Header::TITLES.to_vec().into() } @@ -92,11 +142,24 @@ impl Settings { pub fn new() -> Self { Self { + version: SETTINGS_VERSION_TAG, hide_webview_on_conflict: true, open_forum_link_in_webview: true, show_duplicate_warnings: true, headings: default_headers(), - ..Default::default() + dirty: Default::default(), + install_dir: Default::default(), + install_dir_buf: Default::default(), + last_browsed: Default::default(), + git_warn: Default::default(), + experimental_launch: Default::default(), + experimental_resolution: Default::default(), + show_discrepancies: Default::default(), + theme: Default::default(), + vmparams: Default::default(), + vmparams_linked: Default::default(), + custom_theme: Default::default(), + jre_23: Default::default(), } } @@ -576,8 +639,7 @@ impl Settings { fs::File::open(Settings::path(false)).map_err(|_| LoadError::NoSuchFile)?; let mut config_string = String::new(); - config_file - .read_to_string(&mut config_string)?; + config_file.read_to_string(&mut config_string)?; serde_json::from_str::(&config_string) .map_err(Into::into) diff --git a/moss/src/app/tools/vmparams.rs b/moss/src/app/tools/vmparams.rs index edbbbe4a..48fde7be 100644 --- a/moss/src/app/tools/vmparams.rs +++ b/moss/src/app/tools/vmparams.rs @@ -399,8 +399,7 @@ impl VMParams { fs::File::open(install_dir.as_ref().join(T::path())).map_err(|_| LoadError::NoSuchFile)?; let mut params_string = String::new(); - params_file - .read_to_string(&mut params_string)?; + params_file.read_to_string(&mut params_string)?; let (mut heap_init, mut heap_max, mut thread_stack_size) = (None, None, None); for param in params_string.split_ascii_whitespace() { diff --git a/moss/src/entrypoint.rs b/moss/src/entrypoint.rs index d73e9b14..1a0ae2c8 100644 --- a/moss/src/entrypoint.rs +++ b/moss/src/entrypoint.rs @@ -4,7 +4,12 @@ use tokio::runtime::Builder; use webview::PROJECT; use crate::{ - app::{app_delegate::AppDelegate, installer_impl::Installer, App, AppViewExt}, + app::{ + app_delegate::AppDelegate, + controllers::{AsyncCoordinatorImpl, GLOBAL_ASYNC_CONTROLLER}, + installer_impl::Installer, + App, AppViewExt, + }, theme::save_original_env, }; @@ -23,17 +28,31 @@ pub fn start() { let _guard = runtime.enter(); - let main_window = WindowDesc::new(App::view().overlay().theme_wrapper().env_as_shared_data()) - .title(concatcp!( - "MOSS | Mod Organizer for StarSector v", - env!("CARGO_PKG_VERSION") - )) - .window_size((1280., 1024.)); + let main_window = WindowDesc::new( + App::view() + .overlay() + .theme_wrapper() + .env_as_shared_data() + .async_controller(runtime.handle().clone()), + ) + .title(concatcp!( + "MOSS | Mod Organizer for StarSector v", + env!("CARGO_PKG_VERSION") + )) + .window_size((1280., 1024.)); // start the application let launcher = AppLauncher::with_window(main_window).configure_env(configure_env); let ext_ctx = launcher.get_external_handle(); + + assert!(GLOBAL_ASYNC_CONTROLLER + .set(AsyncCoordinatorImpl::new( + runtime.handle().clone(), + ext_ctx.clone() + )) + .is_ok()); + let installer = Installer::new(ext_ctx); // create the initial app state @@ -46,7 +65,7 @@ pub fn start() { } launcher - .delegate(AppDelegate::new(/* installer */)) + .delegate(AppDelegate::new()) .launch(initial_state) .expect("Failed to launch application"); } diff --git a/moss/src/main.rs b/moss/src/main.rs index 3692c2ba..a5b30ae6 100644 --- a/moss/src/main.rs +++ b/moss/src/main.rs @@ -3,7 +3,6 @@ // Nightly features #![feature(option_zip)] #![feature(result_flattening)] -#![feature(async_closure)] #![feature(hash_set_entry)] #![feature(string_remove_matches)] #![feature(io_error_more)] @@ -16,6 +15,10 @@ #![feature(trait_alias)] #![feature(type_alias_impl_trait)] #![feature(map_try_insert)] +#![feature(extract_if)] +#![feature(linked_list_cursors)] +#![feature(default_field_values)] +#![feature(get_many_mut)] // Ignored lints #![allow(clippy::new_ret_no_self)] #![allow(clippy::module_name_repetitions)] diff --git a/moss/src/util.rs b/moss/src/util.rs index f2e69712..59952a28 100644 --- a/moss/src/util.rs +++ b/moss/src/util.rs @@ -2,6 +2,7 @@ use std::{ any::Any, collections::HashMap, fmt::Debug, + future::Future, hash::Hash, io::Read, marker::PhantomData, @@ -18,7 +19,9 @@ use regex::Regex; use tokio::{select, sync::mpsc}; use web_client::WebClient; -use crate::app::mod_entry::{GameVersion, ModEntry, ModVersionMeta}; +use crate::app::mod_entry::{ + version_checker::AsyncModVersionMetaRes, GameVersion, ModEntry, ModVersionMeta, +}; #[derive(Debug, thiserror::Error)] pub enum LoadError { @@ -30,12 +33,14 @@ pub enum LoadError { ZipError(#[from] zip::result::ZipError), #[error("IO error")] IoError(#[from] std::io::Error), - #[error("Serialization error")] - SerializationError(#[from] serde_json::Error), + #[error("Serde error")] + SerdeError(#[from] serde_json::Error), #[error("Join error")] JoinError(#[from] tokio::task::JoinError), #[error("Parsing error: {0}")] - ParserError(anyhow::Error), + ParsingError(anyhow::Error), + #[error("Other error: {0}")] + Other(#[from] anyhow::Error), } #[derive(Debug, Clone)] @@ -67,48 +72,26 @@ pub fn get_quoted_version( } } -pub const MASTER_VERSION_RECEIVED: Selector<(String, Result)> = - Selector::new("remote_version_received"); - -pub async fn get_master_version( - client: &WebClient, - ext_sink: Option, +pub fn get_master_version( + client: &Arc, remote_url: String, - id: String, -) -> Option { - let request = async |client: &WebClient| { - let res = client.get(remote_url).await; - - match res { - Err(err) => (id, Err(err.into())), - Ok(remote) => { - let mut stripped = String::new(); - if StripComments::new(remote.as_bytes()) - .read_to_string(&mut stripped) - .is_ok() - && let Ok(normalized) = handwritten_json::normalize(&stripped) - && let Ok(remote) = json5::from_str::(&normalized) - { - (id, Ok(remote)) - } else { - (id, Err(anyhow::anyhow!("Parse error. Payload:\n{remote}"))) - } - } - } - }; +) -> impl Future { + let client = Arc::clone(client); - if let Some(ext_sink) = ext_sink { - let client = client.clone(); - tokio::spawn(async move { - let payload = request(&client).await; + async move { + let remote = client + .get(remote_url) + .await + .map_err(|e| anyhow::anyhow!(e))?; - if let Err(err) = ext_sink.submit_command(MASTER_VERSION_RECEIVED, payload, Target::Auto) { - eprintln!("Failed to submit remote version data {err}"); - } - }); - None - } else { - request(client).await.1.ok() + if let Ok(stripped) = std::io::read_to_string(StripComments::new(remote.as_bytes())) + && let Ok(normalized) = handwritten_json::normalize(&stripped) + && let Ok(remote) = json5::from_str::(&normalized) + { + Ok(remote) + } else { + Err(Arc::new(anyhow::anyhow!("Parse error. Payload:\n{remote}"))) + } } } @@ -145,7 +128,7 @@ pub async fn get_starsector_version(ext_ctx: ExtEventSink, install_dir: PathBuf) version_class.read_to_end(&mut buf)?; let (_, class_file) = - class_parser(&buf).map_err(|err| LoadError::ParserError(err.to_owned().into()))?; + class_parser(&buf).map_err(|err| LoadError::ParsingError(err.to_owned().into()))?; let version_string = class_file .fields @@ -166,7 +149,7 @@ pub async fn get_starsector_version(ext_ctx: ExtEventSink, install_dir: PathBuf) None } }) - .ok_or(LoadError::ParserError(anyhow::anyhow!(""))); + .ok_or(LoadError::ParsingError(anyhow::anyhow!(""))); version_string }) diff --git a/types/src/array_set.rs b/types/src/array_set.rs index 0c1370ef..7bacfb3a 100644 --- a/types/src/array_set.rs +++ b/types/src/array_set.rs @@ -121,14 +121,14 @@ impl Clone for ArraySet { impl AsRef<[T]> for ArraySet { #[inline(always)] fn as_ref(&self) -> &[T] { - unsafe { MaybeUninit::slice_assume_init_ref(&self.mem[0..self.len]) } + unsafe { self.mem[0..self.len].assume_init_ref() } } } impl AsMut<[T]> for ArraySet { #[inline(always)] fn as_mut(&mut self) -> &mut [T] { - unsafe { MaybeUninit::slice_assume_init_mut(&mut self.mem[0..self.len]) } + unsafe { self.mem[0..self.len].assume_init_mut() } } } @@ -193,7 +193,7 @@ impl TryFrom<&[T]> for ArraySet let mut new = Self::default(); new.len = value.len(); - MaybeUninit::clone_from_slice(&mut new.mem, value); + new.mem.write_clone_of_slice(value); Ok(new) }