diff --git a/.gitignore b/.gitignore index 2e15ebf..632014b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ logs/ log/ nomi_logs/ +/assets/ +/instances/ +/libraries/ + debug/ node_modules diff --git a/Cargo.lock b/Cargo.lock index 704a1f8..67b112b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1354,7 +1354,7 @@ dependencies = [ [[package]] name = "egui_task_manager" version = "0.1.1" -source = "git+https://github.com/Umatriz/egui-task-manager#19b25af2e22e8ef77ffacb012a4057215663ef7a" +source = "git+https://github.com/Umatriz/egui-task-manager#fee4f0449da1f8a60849e18d384f1d39b9fc576c" dependencies = [ "egui", "tokio", diff --git a/crates/client/src/cache.rs b/crates/client/src/cache.rs new file mode 100644 index 0000000..bebcd31 --- /dev/null +++ b/crates/client/src/cache.rs @@ -0,0 +1,62 @@ +use eframe::egui::Ui; +use itertools::Itertools; +use std::{ + collections::HashMap, + path::PathBuf, + sync::{Arc, LazyLock}, +}; +use tracing::error; + +use nomi_core::{fs::read_toml_config_sync, instance::InstanceProfileId}; +use parking_lot::RwLock; + +use crate::{errors_pool::ErrorPoolExt, views::ModdedProfile}; + +pub static GLOBAL_CACHE: LazyLock>> = LazyLock::new(|| Arc::new(RwLock::new(GlobalCache::new()))); + +pub struct GlobalCache { + profiles: HashMap>>, +} + +impl Default for GlobalCache { + fn default() -> Self { + Self::new() + } +} + +impl GlobalCache { + pub fn new() -> Self { + Self { profiles: HashMap::new() } + } + + pub fn get_profile(&self, id: InstanceProfileId) -> Option>> { + self.profiles.get(&id).cloned() + } + + pub fn load_profile(&mut self, id: InstanceProfileId, path: PathBuf) -> Option>> { + read_toml_config_sync(path) + .inspect_err(|error| error!(%error, "Cannot read profile config")) + .report_error() + .and_then(|profile| { + self.profiles.insert(id, profile); + self.profiles.get(&id).cloned() + }) + } + + fn loaded_profiles(&self) -> Vec>> { + self.profiles.values().cloned().collect_vec() + } +} + +pub fn ui_for_loaded_profiles(ui: &mut Ui) { + ui.vertical(|ui| { + for profile in GLOBAL_CACHE.read().loaded_profiles() { + let profile = profile.read(); + ui.horizontal(|ui| { + ui.label(&profile.profile.name); + ui.label(profile.profile.version()); + }); + ui.separator(); + } + }); +} diff --git a/crates/client/src/collections.rs b/crates/client/src/collections.rs index f3b0bb5..35bf7a9 100644 --- a/crates/client/src/collections.rs +++ b/crates/client/src/collections.rs @@ -1,7 +1,7 @@ use std::{collections::HashSet, sync::Arc}; use egui_task_manager::*; -use nomi_core::repository::fabric_meta::FabricVersions; +use nomi_core::{instance::InstanceProfileId, repository::fabric_meta::FabricVersions}; use nomi_modding::modrinth::{ project::{Project, ProjectId}, version::Version, @@ -9,7 +9,8 @@ use nomi_modding::modrinth::{ use crate::{ errors_pool::ErrorPoolExt, - views::{ProfilesConfig, SimpleDependency}, + toasts, + views::{InstancesConfig, SimpleDependency}, }; pub struct FabricDataCollection; @@ -73,9 +74,9 @@ impl<'c> TasksCollection<'c> for JavaCollection { pub struct GameDownloadingCollection; impl<'c> TasksCollection<'c> for GameDownloadingCollection { - type Context = &'c ProfilesConfig; + type Context = &'c InstancesConfig; - type Target = Option<()>; + type Target = Option; type Executor = executors::Linear; @@ -84,8 +85,17 @@ impl<'c> TasksCollection<'c> for GameDownloadingCollection { } fn handle(context: Self::Context) -> Handler<'c, Self::Target> { - Handler::new(|_| { - context.update_config().report_error(); + Handler::new(|id| { + if let Some(id) = id { + context.update_profile_config(id).report_error(); + if let Some(instance) = context.find_instance(id.instance()) { + if let Some(profile) = instance.write().find_profile_mut(id) { + profile.is_downloaded = true + }; + + instance.read().write_blocking().report_error(); + } + } }) } } @@ -93,9 +103,9 @@ impl<'c> TasksCollection<'c> for GameDownloadingCollection { pub struct GameDeletionCollection; impl<'c> TasksCollection<'c> for GameDeletionCollection { - type Context = (); + type Context = &'c InstancesConfig; - type Target = (); + type Target = InstanceProfileId; type Executor = executors::Linear; @@ -103,8 +113,43 @@ impl<'c> TasksCollection<'c> for GameDeletionCollection { "Game deletion collection" } - fn handle(_context: Self::Context) -> Handler<'c, Self::Target> { - Handler::new(|()| ()) + fn handle(context: Self::Context) -> Handler<'c, Self::Target> { + Handler::new(|id: InstanceProfileId| { + if let Some(instance) = context.find_instance(id.instance()) { + instance.write().remove_profile(id); + if context.update_instance_config(id.instance()).report_error().is_some() { + toasts::add(|toasts| toasts.success("Successfully removed the profile")) + } + } + }) + } +} + +pub struct InstanceDeletionCollection; + +impl<'c> TasksCollection<'c> for InstanceDeletionCollection { + type Context = &'c mut InstancesConfig; + + type Target = Option; + + type Executor = executors::Linear; + + fn name() -> &'static str { + "Instance deletion collection" + } + + fn handle(context: Self::Context) -> Handler<'c, Self::Target> { + Handler::new(|id: Option| { + let Some(id) = id else { + return; + }; + + if context.remove_instance(id).is_none() { + return; + } + + toasts::add(|toasts| toasts.success("Successfully removed the instance")) + }) } } @@ -182,9 +227,9 @@ impl<'c> TasksCollection<'c> for DependenciesCollection { pub struct ModsDownloadingCollection; impl<'c> TasksCollection<'c> for ModsDownloadingCollection { - type Context = &'c ProfilesConfig; + type Context = &'c InstancesConfig; - type Target = Option<()>; + type Target = Option; type Executor = executors::Linear; @@ -193,8 +238,10 @@ impl<'c> TasksCollection<'c> for ModsDownloadingCollection { } fn handle(context: Self::Context) -> Handler<'c, Self::Target> { - Handler::new(|_| { - context.update_config().report_error(); + Handler::new(|id| { + if let Some(id) = id { + context.update_profile_config(id).report_error(); + } }) } } @@ -220,9 +267,9 @@ impl<'c> TasksCollection<'c> for GameRunnerCollection { pub struct DownloadAddedModsCollection; impl<'c> TasksCollection<'c> for DownloadAddedModsCollection { - type Context = (&'c mut HashSet, &'c ProfilesConfig); + type Context = (&'c mut HashSet, &'c InstancesConfig); - type Target = ProjectId; + type Target = (InstanceProfileId, ProjectId); type Executor = executors::Parallel; @@ -231,9 +278,9 @@ impl<'c> TasksCollection<'c> for DownloadAddedModsCollection { } fn handle(context: Self::Context) -> Handler<'c, Self::Target> { - Handler::new(|id| { - context.0.remove(&id); - context.1.update_config().report_error(); + Handler::new(|(profile_id, project_id)| { + context.0.remove(&project_id); + context.1.update_profile_config(profile_id).report_error(); }) } } diff --git a/crates/client/src/consts.rs b/crates/client/src/consts.rs index 5a73ee0..1e57450 100644 --- a/crates/client/src/consts.rs +++ b/crates/client/src/consts.rs @@ -1,4 +1,3 @@ -pub const DOT_NOMI_MODS_STASH_DIR: &str = "./.nomi/mods_stash"; -pub const MINECRAFT_MODS_DIRECTORY: &str = "./minecraft/mods"; -pub const NOMI_LOADED_LOCK_FILE: &str = "./minecraft/mods/Loaded.lock"; +pub const DOT_NOMI_MODS_STASH_DIR: &str = ".nomi/mods_stash"; +pub const NOMI_LOADED_LOCK_FILE: &str = "Loaded.lock"; pub const NOMI_LOADED_LOCK_FILE_NAME: &str = "Loaded"; diff --git a/crates/client/src/context.rs b/crates/client/src/context.rs index 3b452dd..24e8715 100644 --- a/crates/client/src/context.rs +++ b/crates/client/src/context.rs @@ -2,7 +2,7 @@ use crate::{ errors_pool::ErrorPoolExt, states::States, subscriber::EguiLayer, - views::{self, profiles::ProfilesPage, settings::SettingsPage, Logs, ModManager, ModManagerState, ProfileInfo, View}, + views::{self, profiles::Instances, settings::SettingsPage, Logs, ModManager, ModManagerState, ProfileInfo, View}, Tab, TabKind, }; use eframe::egui::{self}; @@ -24,6 +24,8 @@ pub struct MyContext { pub is_allowed_to_take_action: bool, pub is_profile_window_open: bool, + pub is_instance_window_open: bool, + pub is_errors_window_open: bool, pub images_clean_requested: bool, } @@ -44,11 +46,14 @@ impl MyContext { egui_layer, launcher_manifest: launcher_manifest_ref, file_dialog: FileDialog::new(), - is_profile_window_open: false, - states: States::new(), manager: TaskManager::new(), + is_allowed_to_take_action: true, + is_profile_window_open: false, + is_instance_window_open: false, + is_errors_window_open: false, + images_clean_requested: false, } } @@ -69,11 +74,11 @@ impl TabViewer for MyContext { match &tab.kind { TabKind::Mods { profile } => { let profile = profile.read(); - self.states.profiles.profiles.find_profile(profile.profile.id).is_none() + self.states.instances.instances.find_profile(profile.profile.id).is_none() } TabKind::ProfileInfo { profile } => { let profile = profile.read(); - self.states.profiles.profiles.find_profile(profile.profile.id).is_none() + self.states.instances.instances.find_profile(profile.profile.id).is_none() } _ => false, } @@ -81,18 +86,17 @@ impl TabViewer for MyContext { fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { match &tab.kind { - TabKind::Profiles => ProfilesPage { + TabKind::Profiles => Instances { is_allowed_to_take_action: self.is_allowed_to_take_action, profile_info_state: &mut self.states.profile_info, manager: &mut self.manager, settings_state: &self.states.settings, - profiles_state: &mut self.states.profiles, - menu_state: &mut self.states.add_profile_menu_state, + profiles_state: &mut self.states.instances, + menu_state: &mut self.states.add_profile_menu, tabs_state: &mut self.states.tabs, logs_state: &self.states.logs_state, launcher_manifest: self.launcher_manifest, - is_profile_window_open: &mut self.is_profile_window_open, } .ui(ui), TabKind::Settings => SettingsPage { @@ -111,19 +115,19 @@ impl TabViewer for MyContext { TabKind::DownloadProgress => { views::DownloadingProgress { manager: &self.manager, - profiles_state: &mut self.states.profiles, + profiles_state: &mut self.states.instances, } .ui(ui); } TabKind::Mods { profile } => ModManager { task_manager: &mut self.manager, - profiles_config: &mut self.states.profiles.profiles, + profiles_config: &mut self.states.instances.instances, mod_manager_state: &mut self.states.mod_manager, profile: profile.clone(), } .ui(ui), TabKind::ProfileInfo { profile } => ProfileInfo { - profiles: &self.states.profiles.profiles, + profiles: &self.states.instances.instances, task_manager: &mut self.manager, profile: profile.clone(), tabs_state: &mut self.states.tabs, diff --git a/crates/client/src/download.rs b/crates/client/src/download.rs index 96f76d3..e9a6550 100644 --- a/crates/client/src/download.rs +++ b/crates/client/src/download.rs @@ -1,20 +1,17 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::anyhow; +use eframe::egui::Context; use egui_task_manager::TaskProgressShared; use nomi_core::{ configs::profile::{Loader, ProfileState}, - downloads::{ - progress::MappedSender, - traits::{DownloadResult, Downloader}, - AssetsDownloader, DownloadQueue, - }, + downloads::{progress::MappedSender, traits::Downloader, AssetsDownloader}, game_paths::GamePaths, - instance::{launch::LaunchSettings, Instance}, + instance::{launch::LaunchSettings, Profile}, loaders::{ + combined::VanillaCombinedDownloader, fabric::Fabric, forge::{Forge, ForgeVersion}, - vanilla::Vanilla, }, repository::java_runner::JavaRunner, state::get_launcher_manifest, @@ -23,14 +20,22 @@ use parking_lot::RwLock; use crate::{errors_pool::ErrorPoolExt, views::ModdedProfile}; -pub async fn task_download_version(profile: Arc>, progress_shared: TaskProgressShared) -> Option<()> { - try_download_version(profile, progress_shared).await.report_error() +pub async fn task_download_version( + progress_shared: TaskProgressShared, + ctx: Context, + profile: Arc>, + java_runner: JavaRunner, +) -> Option<()> { + try_download_version(progress_shared, ctx, profile, java_runner).await.report_error() } -async fn try_download_version(profile: Arc>, progress_shared: TaskProgressShared) -> anyhow::Result<()> { +async fn try_download_version( + progress_shared: TaskProgressShared, + ctx: Context, + profile: Arc>, + java_runner: JavaRunner, +) -> anyhow::Result<()> { let launch_instance = { - let mc_dir = PathBuf::from("./minecraft"); - let version_profile = { let version_profile = &profile.read().profile; version_profile.clone() @@ -45,58 +50,49 @@ async fn try_download_version(profile: Arc>, progress_shar return Err(anyhow!("This profile is already downloaded")); }; - let game_paths = GamePaths { - game: mc_dir.clone(), - assets: mc_dir.join("assets"), - version: mc_dir.join("versions").join(version), - libraries: mc_dir.join("libraries"), - }; + let game_paths = GamePaths::from_id(version_profile.id); - let builder = Instance::builder() + let builder = Profile::builder() .name(version_profile.name.clone()) .version(version_profile.version().to_string()) .game_paths(game_paths.clone()); + let combined_downloader = VanillaCombinedDownloader::new(version_profile.version(), game_paths.clone()).await?; let instance = match loader { - Loader::Vanilla => builder.instance(Box::new(Vanilla::new(version_profile.version(), game_paths.clone()).await?)), - Loader::Fabric { version } => builder.instance(Box::new( - Fabric::new(version_profile.version(), version.as_ref(), game_paths.clone()).await?, - )), - Loader::Forge => builder.instance(Box::new( - Forge::new(version_profile.version(), ForgeVersion::Recommended, game_paths.clone()).await?, - )), + Loader::Vanilla => builder.downloader(Box::new(combined_downloader)), + Loader::Fabric { version } => { + let combined = combined_downloader + .with_loader(|game_version, game_paths| Fabric::new(game_version, version.as_ref(), game_paths)) + .await?; + builder.downloader(Box::new(combined)) + } + Loader::Forge => { + let combined = combined_downloader + .with_loader(|game_version, game_paths| Forge::new(game_version, ForgeVersion::Recommended, game_paths, java_runner)) + .await?; + builder.downloader(Box::new(combined)) + } } .build(); let settings = LaunchSettings { - assets: instance.game_paths.assets.clone(), - game_dir: instance.game_paths.game.clone(), - java_bin: JavaRunner::default(), - libraries_dir: instance.game_paths.libraries.clone(), - manifest_file: instance.game_paths.version.join(format!("{}.json", &version)), - natives_dir: instance.game_paths.version.join("natives"), - version_jar_file: instance.game_paths.version.join(format!("{}.jar", &version)), + java_runner: None, version: version.to_string(), version_type: version_type.clone(), }; let launch_instance = instance.launch_instance(settings, Some(vec!["-Xms2G".to_string(), "-Xmx4G".to_string()])); - let instance = instance.instance(); - + let instance = instance.downloader(); let io = instance.io(); - - let downloader: Box> = instance.into_downloader(); - - io.await?; - - let downloader = DownloadQueue::new().with_downloader_dyn(downloader); + let downloader = instance.into_downloader(); let _ = progress_shared.set_total(downloader.total()); - let mapped_sender = MappedSender::new_progress_mapper(Box::new(progress_shared.sender())); + let mapped_sender = MappedSender::new_progress_mapper(Box::new(progress_shared.sender())).with_side_effect(move || ctx.request_repaint()); + downloader.download(&mapped_sender).await; - Box::new(downloader).download(&mapped_sender).await; + io.await?; launch_instance }; @@ -106,11 +102,11 @@ async fn try_download_version(profile: Arc>, progress_shar Ok(()) } -pub async fn task_assets(version: String, assets_dir: PathBuf, progress_shared: TaskProgressShared) -> Option<()> { - try_assets(version, assets_dir, progress_shared).await.report_error() +pub async fn task_assets(progress_shared: TaskProgressShared, ctx: Context, version: String, assets_dir: PathBuf) -> Option<()> { + try_assets(progress_shared, ctx, version, assets_dir).await.report_error() } -async fn try_assets(version: String, assets_dir: PathBuf, progress_shared: TaskProgressShared) -> anyhow::Result<()> { +async fn try_assets(progress_shared: TaskProgressShared, ctx: Context, version: String, assets_dir: PathBuf) -> anyhow::Result<()> { let manifest = get_launcher_manifest().await?; let version_manifest = manifest.get_version_manifest(version).await?; @@ -126,7 +122,7 @@ async fn try_assets(version: String, assets_dir: PathBuf, progress_shared: TaskP let _ = progress_shared.set_total(downloader.total()); - let mapped_sender = MappedSender::new_progress_mapper(Box::new(progress_shared.sender())); + let mapped_sender = MappedSender::new_progress_mapper(Box::new(progress_shared.sender())).with_side_effect(move || ctx.request_repaint()); Box::new(downloader).download(&mapped_sender).await; diff --git a/crates/client/src/errors_pool.rs b/crates/client/src/errors_pool.rs index b36d4ad..8122894 100644 --- a/crates/client/src/errors_pool.rs +++ b/crates/client/src/errors_pool.rs @@ -1,10 +1,12 @@ use std::{ fmt::{Debug, Display}, - sync::{Arc, RwLock}, + sync::Arc, }; use once_cell::sync::Lazy; -use tracing::error; +use parking_lot::RwLock; + +use crate::toasts; pub static ERRORS_POOL: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(ErrorsPool::default()))); @@ -46,17 +48,8 @@ impl ErrorsPool { } } -#[derive(Default)] -pub struct ErrorsPoolState { - pub is_window_open: bool, - pub number_of_errors: usize, -} - pub trait ErrorPoolExt { fn report_error(self) -> Option; - fn report_error_with_context(self, context: C) -> Option - where - C: Display + Send + Sync + 'static; } impl ErrorPoolExt for Result @@ -67,34 +60,9 @@ where match self { Ok(value) => Some(value), Err(error) => { - error!("{:#?}", error); - if let Ok(mut pool) = ERRORS_POOL - .clone() - .write() - .inspect_err(|err| error!("Unable to write into the `ERRORS_POOL`\n{}", err)) - { - pool.push_error(error); - } - None - } - } - } - - fn report_error_with_context(self, context: C) -> Option - where - C: Display + Send + Sync + 'static, - { - match self { - Ok(value) => Some(value), - Err(error) => { - error!("{:#?}", error); - if let Ok(mut pool) = ERRORS_POOL - .clone() - .write() - .inspect_err(|err| error!("Unable to write into the `ERRORS_POOL`\n{}", err)) - { - pool.push_error(anyhow::Error::msg(error).context(context)); - } + toasts::add(|toasts| toasts.error(error.to_string())); + let mut pool = ERRORS_POOL.write(); + pool.push_error(error); None } } diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 28c1b60..18936ce 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -1,19 +1,19 @@ // Remove console window in release builds #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use anyhow::anyhow; use collections::{AssetsCollection, GameDownloadingCollection, GameRunnerCollection, JavaCollection}; use context::MyContext; use eframe::{ - egui::{self, Align, Align2, Button, Frame, Id, Layout, RichText, ScrollArea, ViewportBuilder}, + egui::{self, Align, Button, Frame, Id, Layout, RichText, ScrollArea, ViewportBuilder}, epaint::Vec2, }; use egui_dock::{DockArea, DockState, NodeIndex, Style}; -use egui_notify::Toasts; use open_directory::open_directory_native; use std::path::Path; use subscriber::EguiLayer; -use ui_ext::TOASTS_ID; -use views::{add_tab_menu::AddTab, View}; +use ui_ext::UiExt; +use views::{add_tab_menu::AddTab, AddProfileMenu, CreateInstanceMenu, View}; use errors_pool::{ErrorPoolExt, ERRORS_POOL}; use nomi_core::{DOT_NOMI_DATA_PACKS_DIR, DOT_NOMI_LOGS_DIR}; @@ -31,6 +31,9 @@ pub mod ui_ext; pub mod utils; pub mod views; +pub mod cache; +pub mod toasts; + pub mod mods; pub mod open_directory; @@ -124,23 +127,15 @@ impl MyTabs { impl eframe::App for MyTabs { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - { - use parking_lot::Mutex; - use std::sync::Arc; - - let toasts = ctx.data_mut(|data| data.get_temp_mut_or_default::>>(egui::Id::new(TOASTS_ID)).clone()); - - let mut locked = toasts.lock(); - - locked.show(ctx); - } + toasts::show(ctx); self.context .manager .add_collection::(()) - .add_collection::(&mut self.context.states.add_profile_menu_state.fabric_versions) - .add_collection::(()) - .add_collection::(&self.context.states.profiles.profiles) + .add_collection::(&mut self.context.states.add_profile_menu.fabric_versions) + .add_collection::(&self.context.states.instances.instances) + .add_collection::(&mut self.context.states.instances.instances) + .add_collection::(&self.context.states.instances.instances) .add_collection::(()) .add_collection::(&mut self.context.states.mod_manager.current_project) .add_collection::(&mut self.context.states.mod_manager.current_versions) @@ -148,19 +143,21 @@ impl eframe::App for MyTabs { &mut self.context.states.mod_manager.current_dependencies, self.context.states.mod_manager.current_project.as_ref().map(|p| &p.id), )) - .add_collection::(&self.context.states.profiles.profiles) + .add_collection::(&self.context.states.instances.instances) .add_collection::(()) .add_collection::(( &mut self.context.states.profile_info.currently_downloading_mods, - &self.context.states.profiles.profiles, + &self.context.states.instances.instances, )); ctx.set_pixels_per_point(self.context.states.client_settings.pixels_per_point); if !self.context.states.java.is_downloaded { - self.context.states.java.download_java(&mut self.context.manager); + self.context.states.java.download_java(&mut self.context.manager, ctx.clone()); } + // egui::Window::new("Loaded profiles").show(ctx, |ui| ui_for_loaded_profiles(ui)); + egui::TopBottomPanel::top("top_panel_id").show(ctx, |ui| { ui.with_layout(Layout::left_to_right(Align::Center).with_cross_align(Align::Center), |ui| { // The way to calculate the target size is captured from the @@ -170,11 +167,10 @@ impl eframe::App for MyTabs { let last_others_width = ui.data(|data| data.get_temp(id_cal_target_size).unwrap_or(this_init_max_width)); let this_target_width = this_init_max_width - last_others_width; - AddTab { - dock_state: &self.dock_state, - tabs_state: &mut self.context.states.tabs, - } - .ui(ui); + ui.menu_button("New", |ui| { + ui.toggle_button(&mut self.context.is_instance_window_open, "Create new instance"); + ui.toggle_button(&mut self.context.is_profile_window_open, "Add new profile"); + }); ui.menu_button("Open", |ui| { if ui @@ -188,58 +184,97 @@ impl eframe::App for MyTabs { } }); + AddTab { + dock_state: &self.dock_state, + tabs_state: &mut self.context.states.tabs, + } + .ui(ui); + ui.add_space(this_target_width); - ui.horizontal(|ui| { - egui::warn_if_debug_build(ui); - ui.hyperlink_to( - RichText::new(format!("{} Nomi on GitHub", egui::special_emojis::GITHUB)).small(), - "https://github.com/Umatriz/nomi", - ); - ui.hyperlink_to(RichText::new("Nomi's Discord server").small(), "https://discord.gg/qRD5XEJKc4"); - }); + + egui::warn_if_debug_build(ui); + + if ui.button("Cause an error").clicked() { + Err::<(), _>(anyhow!("Error!")).report_error(); + } + + let is_errors = { !ERRORS_POOL.read().is_empty() }; + if is_errors { + let button = egui::Button::new(RichText::new("Errors").color(ui.visuals().error_fg_color)); + let button = ui.add(button); + if button.clicked() { + self.context.is_errors_window_open = true; + } + } + + ui.hyperlink_to( + RichText::new(format!("{} Nomi on GitHub", egui::special_emojis::GITHUB)).small(), + "https://github.com/Umatriz/nomi", + ); + ui.hyperlink_to(RichText::new("Nomi's Discord server").small(), "https://discord.gg/qRD5XEJKc4"); ui.data_mut(|data| data.insert_temp(id_cal_target_size, ui.min_rect().width() - this_target_width)); }); }); - if let Ok(len) = ERRORS_POOL.try_read().map(|pool| pool.len()) { - if self.context.states.errors_pool.number_of_errors != len { - self.context.states.errors_pool.number_of_errors = len; - self.context.states.errors_pool.is_window_open = true; - } + { + egui::Window::new("Add new profile") + .collapsible(true) + .resizable(true) + .open(&mut self.context.is_profile_window_open) + .show(ctx, |ui| { + AddProfileMenu { + menu_state: &mut self.context.states.add_profile_menu, + profiles_state: &mut self.context.states.instances, + launcher_manifest: self.context.launcher_manifest, + manager: &mut self.context.manager, + } + .ui(ui); + }); + + egui::Window::new("Create new instance") + .collapsible(true) + .resizable(true) + .open(&mut self.context.is_instance_window_open) + .show(ctx, |ui| { + CreateInstanceMenu { + instances_state: &mut self.context.states.instances, + create_instance_menu_state: &mut self.context.states.create_instance_menu, + } + .ui(ui); + }); } egui::Window::new("Errors") .id("error_window".into()) - .open(&mut self.context.states.errors_pool.is_window_open) - .resizable(false) - .movable(false) - .anchor(Align2::RIGHT_BOTTOM, [0.0, 0.0]) + .open(&mut self.context.is_errors_window_open) .show(ctx, |ui| { - { - match ERRORS_POOL.try_read() { - Ok(pool) => { - if pool.is_empty() { - ui.label("No errors"); - } - ScrollArea::vertical().show(ui, |ui| { - ui.vertical(|ui| { - for error in pool.iter_errors() { - ui.label(format!("{:#?}", error)); - ui.separator(); - } - }); - }); + ScrollArea::vertical().show(ui, |ui| { + ui.horizontal(|ui| { + if ui.button("Clear").clicked() { + ERRORS_POOL.write().clear() } - Err(_) => { - ui.spinner(); + + ui.label("See the Logs tab for detailed information"); + }); + + { + let pool = ERRORS_POOL.read(); + + if pool.is_empty() { + ui.label("No errors"); } - } - } - if ui.button("Clear").clicked() { - ERRORS_POOL.write().unwrap().clear() - } + egui::Frame::dark_canvas(ui.style()).show(ui, |ui| { + ui.vertical(|ui| { + for error in pool.iter_errors() { + ui.label(format!("{:#?}", error)); + ui.separator(); + } + }); + }); + } + }); }); egui::CentralPanel::default() diff --git a/crates/client/src/mods.rs b/crates/client/src/mods.rs index d9915c0..48bd013 100644 --- a/crates/client/src/mods.rs +++ b/crates/client/src/mods.rs @@ -4,12 +4,14 @@ use std::{ sync::{mpsc::Sender, Arc}, }; +use eframe::egui::Context; use egui_task_manager::{Progress, TaskProgressShared}; use itertools::Itertools; use nomi_core::{ calculate_sha1, downloads::{progress::MappedSender, traits::Downloader, DownloadSet, FileDownloader}, fs::read_toml_config, + instance::{Instance, InstanceProfileId}, }; use nomi_modding::{ modrinth::{ @@ -21,12 +23,10 @@ use nomi_modding::{ use serde::{Deserialize, Serialize}; use tokio::{fs::File, io::AsyncWriteExt}; -use crate::{ - errors_pool::ErrorPoolExt, progress::UnitProgress, DOT_NOMI_MODS_STASH_DIR, MINECRAFT_MODS_DIRECTORY, NOMI_LOADED_LOCK_FILE, - NOMI_LOADED_LOCK_FILE_NAME, -}; +use crate::{errors_pool::ErrorPoolExt, progress::UnitProgress, DOT_NOMI_MODS_STASH_DIR, NOMI_LOADED_LOCK_FILE, NOMI_LOADED_LOCK_FILE_NAME}; #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Hash, Debug)] +#[serde(transparent)] pub struct ModsConfig { pub mods: Vec, } @@ -57,20 +57,19 @@ pub struct SimpleDependency { pub is_required: bool, } -pub async fn download_added_mod(progress: TaskProgressShared, profile_id: usize, files: Vec) { +pub async fn download_added_mod(progress: TaskProgressShared, ctx: Context, target_path: PathBuf, files: Vec) { let _ = progress.set_total(files.len() as u32); let mut set = DownloadSet::new(); - let mods_stash = Path::new(DOT_NOMI_MODS_STASH_DIR).join(format!("{profile_id}")); for file in files { - let downloader = FileDownloader::new(file.url, mods_stash.join(file.filename)) + let downloader = FileDownloader::new(file.url, target_path.join(file.filename)) .with_sha1(file.sha1) .into_retry(); set.add(Box::new(downloader)); } - let sender = MappedSender::new_progress_mapper(Box::new(progress.sender())); + let sender = MappedSender::new_progress_mapper(Box::new(progress.sender())).with_side_effect(move || ctx.request_repaint()); Box::new(set).download(&sender).await; } @@ -108,7 +107,7 @@ pub async fn proceed_deps(dist: &mut Vec, version: Arc, PathBuf, String)>) -> anyhow::Result> { +pub async fn download_mods(progress: TaskProgressShared, ctx: Context, versions: Vec<(Arc, PathBuf, String)>) -> anyhow::Result> { let _ = progress.set_total( versions .iter() @@ -118,14 +117,14 @@ pub async fn download_mods(progress: TaskProgressShared, versions: Vec<(Arc>, dir: PathBuf, name: String, version: Arc) -> anyhow::Result { +pub async fn download_mod(sender: Sender>, ctx: Context, dir: PathBuf, name: String, version: Arc) -> anyhow::Result { let mut set = DownloadSet::new(); let mut downloaded_files = Vec::new(); @@ -152,7 +151,7 @@ pub async fn download_mod(sender: Sender>, dir: PathBuf, name: set.add(Box::new(downloader)); } - let sender = MappedSender::new_progress_mapper(Box::new(sender)); + let sender = MappedSender::new_progress_mapper(Box::new(sender)).with_side_effect(move || ctx.request_repaint()); Box::new(set).download(&sender).await; @@ -187,27 +186,31 @@ impl CurrentlyLoaded { } /// Load profile's mods by creating hard links. -pub async fn load_mods(profile_id: usize) -> anyhow::Result<()> { - async fn make_link(source: &Path, file_name: &OsStr) -> anyhow::Result<()> { - let dst = PathBuf::from(MINECRAFT_MODS_DIRECTORY).join(file_name); +pub async fn load_mods(id: InstanceProfileId) -> anyhow::Result<()> { + async fn make_link(source: &Path, mods_dir: &Path, file_name: &OsStr) -> anyhow::Result<()> { + let dst = mods_dir.join(file_name); tokio::fs::hard_link(source, dst).await.map_err(|e| e.into()) } - if !Path::new(NOMI_LOADED_LOCK_FILE).exists() { - CurrentlyLoaded { id: profile_id }.write_with_comment(NOMI_LOADED_LOCK_FILE).await? + let instance_path = Instance::path_from_id(id.instance()); + let mods_stash = mods_stash_path_for_profile(id); + let mods_dir = instance_path.join("mods"); + let loaded_lock_path = mods_dir.join(NOMI_LOADED_LOCK_FILE); + + if !loaded_lock_path.exists() { + CurrentlyLoaded { id: id.profile() }.write_with_comment(&loaded_lock_path).await? } - let mut loaded = read_toml_config::(NOMI_LOADED_LOCK_FILE).await?; + let mut loaded = read_toml_config::(&loaded_lock_path).await?; - let target_dir = PathBuf::from(MINECRAFT_MODS_DIRECTORY) + let target_dir = mods_dir .read_dir()? .filter_map(|r| r.ok()) .map(|e| (e.file_name(), e.path())) .collect::>(); - if loaded.id == profile_id { - let path = PathBuf::from(DOT_NOMI_MODS_STASH_DIR).join(format!("{profile_id}")); - let mut dir = tokio::fs::read_dir(path).await?; + if loaded.id == id.profile() { + let mut dir = tokio::fs::read_dir(mods_stash).await?; let mut mods_in_the_stash = Vec::new(); @@ -218,13 +221,13 @@ pub async fn load_mods(profile_id: usize) -> anyhow::Result<()> { continue; } - let path = entry.path(); + let source = entry.path(); - let Some(file_name) = path.file_name() else { + let Some(file_name) = source.file_name() else { continue; }; - make_link(&path, file_name).await?; + make_link(&source, &mods_dir, file_name).await?; } for (file_name, path) in target_dir { @@ -242,7 +245,7 @@ pub async fn load_mods(profile_id: usize) -> anyhow::Result<()> { return Ok(()); } - let mut dir = tokio::fs::read_dir(MINECRAFT_MODS_DIRECTORY).await?; + let mut dir = tokio::fs::read_dir(&mods_dir).await?; while let Ok(Some(entry)) = dir.next_entry().await { if entry.file_name() == NOMI_LOADED_LOCK_FILE_NAME { continue; @@ -251,21 +254,27 @@ pub async fn load_mods(profile_id: usize) -> anyhow::Result<()> { tokio::fs::remove_file(entry.path()).await?; } - let mut dir = tokio::fs::read_dir(PathBuf::from(DOT_NOMI_MODS_STASH_DIR).join(format!("{profile_id}"))).await?; + let mut dir = tokio::fs::read_dir(mods_stash).await?; while let Ok(Some(entry)) = dir.next_entry().await { - let path = entry.path(); + let source = entry.path(); - let Some(file_name) = path.file_name() else { + let Some(file_name) = source.file_name() else { continue; }; - make_link(&path, file_name).await?; + make_link(&source, &mods_dir, file_name).await?; } - loaded.id = profile_id; + loaded.id = id.profile(); - loaded.write_with_comment(NOMI_LOADED_LOCK_FILE).await?; + loaded.write_with_comment(loaded_lock_path).await?; Ok(()) } + +pub fn mods_stash_path_for_profile(profile_id: InstanceProfileId) -> PathBuf { + Instance::path_from_id(profile_id.instance()) + .join(DOT_NOMI_MODS_STASH_DIR) + .join(format!("{}", profile_id.profile())) +} diff --git a/crates/client/src/states.rs b/crates/client/src/states.rs index 57547a7..b5c3cf4 100644 --- a/crates/client/src/states.rs +++ b/crates/client/src/states.rs @@ -1,34 +1,35 @@ -use std::{collections::HashSet, path::PathBuf}; +use std::path::PathBuf; +use eframe::egui::Context; use egui_task_manager::{Caller, Task, TaskManager}; use nomi_core::{ downloads::{java::JavaDownloader, progress::MappedSender, traits::Downloader}, fs::read_toml_config_sync, - DOT_NOMI_JAVA_DIR, DOT_NOMI_JAVA_EXECUTABLE, DOT_NOMI_PROFILES_CONFIG, DOT_NOMI_SETTINGS_CONFIG, + DOT_NOMI_JAVA_DIR, DOT_NOMI_JAVA_EXECUTABLE, DOT_NOMI_SETTINGS_CONFIG, }; use tracing::info; use crate::{ collections::JavaCollection, - errors_pool::{ErrorPoolExt, ErrorsPoolState}, + errors_pool::ErrorPoolExt, views::{ add_tab_menu::TabsState, - profiles::ProfilesState, + profiles::InstancesState, settings::{ClientSettingsState, SettingsState}, - AddProfileMenuState, LogsState, ModManagerState, ProfileInfoState, ProfilesConfig, + AddProfileMenuState, CreateInstanceMenuState, LogsState, ModManagerState, ProfileInfoState, }, }; pub struct States { pub tabs: TabsState, - pub errors_pool: ErrorsPoolState, pub logs_state: LogsState, pub java: JavaState, - pub profiles: ProfilesState, + pub instances: InstancesState, pub settings: SettingsState, pub client_settings: ClientSettingsState, - pub add_profile_menu_state: AddProfileMenuState, + pub add_profile_menu: AddProfileMenuState, + pub create_instance_menu: CreateInstanceMenuState, pub mod_manager: ModManagerState, pub profile_info: ProfileInfoState, } @@ -39,16 +40,13 @@ impl Default for States { Self { tabs: TabsState::new(), - errors_pool: ErrorsPoolState::default(), logs_state: LogsState::new(), java: JavaState::new(), - profiles: ProfilesState { - currently_downloading_profiles: HashSet::new(), - profiles: read_toml_config_sync::(DOT_NOMI_PROFILES_CONFIG).unwrap_or_default(), - }, + instances: InstancesState::new(), client_settings: settings.client_settings.clone(), settings, - add_profile_menu_state: AddProfileMenuState::default(), + add_profile_menu: AddProfileMenuState::new(), + create_instance_menu: CreateInstanceMenuState::new(), mod_manager: ModManagerState::new(), profile_info: ProfileInfoState::new(), } @@ -74,7 +72,7 @@ impl JavaState { } } - pub fn download_java(&mut self, manager: &mut TaskManager) { + pub fn download_java(&mut self, manager: &mut TaskManager, ctx: Context) { info!("Downloading Java"); self.is_downloaded = true; @@ -86,7 +84,7 @@ impl JavaState { let io = downloader.io(); - let mapped_sender = MappedSender::new_progress_mapper(Box::new(progress.sender())); + let mapped_sender = MappedSender::new_progress_mapper(Box::new(progress.sender())).with_side_effect(move || ctx.request_repaint()); Box::new(downloader).download(&mapped_sender).await; diff --git a/crates/client/src/toasts.rs b/crates/client/src/toasts.rs new file mode 100644 index 0000000..6541076 --- /dev/null +++ b/crates/client/src/toasts.rs @@ -0,0 +1,20 @@ +use std::sync::{Arc, LazyLock}; + +use eframe::egui::Context; +use egui_notify::{Toast, Toasts}; +use parking_lot::RwLock; + +pub static TOASTS: LazyLock>> = LazyLock::new(|| Arc::new(RwLock::new(new()))); + +fn new() -> Toasts { + Toasts::default() +} + +pub fn show(ctx: &Context) { + TOASTS.write().show(ctx) +} + +pub fn add(writer: impl FnOnce(&mut Toasts) -> &mut Toast) { + let mut toasts = TOASTS.write(); + writer(&mut toasts); +} diff --git a/crates/client/src/ui_ext.rs b/crates/client/src/ui_ext.rs index c44adc5..5450021 100644 --- a/crates/client/src/ui_ext.rs +++ b/crates/client/src/ui_ext.rs @@ -1,7 +1,4 @@ use eframe::egui::{self, popup_below_widget, Id, PopupCloseBehavior, Response, RichText, Ui, WidgetText}; -use egui_notify::{Toast, Toasts}; - -pub const TOASTS_ID: &str = "global_egui_notify_toasts"; pub trait UiExt { fn ui(&self) -> &Ui; @@ -27,6 +24,10 @@ pub trait UiExt { ui.label(RichText::new(format!("⚠ {}", text.into())).color(ui.visuals().warn_fg_color)) } + fn warn_irreversible_action(&mut self) -> Response { + self.warn_label_with_icon_before("This action is irreversible.") + } + fn markdown_ui(&mut self, id: egui::Id, markdown: &str) { use parking_lot::Mutex; use std::sync::Arc; @@ -69,18 +70,6 @@ pub trait UiExt { button } - - fn toasts(&mut self, writer: impl FnOnce(&mut Toasts) -> &mut Toast) { - use parking_lot::Mutex; - use std::sync::Arc; - - let ui = self.ui_mut(); - let toasts = ui.data_mut(|data| data.get_temp_mut_or_default::>>(egui::Id::new(TOASTS_ID)).clone()); - - let mut locked = toasts.lock(); - - writer(&mut locked); - } } impl UiExt for Ui { diff --git a/crates/client/src/views.rs b/crates/client/src/views.rs index a86f4cb..ac1d442 100644 --- a/crates/client/src/views.rs +++ b/crates/client/src/views.rs @@ -2,6 +2,7 @@ use eframe::egui::Ui; pub mod add_profile_menu; pub mod add_tab_menu; +pub mod create_instance_menu; pub mod downloading_progress; pub mod logs; pub mod mods_manager; @@ -11,6 +12,7 @@ pub mod settings; pub use add_profile_menu::*; pub use add_tab_menu::*; +pub use create_instance_menu::*; pub use downloading_progress::*; pub use logs::*; pub use mods_manager::*; diff --git a/crates/client/src/views/add_profile_menu.rs b/crates/client/src/views/add_profile_menu.rs index a660646..07ddfbf 100644 --- a/crates/client/src/views/add_profile_menu.rs +++ b/crates/client/src/views/add_profile_menu.rs @@ -1,26 +1,34 @@ +use std::sync::Arc; + use eframe::egui::{self, Color32, RichText}; use egui_task_manager::{Caller, Task, TaskManager}; use nomi_core::{ configs::profile::{Loader, ProfileState, VersionProfile}, + fs::write_toml_config_sync, + game_paths::GamePaths, + instance::{Instance, ProfilePayload}, repository::{ fabric_meta::{get_fabric_versions, FabricVersions}, launcher_manifest::{LauncherManifest, Version}, manifest::VersionType, }, }; +use parking_lot::RwLock; -use crate::{collections::FabricDataCollection, errors_pool::ErrorPoolExt, views::ModdedProfile}; +use crate::{collections::FabricDataCollection, errors_pool::ErrorPoolExt, ui_ext::UiExt, views::ModdedProfile}; -use super::{profiles::ProfilesState, View}; +use super::{profiles::InstancesState, View}; pub struct AddProfileMenu<'a> { pub manager: &'a mut TaskManager, pub launcher_manifest: &'a LauncherManifest, pub menu_state: &'a mut AddProfileMenuState, - pub profiles_state: &'a mut ProfilesState, + pub profiles_state: &'a mut InstancesState, } pub struct AddProfileMenuState { + parent_instance: Option>>, + selected_version_type: VersionType, profile_name_buf: String, @@ -46,13 +54,15 @@ impl AddProfileMenuState { impl Default for AddProfileMenuState { fn default() -> Self { - Self::default_const() + Self::new() } } impl AddProfileMenuState { - pub const fn default_const() -> Self { + pub fn new() -> Self { Self { + parent_instance: None, + selected_version_type: VersionType::Release, profile_name_buf: String::new(), @@ -74,6 +84,30 @@ impl View for AddProfileMenu<'_> { } } + egui::ComboBox::from_label("Select instance to create profile for") + .selected_text( + self.menu_state + .parent_instance + .as_ref() + .map_or(String::from("No instance selected"), |i| i.read().name().to_owned()), + ) + .show_ui(ui, |ui| { + for instance in &self.profiles_state.instances.instances { + if ui + .selectable_label( + self.menu_state + .parent_instance + .as_ref() + .is_some_and(|i| i.read().id() == instance.read().id()), + instance.read().name(), + ) + .clicked() + { + self.menu_state.parent_instance = Some(instance.clone()); + } + } + }); + { ui.label("Profile name:"); ui.text_edit_singleline(&mut self.menu_state.profile_name_buf); @@ -86,7 +120,7 @@ impl View for AddProfileMenu<'_> { }); let versions_iter = self.launcher_manifest.versions.iter(); - let versions = match self.menu_state.selected_version_type { + let versions = match &self.menu_state.selected_version_type { VersionType::Release => versions_iter.filter(|v| v.version_type == "release").collect::>(), VersionType::Snapshot => versions_iter.filter(|v| v.version_type == "snapshot").collect::>(), }; @@ -110,6 +144,7 @@ impl View for AddProfileMenu<'_> { .selected_text(format!("{}", self.menu_state.selected_loader_buf)) .show_ui(ui, |ui| { ui.selectable_value(&mut self.menu_state.selected_loader_buf, Loader::Vanilla, "Vanilla"); + ui.selectable_value(&mut self.menu_state.selected_loader_buf, Loader::Forge, "Forge"); let fabric = ui.selectable_value(&mut self.menu_state.selected_loader_buf, Loader::Fabric { version: None }, "Fabric"); if fabric.clicked() { @@ -182,38 +217,67 @@ impl View for AddProfileMenu<'_> { let fabric_versions_non_empty = || !self.menu_state.fabric_versions.is_empty(); if self.menu_state.profile_name_buf.trim().is_empty() { - ui.label(RichText::new("You must enter the profile name").color(ui.visuals().error_fg_color)); + ui.error_label("You must enter the profile name"); } if self.menu_state.selected_version_buf.is_none() { - ui.label(RichText::new("You must select the version").color(ui.visuals().error_fg_color)); + ui.error_label("You must select the version"); } if fabric_version_is_none() { - ui.label(RichText::new("You must select the Fabric Version").color(ui.visuals().error_fg_color)); + ui.error_label("You must select the Fabric Version"); + } + + if self.menu_state.parent_instance.is_none() { + ui.error_label("You must select the instance to create profile for"); } if ui .add_enabled( - some_version_buf() - && ((matches!(self.menu_state.selected_loader_buf, Loader::Vanilla)) + self.menu_state.parent_instance.is_some() + && some_version_buf() + && (matches!(self.menu_state.selected_loader_buf, Loader::Vanilla) + || matches!(self.menu_state.selected_loader_buf, Loader::Forge) || (fabric_version_is_some() && fabric_versions_non_empty())), egui::Button::new("Create"), ) .clicked() { - self.profiles_state.profiles.add_profile(ModdedProfile::new(VersionProfile { - id: self.profiles_state.profiles.create_id(), - name: self.menu_state.profile_name_buf.trim_end().to_owned(), - state: ProfileState::NotDownloaded { - // PANICS: It will never panic because it's - // unreachable for `selected_version_buf` to be `None` - version: self.menu_state.selected_version_buf.clone().unwrap().id, - loader: self.menu_state.selected_loader_buf.clone(), - version_type: self.menu_state.selected_version_type.clone(), - }, - })); - self.profiles_state.profiles.update_config().report_error(); + if let Some(instance) = &self.menu_state.parent_instance { + let payload = { + let instance = instance.read(); + let version = self.menu_state.selected_version_buf.clone().unwrap().id; + let profile = VersionProfile { + id: instance.next_id(), + name: self.menu_state.profile_name_buf.trim_end().to_owned(), + state: ProfileState::NotDownloaded { + // PANICS: It will never panic because it's + // unreachable for `selected_version_buf` to be `None` + version: version.clone(), + loader: self.menu_state.selected_loader_buf.clone(), + version_type: self.menu_state.selected_version_type.clone(), + }, + }; + + let path = GamePaths::from_instance_path(instance.path(), profile.id.profile()).profile_config(); + + let payload = ProfilePayload::from_version_profile(&profile, &path); + let profile = ModdedProfile::new(profile); + + write_toml_config_sync(&profile, path).report_error(); + + payload + }; + + { + let id = { + let mut instance = instance.write(); + instance.add_profile(payload); + instance.id() + }; + self.profiles_state.instances.update_instance_config(id).report_error(); + } + } } } } diff --git a/crates/client/src/views/create_instance_menu.rs b/crates/client/src/views/create_instance_menu.rs new file mode 100644 index 0000000..6eadd0a --- /dev/null +++ b/crates/client/src/views/create_instance_menu.rs @@ -0,0 +1,41 @@ +use eframe::egui; +use nomi_core::instance::Instance; + +use crate::{errors_pool::ErrorPoolExt, toasts}; + +use super::{InstancesState, View}; + +pub struct CreateInstanceMenu<'a> { + pub instances_state: &'a mut InstancesState, + pub create_instance_menu_state: &'a mut CreateInstanceMenuState, +} + +#[derive(Default)] +pub struct CreateInstanceMenuState { + pub name: String, +} + +impl CreateInstanceMenuState { + pub fn new() -> Self { + Self::default() + } +} + +impl View for CreateInstanceMenu<'_> { + fn ui(self, ui: &mut eframe::egui::Ui) { + egui::TextEdit::singleline(&mut self.create_instance_menu_state.name) + .hint_text("Instance name") + .show(ui); + + if ui + .add_enabled(!self.create_instance_menu_state.name.trim_end().is_empty(), egui::Button::new("Create")) + .clicked() + { + let id = self.instances_state.instances.next_id(); + let instance = Instance::new(self.create_instance_menu_state.name.trim_end().to_owned(), id); + self.instances_state.instances.add_instance(instance); + self.instances_state.instances.update_instance_config(id).report_error(); + toasts::add(|toasts| toasts.success("New instance created")); + } + } +} diff --git a/crates/client/src/views/downloading_progress.rs b/crates/client/src/views/downloading_progress.rs index ea4664a..ba60e0e 100644 --- a/crates/client/src/views/downloading_progress.rs +++ b/crates/client/src/views/downloading_progress.rs @@ -1,11 +1,11 @@ use eframe::egui::{self, Layout}; use egui_task_manager::TaskManager; -use super::{profiles::ProfilesState, View}; +use super::{profiles::InstancesState, View}; pub struct DownloadingProgress<'a> { pub manager: &'a TaskManager, - pub profiles_state: &'a mut ProfilesState, + pub profiles_state: &'a mut InstancesState, } impl View for DownloadingProgress<'_> { @@ -13,7 +13,7 @@ impl View for DownloadingProgress<'_> { ui.with_layout(Layout::top_down_justified(egui::Align::Min), |ui| { for collection in self.manager.iter_collections() { for task in collection.iter_tasks() { - task.ui(ui) + ui.group(|ui| task.ui(ui)); } } }); diff --git a/crates/client/src/views/mods_manager.rs b/crates/client/src/views/mods_manager.rs index cde4b30..afd2196 100644 --- a/crates/client/src/views/mods_manager.rs +++ b/crates/client/src/views/mods_manager.rs @@ -7,7 +7,10 @@ use std::{ use eframe::egui::{self, Button, Color32, ComboBox, Id, Image, Key, Layout, RichText, ScrollArea, Vec2}; use egui_infinite_scroll::{InfiniteScroll, LoadingState}; use egui_task_manager::{Caller, Task, TaskManager}; -use nomi_core::{DOT_NOMI_DATA_PACKS_DIR, MINECRAFT_DIR}; +use nomi_core::{ + instance::{Instance, InstanceProfileId}, + DOT_NOMI_DATA_PACKS_DIR, +}; use nomi_modding::{ capitalize_first_letters_whitespace_split, modrinth::{ @@ -25,16 +28,15 @@ use crate::{ collections::{DependenciesCollection, ModsDownloadingCollection, ProjectCollection, ProjectVersionsCollection}, errors_pool::ErrorPoolExt, ui_ext::UiExt, - DOT_NOMI_MODS_STASH_DIR, }; -use super::{ModdedProfile, ProfilesConfig, View}; +use super::{InstancesConfig, ModdedProfile, View}; pub use crate::mods::*; pub struct ModManager<'a> { pub task_manager: &'a mut TaskManager, - pub profiles_config: &'a mut ProfilesConfig, + pub profiles_config: &'a mut InstancesConfig, pub profile: Arc>, pub mod_manager_state: &'a mut ModManagerState, } @@ -69,9 +71,10 @@ pub enum DataPackDownloadDirectory { } impl DataPackDownloadDirectory { - pub fn as_path_buf(&self, profile_id: usize) -> PathBuf { + pub fn as_path_buf(&self, profile_id: InstanceProfileId) -> PathBuf { match self { - DataPackDownloadDirectory::Mods => PathBuf::from(DOT_NOMI_MODS_STASH_DIR).join(format!("{profile_id}")), + DataPackDownloadDirectory::Mods => mods_stash_path_for_profile(profile_id), + // TODO: Maybe make this local for each instance DataPackDownloadDirectory::DataPacks => PathBuf::from(DOT_NOMI_DATA_PACKS_DIR), } } @@ -91,11 +94,11 @@ fn fix_svg(text: &str, color: Color32) -> Option { Some(format!(" PathBuf { +fn directory_from_project_type(project_type: ProjectType, profile_id: InstanceProfileId) -> PathBuf { match project_type { - ProjectType::Mod | ProjectType::Modpack => PathBuf::from(DOT_NOMI_MODS_STASH_DIR).join(format!("{}", profile_id)), - ProjectType::ResourcePack => PathBuf::from(MINECRAFT_DIR).join("resourcepacks"), - ProjectType::Shader => PathBuf::from(MINECRAFT_DIR).join("shaderpacks"), + ProjectType::Mod | ProjectType::Modpack => mods_stash_path_for_profile(profile_id), + ProjectType::ResourcePack => Instance::path_from_id(profile_id.instance()).join("resourcepacks"), + ProjectType::Shader => Instance::path_from_id(profile_id.instance()).join("shaderpacks"), ProjectType::DataPack => PathBuf::from(DOT_NOMI_DATA_PACKS_DIR), _ => unreachable!("You cannot download plugins"), } @@ -191,7 +194,7 @@ impl View for ModManager<'_> { ui.horizontal(|ui| { for project_type in ProjectType::iter().filter(|t| !matches!(t, ProjectType::Plugin)) { let enabled = { - (self.profile.read().profile.loader().is_fabric() || matches!(project_type, ProjectType::DataPack)) + (self.profile.read().profile.loader().support_mods() || matches!(project_type, ProjectType::DataPack)) && !matches!(project_type, ProjectType::Modpack) }; @@ -586,12 +589,13 @@ impl View for ModManager<'_> { let project_type = project.project_type; - let _ = self.profiles_config.update_config().report_error(); + let _ = self.profiles_config.update_profile_config(self.profile.read().profile.id).report_error(); let is_data_pack = self.mod_manager_state.is_datapack; let profile_id = { let lock = profile.read(); lock.profile.id }; + let ctx = ui.ctx().clone(); let download_mod = Task::new( "Download mods", Caller::progressing(move |progress| async move { @@ -622,18 +626,18 @@ impl View for ModManager<'_> { versions_with_paths.push(data); } - let mods = download_mods(progress, versions_with_paths).await.report_error(); + let mods = download_mods(progress, ctx, versions_with_paths).await.report_error(); if let Some((mut profile, mods)) = mods.map(|mods| (profile.write(), mods)) { if matches!(project_type, ProjectType::Mod) { profile.mods.mods.extend(mods); profile.mods.mods.sort(); profile.mods.mods.dedup(); - debug!("Added mods to profile {} successfully", profile.profile.id); + debug!(id = ?profile.profile.id, "Added mods to profile successfully"); } } - Some(()) + Some(profile_id) }), ); diff --git a/crates/client/src/views/profile_info.rs b/crates/client/src/views/profile_info.rs index 07dcca6..217651a 100644 --- a/crates/client/src/views/profile_info.rs +++ b/crates/client/src/views/profile_info.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, path::Path, sync::Arc}; +use std::{collections::HashSet, sync::Arc}; use eframe::egui::{self, Color32, Id, RichText, TextEdit}; use egui_task_manager::{Caller, Task, TaskManager}; @@ -7,14 +7,14 @@ use nomi_modding::modrinth::project::ProjectId; use parking_lot::RwLock; use crate::{ - collections::DownloadAddedModsCollection, errors_pool::ErrorPoolExt, open_directory::open_directory_native, ui_ext::UiExt, views::ProfilesConfig, - TabKind, DOT_NOMI_MODS_STASH_DIR, + collections::DownloadAddedModsCollection, errors_pool::ErrorPoolExt, open_directory::open_directory_native, toasts, ui_ext::UiExt, + views::InstancesConfig, TabKind, }; -use super::{download_added_mod, Mod, ModdedProfile, TabsState, View}; +use super::{download_added_mod, mods_stash_path_for_profile, Mod, ModdedProfile, TabsState, View}; pub struct ProfileInfo<'a> { - pub profiles: &'a ProfilesConfig, + pub profiles: &'a InstancesConfig, pub task_manager: &'a mut TaskManager, pub profile: Arc>, pub tabs_state: &'a mut TabsState, @@ -273,7 +273,8 @@ impl View for ProfileInfo<'_> { } { - self.profiles.update_config().report_error(); + let id = self.profile.read().profile.id; + self.profiles.update_profile_config(id).report_error(); } self.profile_info_state.mods_to_import.clear(); @@ -329,7 +330,7 @@ impl View for ProfileInfo<'_> { ui.ctx().copy_text(export_code); } - ui.toasts(|toasts| toasts.success("Copied the export code to the clipboard")); + toasts::add(|toasts| toasts.success("Copied the export code to the clipboard")); } }); @@ -375,9 +376,18 @@ impl View for ProfileInfo<'_> { if let ProfileState::Downloaded(instance) = &mut profile.profile.state { instance.jvm_arguments_mut().clone_from(&self.profile_info_state.profile_jvm_args); } + + if let Some(instance) = self.profiles.find_instance(profile.profile.id.instance()) { + if let Some(profile) = instance.write().find_profile_mut(profile.profile.id) { + profile.name.clone_from(&self.profile_info_state.profile_name); + } + } } - self.profiles.update_config().report_error(); + self.profiles + .update_instance_config(self.profile.read().profile.id.instance()) + .report_error(); + self.profiles.update_profile_config(self.profile.read().profile.id).report_error(); } if ui.button("Reset").clicked() { @@ -387,7 +397,7 @@ impl View for ProfileInfo<'_> { ui.heading("Mods"); - ui.add_enabled_ui(self.profile.read().profile.loader().is_fabric(), |ui| { + ui.add_enabled_ui(self.profile.read().profile.loader().support_mods(), |ui| { ui.toggle_button(&mut self.profile_info_state.is_import_window_open, "Import mods"); if ui .toggle_button(&mut self.profile_info_state.is_export_window_open, "Export mods") @@ -401,7 +411,9 @@ impl View for ProfileInfo<'_> { .on_hover_text("Open a folder where mods for this profile are located.") .clicked() { - let path = Path::new(DOT_NOMI_MODS_STASH_DIR).join(format!("{}", self.profile.read().profile.id)); + let profile_id = self.profile.read().profile.id; + let path = mods_stash_path_for_profile(profile_id); + if !path.exists() { std::fs::create_dir_all(&path).report_error(); } @@ -441,7 +453,7 @@ impl View for ProfileInfo<'_> { if yes.clicked() { mods_to_remove.push(m.project_id.clone()); - let path = Path::new(DOT_NOMI_MODS_STASH_DIR).join(format!("{profile_id}")); + let path = mods_stash_path_for_profile(profile_id); for file in &m.files { std::fs::remove_file(path.join(&file.filename)).report_error(); } @@ -456,11 +468,12 @@ impl View for ProfileInfo<'_> { let profile_id = self.profile.read().profile.id; let files = m.files.clone(); let project_id = m.project_id.clone(); + let ctx = ui.ctx().clone(); let download_task = Task::new( "Download mod", Caller::progressing(move |progress| async move { - download_added_mod(progress, profile_id, files).await; - project_id + download_added_mod(progress, ctx, mods_stash_path_for_profile(profile_id), files).await; + (profile_id, project_id) }), ); @@ -476,7 +489,7 @@ impl View for ProfileInfo<'_> { vec.retain(|m| !mods_to_remove.contains(&m.project_id)); if !mods_to_remove.is_empty() { - self.profiles.update_config().report_error(); + self.profiles.update_profile_config(self.profile.read().profile.id).report_error(); } let _ = std::mem::replace(&mut self.profile.write().mods.mods, vec); diff --git a/crates/client/src/views/profiles.rs b/crates/client/src/views/profiles.rs index e8988b2..3d1d9d3 100644 --- a/crates/client/src/views/profiles.rs +++ b/crates/client/src/views/profiles.rs @@ -1,68 +1,70 @@ -use std::{ - collections::HashSet, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{collections::HashSet, mem, path::PathBuf, sync::Arc}; -use eframe::egui::{self, popup_below_widget, Align2, Button, Id, PopupCloseBehavior, TextWrapMode, Ui}; +use anyhow::bail; +use eframe::egui::{self, Id, RichText, TextWrapMode, Ui}; use egui_extras::{Column, TableBuilder}; use egui_task_manager::{Caller, Task, TaskManager}; +use itertools::Itertools; use nomi_core::{ - configs::profile::{Loader, ProfileState, VersionProfile}, - fs::{read_toml_config, read_toml_config_sync, write_toml_config, write_toml_config_sync}, - instance::launch::arguments::UserData, + configs::profile::{ProfileState, VersionProfile}, + fs::write_toml_config_sync, + game_paths::GamePaths, + instance::{delete_profile, launch::arguments::UserData, load_instances, Instance, InstanceProfileId, ProfilePayload}, repository::{launcher_manifest::LauncherManifest, username::Username}, - DOT_NOMI_PROFILES_CONFIG, }; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; +use tracing::error; use crate::{ - collections::{AssetsCollection, GameDeletionCollection, GameDownloadingCollection, GameRunnerCollection}, + cache::GLOBAL_CACHE, + collections::{AssetsCollection, GameDeletionCollection, GameDownloadingCollection, GameRunnerCollection, InstanceDeletionCollection}, download::{task_assets, task_download_version}, errors_pool::ErrorPoolExt, + toasts, ui_ext::UiExt, - TabKind, DOT_NOMI_MODS_STASH_DIR, + TabKind, }; -use super::{ - add_profile_menu::{AddProfileMenu, AddProfileMenuState}, - load_mods, - settings::SettingsState, - LogsState, ModsConfig, ProfileInfoState, TabsState, View, -}; +use super::{add_profile_menu::AddProfileMenuState, load_mods, settings::SettingsState, LogsState, ModsConfig, ProfileInfoState, TabsState, View}; -pub struct ProfilesPage<'a> { +pub struct Instances<'a> { pub is_allowed_to_take_action: bool, pub manager: &'a mut TaskManager, pub settings_state: &'a SettingsState, pub profile_info_state: &'a mut ProfileInfoState, - pub is_profile_window_open: &'a mut bool, - pub logs_state: &'a LogsState, pub tabs_state: &'a mut TabsState, - pub profiles_state: &'a mut ProfilesState, + pub profiles_state: &'a mut InstancesState, pub menu_state: &'a mut AddProfileMenuState, pub launcher_manifest: &'static LauncherManifest, } -pub struct ProfilesState { - pub currently_downloading_profiles: HashSet, - pub profiles: ProfilesConfig, +pub struct InstancesState { + pub currently_downloading_profiles: HashSet, + pub instances: InstancesConfig, +} + +impl Default for InstancesState { + fn default() -> Self { + Self::new() + } } -#[derive(Default, PartialEq, Eq, Hash, Clone)] -pub struct SimpleProfile { - pub name: String, - pub version: String, - pub loader: Loader, +impl InstancesState { + pub fn new() -> Self { + Self { + currently_downloading_profiles: HashSet::new(), + instances: InstancesConfig::load(), + } + } } #[derive(Serialize, Deserialize, Default)] -pub struct ProfilesConfig { - pub profiles: Vec>>, +pub struct InstancesConfig { + pub instances: Vec>>, } #[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -80,68 +82,174 @@ impl ModdedProfile { } } -impl ProfilesConfig { - pub fn find_profile(&self, target_id: usize) -> Option<&Arc>> { - self.profiles.iter().find(|p| p.read().profile.id == target_id) +impl InstancesConfig { + pub fn find_profile(&self, id: InstanceProfileId) -> Option>> { + let mut cache = GLOBAL_CACHE.write(); + cache + .get_profile(id) + .or_else(|| self.get_profile_path(id).and_then(|path| cache.load_profile(id, path))) } - pub fn read() -> Self { - read_toml_config_sync::(DOT_NOMI_PROFILES_CONFIG).unwrap_or_default() + pub fn get_profile_path(&self, id: InstanceProfileId) -> Option { + self.find_instance(id.instance()) + .and_then(|i| i.read().profiles().iter().find(|p| p.id == id).map(|p| p.path.clone())) } - pub async fn read_async() -> Self { - read_toml_config::(DOT_NOMI_PROFILES_CONFIG).await.unwrap_or_default() + pub fn find_instance(&self, id: usize) -> Option>> { + self.instances.iter().find(|p| p.read().id() == id).cloned() } - pub fn try_read() -> anyhow::Result { - read_toml_config_sync::(DOT_NOMI_PROFILES_CONFIG) + pub fn remove_instance(&mut self, id: usize) -> Option>> { + self.instances + .iter() + .position(|i| i.read().id() == id) + .map(|idx| self.instances.remove(idx)) } - pub fn add_profile(&mut self, profile: ModdedProfile) { - self.profiles.push(RwLock::new(profile).into()) + pub fn load() -> Self { + Self { + instances: load_instances() + .unwrap_or_default() + .into_iter() + .map(RwLock::new) + .map(Arc::new) + .collect_vec(), + } + } + + pub async fn load_async() -> anyhow::Result { + tokio::task::spawn_blocking(Self::load).await.map_err(Into::into) + } + + pub fn add_instance(&mut self, instance: Instance) { + self.instances.push(RwLock::new(instance).into()) } - pub fn create_id(&self) -> usize { - match &self.profiles.iter().max_by_key(|profile| profile.read().profile.id) { - Some(v) => v.read().profile.id + 1, + pub fn next_id(&self) -> usize { + match &self.instances.iter().map(|instance| instance.read().id()).max() { + Some(id) => id + 1, None => 0, } } - pub fn update_config(&self) -> anyhow::Result<()> { - write_toml_config_sync(&self, DOT_NOMI_PROFILES_CONFIG) + pub fn update_profile_config(&self, id: InstanceProfileId) -> anyhow::Result<()> { + let Some((path, profile)) = self + .get_profile_path(id) + .and_then(|path| self.find_profile(id).map(|profile| (path, profile))) + else { + error!(?id, "Cannot find the profile"); + bail!("Cannot find ") + }; + + let profile = profile.read(); + write_toml_config_sync(&*profile, path) } - pub async fn update_config_async(&self) -> anyhow::Result<()> { - write_toml_config(&self, DOT_NOMI_PROFILES_CONFIG).await + pub fn update_all_instance_configs(&self) -> anyhow::Result<()> { + for instance in self.instances.iter() { + let instance = instance.read(); + instance.write_blocking().report_error(); + } + + Ok(()) + } + + pub fn update_instance_config(&self, id: usize) -> anyhow::Result<()> { + let Some(instance) = self.find_instance(id) else { + bail!("No such instance") + }; + + let instance = instance.read(); + + instance.write_blocking() } } -impl View for ProfilesPage<'_> { - fn ui(self, ui: &mut Ui) { - { - ui.toggle_value(self.is_profile_window_open, "Add new profile"); - - egui::Window::new("Create new profile") - .title_bar(true) - .collapsible(false) - .resizable(false) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .movable(false) - .open(self.is_profile_window_open) - .show(ui.ctx(), |ui| { - AddProfileMenu { - menu_state: self.menu_state, - profiles_state: self.profiles_state, - launcher_manifest: self.launcher_manifest, - manager: self.manager, // is_profile_window_open: self.is_profile_window_open, - } - .ui(ui); - }); +impl Instances<'_> { + fn profile_action_ui(&mut self, ui: &mut Ui, profile_payload: &ProfilePayload) { + let button = if profile_payload.is_downloaded { + ui.add_enabled(self.is_allowed_to_take_action, egui::Button::new("Launch")) + } else { + ui.add_enabled( + !self.profiles_state.currently_downloading_profiles.contains(&profile_payload.id), + egui::Button::new("Download"), + ) + }; + + if button.clicked() { + let Some(profile_lock) = self.profiles_state.instances.find_profile(profile_payload.id) else { + error!(id = ?profile_payload.id, "Cannot find the profile"); + return; + }; + + self.profile_action(ui, profile_lock) } + } - ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); + fn profile_action(&mut self, ui: &mut Ui, profile_lock: Arc>) { + let profile = profile_lock.read(); + match &profile.profile.state { + ProfileState::Downloaded(instance) => { + let user_data = UserData { + username: Username::new(self.settings_state.username.clone()).unwrap(), + uuid: Some(self.settings_state.uuid.clone()), + access_token: None, + }; + + let instance = instance.clone(); + let java_runner = self.settings_state.java.clone(); + + let should_load_mods = profile.profile.loader().support_mods(); + let profile_id = profile.profile.id; + + let game_logs = self.logs_state.game_logs.clone(); + game_logs.clear(); + let run_game = Task::new( + "Running the game", + Caller::standard(async move { + if should_load_mods { + load_mods(profile_id).await.report_error(); + } + + instance + .launch(GamePaths::from_id(profile_id), user_data, &java_runner, &*game_logs) + .await + .report_error() + }), + ); + + self.manager.push_task::(run_game) + } + ProfileState::NotDownloaded { .. } => { + self.profiles_state.currently_downloading_profiles.insert(profile.profile.id); + + let game_version = profile.profile.version().to_owned(); + + let game_paths = GamePaths::from_id(profile.profile.id); + let ctx = ui.ctx().clone(); + let assets_task = Task::new( + format!("Assets ({})", profile.profile.version()), + Caller::progressing(|progress| task_assets(progress, ctx, game_version, game_paths.assets)), + ); + self.manager.push_task::(assets_task); + + let profile_clone = profile_lock.clone(); + + let id = profile.profile.id; + let ctx = ui.ctx().clone(); + let java_runner = self.settings_state.java.clone(); + let game_task = Task::new( + format!("Downloading version {}", profile.profile.version()), + Caller::progressing(move |progress| async move { + task_download_version(progress, ctx, profile_clone, java_runner).await.map(|()| id) + }), + ); + self.manager.push_task::(game_task); + } + } + } + fn show_profiles_for_instance(&mut self, ui: &mut Ui, profiles: &[ProfilePayload]) { TableBuilder::new(ui) .column(Column::auto().at_least(120.0).at_most(240.0)) .columns(Column::auto(), 5) @@ -157,171 +265,160 @@ impl View for ProfilesPage<'_> { }); }) .body(|mut body| { - let mut is_deleting = vec![]; + // let mut is_deleting = vec![]; - for (index, profile_lock) in self.profiles_state.profiles.profiles.iter().enumerate() { + for profile in profiles.iter() { body.row(30.0, |mut row| { - let profile = profile_lock.read(); row.col(|ui| { - ui.add(egui::Label::new(&profile.profile.name).truncate()); + ui.add(egui::Label::new(&profile.name).truncate()); }); row.col(|ui| { - ui.label(profile.profile.version()); + ui.label(&profile.version); }); row.col(|ui| { - ui.label(profile.profile.loader_name()); - }); - row.col(|ui| match &profile.profile.state { - ProfileState::Downloaded(instance) => { - if ui.add_enabled(self.is_allowed_to_take_action, egui::Button::new("Launch")).clicked() { - let user_data = UserData { - username: Username::new(self.settings_state.username.clone()).unwrap(), - uuid: Some(self.settings_state.uuid.clone()), - access_token: None, - }; - - let instance = instance.clone(); - let java_runner = self.settings_state.java.clone(); - - let should_load_mods = profile.profile.loader().is_fabric(); - let profile_id = profile.profile.id; - - let game_logs = self.logs_state.game_logs.clone(); - game_logs.clear(); - let run_game = Task::new( - "Running the game", - Caller::standard(async move { - if should_load_mods { - load_mods(profile_id).await.report_error(); - } - - instance.launch(user_data, &java_runner, &*game_logs).await.report_error() - }), - ); - - self.manager.push_task::(run_game) - } - } - ProfileState::NotDownloaded { .. } => { - if ui - .add_enabled( - !self.profiles_state.currently_downloading_profiles.contains(&profile.profile.id), - egui::Button::new("Download"), - ) - .clicked() - { - self.profiles_state.currently_downloading_profiles.insert(profile.profile.id); - - let game_version = profile.profile.version().to_owned(); - - let assets_task = Task::new( - format!("Assets ({})", profile.profile.version()), - Caller::progressing(|progress| task_assets(game_version, PathBuf::from("./minecraft/assets"), progress)), - ); - self.manager.push_task::(assets_task); - - let profile_clone = profile_lock.clone(); - - let game_task = Task::new( - format!("Downloading version {}", profile.profile.version()), - Caller::progressing(|progress| task_download_version(profile_clone, progress)), - ); - self.manager.push_task::(game_task); - } - } + ui.label(profile.loader.name()); }); + row.col(|ui| self.profile_action_ui(ui, profile)); row.col(|ui| { if ui.button("Details").clicked() { - self.profile_info_state.set_profile_to_edit(&profile_lock.read()); + if let Some(profile_lock) = self.profiles_state.instances.find_profile(profile.id) { + self.profile_info_state.set_profile_to_edit(&profile_lock.read()); - let kind = TabKind::ProfileInfo { - profile: profile_lock.clone(), + let kind = TabKind::ProfileInfo { + profile: profile_lock.clone(), + }; + self.tabs_state.0.insert(kind.id(), kind); }; - self.tabs_state.0.insert(kind.id(), kind); } }); row.col(|ui| { - if let ProfileState::Downloaded(instance) = &profile.profile.state { - let popup_id = ui.make_persistent_id("delete_popup_id"); - let button = ui - .add_enabled(self.is_allowed_to_take_action, Button::new("Delete")) - .on_hover_text("It will delete the profile and it's data"); - - if button.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } - - popup_below_widget(ui, popup_id, &button, PopupCloseBehavior::CloseOnClickOutside, |ui| { - ui.set_min_width(150.0); - - let delete_client_id = Id::new("delete_client"); - let delete_libraries_id = Id::new("delete_libraries"); - let delete_assets_id = Id::new("delete_assets"); - let delete_mods_id = Id::new("delete_mods"); - - let mut make_checkbox = |text: &str, id, default: bool| { - let mut state = ui.data_mut(|map| *map.get_temp_mut_or_insert_with(id, move || default)); - ui.checkbox(&mut state, text); - ui.data_mut(|map| map.insert_temp(id, state)); - }; + ui.button_with_confirm_popup(Id::new("confirm_profile_deletion").with(profile.id), "Delete", |ui| { + ui.set_width(200.0); + ui.label("Are you sure you want to delete this profile?"); + ui.warn_irreversible_action(); + + ui.horizontal(|ui| { + let yes_button = ui.button("Yes"); + let no_button = ui.button("No"); + + if yes_button.clicked() { + let id = profile.id; + if let Some(profile) = self.profiles_state.instances.find_profile(id) { + let profile = profile.read(); + let game_version = profile.profile.version().to_owned(); + let task = Task::new( + "Deleting profile", + Caller::standard(async move { + delete_profile(GamePaths::from_id(id), &game_version).await; + id + }), + ); + + self.manager.push_task::(task) + } else { + toasts::add(|toasts| toasts.warning("Cannot find profile to delete")); + } + } - make_checkbox("Delete profile's client", delete_client_id, true); - make_checkbox("Delete profile's libraries", delete_libraries_id, false); - if profile.profile.loader().is_fabric() { - make_checkbox("Delete profile's mods", delete_mods_id, true); + if yes_button.clicked() || no_button.clicked() { + ui.memory_mut(|mem| mem.close_popup()) } - make_checkbox("Delete profile's assets", delete_assets_id, false); + }); + }); + }); + }); + } + }); + } +} - ui.label("Are you sure you want to delete this profile and it's data?"); - ui.horizontal(|ui| { - ui.warn_icon_with_hover_text("Deleting profile's assets and libraries might break other profiles."); - if ui.button("Yes").clicked() { - is_deleting.push(index); +impl View for Instances<'_> { + fn ui(mut self, ui: &mut Ui) { + ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); - let version = &instance.settings.version; + let iter = self.profiles_state.instances.instances.iter().cloned().collect_vec().into_iter(); + for instance in iter { + ui.group(|ui| { + let id = ui.make_persistent_id("instance_details").with(instance.read().id()); + egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, false) + .show_header(ui, |ui| { + let instance = instance.read(); + ui.label(RichText::new(instance.name()).strong()); + + if let Some(profile_lock) = instance.main_profile().and_then(|id| self.profiles_state.instances.find_profile(id)) { + let response = if profile_lock.read().profile.is_downloaded() { + ui.add_enabled(self.is_allowed_to_take_action, egui::Button::new("Launch")) + } else { + ui.add_enabled(self.is_allowed_to_take_action, egui::Button::new("Download")) + }; + + if response.clicked() { + self.profile_action(ui, profile_lock) + } + } + }) + .body(|ui| { + { + let id = { + let instance_id = instance.read().id(); + Id::new("select_instance_main_profile").with(instance_id) + }; + + let selected_text = { + let instance = instance.read(); + instance + .main_profile() + .and_then(|id| self.profiles_state.instances.find_profile(id)) + .map_or(String::from("No profile selected"), |profile| profile.read().profile.name.clone()) + }; + + egui::ComboBox::new(id, "Main profile").selected_text(selected_text).show_ui(ui, |ui| { + let mut instance = instance.write(); + + let profiles = mem::take(instance.profiles_mut()); + + for profile in &profiles { + ui.selectable_value(instance.main_profile_mut(), Some(profile.id), &profile.name); + } - let checkbox_data = |id| ui.data(|data| data.get_temp(id)).unwrap_or_default(); + let _ = mem::replace(instance.profiles_mut(), profiles); + }); + } - let delete_client = checkbox_data(delete_client_id); - let delete_libraries = checkbox_data(delete_libraries_id); - let delete_assets = checkbox_data(delete_assets_id); - let delete_mods = checkbox_data(delete_mods_id); + let instance = instance.read(); - let profile_id = profile.profile.id; + ui.button_with_confirm_popup(Id::new("confirm_instance_deletion").with(instance.id()), "Delete", |ui| { + ui.set_width(200.0); + ui.label("Are you sure you want to delete this instance?"); + ui.warn_irreversible_action(); - let instance = instance.clone(); - let caller = Caller::standard(async move { - let path = Path::new(DOT_NOMI_MODS_STASH_DIR).join(format!("{}", profile_id)); - if delete_mods && path.exists() { - tokio::fs::remove_dir_all(path).await.report_error(); - } - instance.delete(delete_client, delete_libraries, delete_assets).await.report_error(); - }); + ui.horizontal(|ui| { + let yes_button = ui.button("Yes"); + let no_button = ui.button("No"); - let task = Task::new(format!("Deleting the game's files ({})", version), caller); + if yes_button.clicked() { + let path = instance.path(); + let id = instance.id(); + let task = Task::new( + "Deleting the instance", + Caller::standard(async move { tokio::fs::remove_dir_all(path).await.report_error().map(|()| id) }), + ); + self.manager.push_task::(task); + } - self.manager.push_task::(task); + if yes_button.clicked() || no_button.clicked() { + ui.memory_mut(|mem| mem.close_popup()) + } + }); + }); - self.tabs_state.remove_profile_related_tabs(&profile); + ui.heading("Profiles"); - ui.memory_mut(|mem| mem.close_popup()); - } - if ui.button("No").clicked() { - ui.memory_mut(|mem| mem.close_popup()); - } - }); - }); - } - }); + self.show_profiles_for_instance(ui, instance.profiles()) }); - } - - is_deleting.drain(..).for_each(|index| { - self.profiles_state.profiles.profiles.remove(index); - self.profiles_state.profiles.update_config().report_error(); - }); }); + } } } diff --git a/crates/client/src/views/settings.rs b/crates/client/src/views/settings.rs index 89ff523..0889634 100644 --- a/crates/client/src/views/settings.rs +++ b/crates/client/src/views/settings.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use eframe::egui::{self, RichText}; +use eframe::egui::{self}; use egui_file_dialog::FileDialog; use egui_form::{garde::field_path, Form, FormField}; use egui_task_manager::TaskManager; @@ -11,7 +11,7 @@ use nomi_core::{ }; use serde::{Deserialize, Serialize}; -use crate::{collections::JavaCollection, errors_pool::ErrorPoolExt, states::JavaState}; +use crate::{collections::JavaCollection, errors_pool::ErrorPoolExt, states::JavaState, ui_ext::UiExt}; use super::View; @@ -93,27 +93,27 @@ fn check_uuid(value: &str, _context: &()) -> garde::Result { impl View for SettingsPage<'_> { fn ui(self, ui: &mut eframe::egui::Ui) { - ui.collapsing("Utils", |ui| { - let launcher_path = PathBuf::from(DOT_NOMI_LOGS_DIR); + ui.heading("Utils"); - if launcher_path.exists() { - if ui.button("Delete launcher's logs").clicked() { - let _ = std::fs::remove_dir_all(launcher_path); - } - } else { - ui.label(RichText::new("The launcher log's directory is already deleted").color(ui.visuals().warn_fg_color)); + let launcher_path = PathBuf::from(DOT_NOMI_LOGS_DIR); + + if launcher_path.exists() { + if ui.button("Delete launcher's logs").clicked() { + let _ = std::fs::remove_dir_all(launcher_path); } + } else { + ui.warn_label("The launcher log's directory is already deleted"); + } - let game_path = PathBuf::from("./logs"); + let game_path = PathBuf::from("./logs"); - if game_path.exists() { - if ui.button("Delete game's logs").clicked() { - let _ = std::fs::remove_dir_all(game_path); - } - } else { - ui.label(RichText::new("The games log's directory is already deleted").color(ui.visuals().warn_fg_color)); + if game_path.exists() { + if ui.button("Delete game's logs").clicked() { + let _ = std::fs::remove_dir_all(game_path); } - }); + } else { + ui.warn_label("The games log's directory is already deleted"); + } let settings_data = self.settings_state.clone(); @@ -128,52 +128,52 @@ impl View for SettingsPage<'_> { } } - ui.collapsing("User", |ui| { - FormField::new(&mut form, field_path!("username")) - .label("Username") - .ui(ui, egui::TextEdit::singleline(&mut self.settings_state.username)); - - FormField::new(&mut form, field_path!("uuid")) - .label("UUID") - .ui(ui, egui::TextEdit::singleline(&mut self.settings_state.uuid)); - }); + ui.heading("User"); + + FormField::new(&mut form, field_path!("username")) + .label("Username") + .ui(ui, egui::TextEdit::singleline(&mut self.settings_state.username)); + + FormField::new(&mut form, field_path!("uuid")) + .label("UUID") + .ui(ui, egui::TextEdit::singleline(&mut self.settings_state.uuid)); + + ui.heading("Java"); + + if ui + .add_enabled( + self.manager.get_collection::().tasks().is_empty(), + egui::Button::new("Download Java"), + ) + .on_hover_text("Pressing this button will start the Java downloading process and add the downloaded binary as the selected one") + .clicked() + { + self.java_state.download_java(self.manager, ui.ctx().clone()); + self.settings_state.java = JavaRunner::path(PathBuf::from(DOT_NOMI_JAVA_EXECUTABLE)); + self.settings_state.update_config(); + } - ui.collapsing("Java", |ui| { - if ui - .add_enabled( - self.manager.get_collection::().tasks().is_empty(), - egui::Button::new("Download Java"), - ) - .on_hover_text("Pressing this button will start the Java downloading process and add the downloaded binary as selected") - .clicked() - { - self.java_state.download_java(self.manager); - self.settings_state.java = JavaRunner::path(PathBuf::from(DOT_NOMI_JAVA_EXECUTABLE)); - self.settings_state.update_config(); - } + FormField::new(&mut form, field_path!("java")).label("Java").ui(ui, |ui: &mut egui::Ui| { + ui.radio_value(&mut self.settings_state.java, JavaRunner::command("java"), "Command"); - FormField::new(&mut form, field_path!("java")).label("Java").ui(ui, |ui: &mut egui::Ui| { - ui.radio_value(&mut self.settings_state.java, JavaRunner::command("java"), "Command"); + ui.radio_value(&mut self.settings_state.java, JavaRunner::path(PathBuf::new()), "Custom path"); - ui.radio_value(&mut self.settings_state.java, JavaRunner::path(PathBuf::new()), "Custom path"); + if matches!(settings_data.java, JavaRunner::Path(_)) && ui.button("Select custom java binary").clicked() { + self.file_dialog.select_file(); + } - if matches!(settings_data.java, JavaRunner::Path(_)) && ui.button("Select custom java binary").clicked() { - self.file_dialog.select_file(); + ui.label(format!( + "Java will be run using {}", + match &settings_data.java { + JavaRunner::Command(command) => format!("{} command", command), + JavaRunner::Path(path) => format!("{} executable", path.display()), } - - ui.label(format!( - "Java will be run using {}", - match &settings_data.java { - JavaRunner::Command(command) => format!("{} command", command), - JavaRunner::Path(path) => format!("{} executable", path.display()), - } - )) - }); + )) }); - ui.collapsing("Client", |ui| { - ui.add(egui::Slider::new(&mut self.settings_state.client_settings.pixels_per_point, 0.5..=5.0).text("Pixels per point")) - }); + ui.heading("Launcher"); + + ui.add(egui::Slider::new(&mut self.settings_state.client_settings.pixels_per_point, 0.5..=5.0).text("Pixels per point")); } if let Some(Ok(())) = form.handle_submit(&ui.button("Save"), ui) { diff --git a/crates/nomi-core/src/configs/profile.rs b/crates/nomi-core/src/configs/profile.rs index 8ea9727..8f75b0a 100644 --- a/crates/nomi-core/src/configs/profile.rs +++ b/crates/nomi-core/src/configs/profile.rs @@ -5,9 +5,11 @@ use serde::{Deserialize, Serialize}; use typed_builder::TypedBuilder; use crate::{ + game_paths::GamePaths, instance::{ launch::{arguments::UserData, LaunchInstance}, logs::GameLogsWriter, + InstanceProfileId, }, repository::{java_runner::JavaRunner, manifest::VersionType}, }; @@ -33,6 +35,10 @@ impl Display for Loader { } impl Loader { + pub fn support_mods(&self) -> bool { + !self.is_vanilla() + } + pub fn is_fabric(&self) -> bool { matches!(*self, Self::Fabric { .. }) } @@ -44,6 +50,10 @@ impl Loader { pub fn is_vanilla(&self) -> bool { matches!(*self, Self::Vanilla) } + + pub fn name(&self) -> String { + format!("{self}") + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] @@ -73,16 +83,22 @@ impl ProfileState { #[derive(Serialize, Deserialize, Debug, TypedBuilder, Clone, PartialEq, Eq, Hash)] pub struct VersionProfile { - pub id: usize, + pub id: InstanceProfileId, pub name: String, pub state: ProfileState, } impl VersionProfile { - pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner, logs_writer: &dyn GameLogsWriter) -> anyhow::Result<()> { + pub async fn launch( + &self, + paths: GamePaths, + user_data: UserData, + java_runner: &JavaRunner, + logs_writer: &dyn GameLogsWriter, + ) -> anyhow::Result<()> { match &self.state { - ProfileState::Downloaded(instance) => instance.launch(user_data, java_runner, logs_writer).await, + ProfileState::Downloaded(instance) => instance.launch(paths, user_data, java_runner, logs_writer).await, ProfileState::NotDownloaded { .. } => Err(anyhow!("This profile is not downloaded!")), } } @@ -109,4 +125,8 @@ impl VersionProfile { ProfileState::NotDownloaded { version, .. } => version, } } + + pub fn is_downloaded(&self) -> bool { + matches!(self.state, ProfileState::Downloaded(_)) + } } diff --git a/crates/nomi-core/src/consts.rs b/crates/nomi-core/src/consts.rs index f67a141..e97d5f6 100644 --- a/crates/nomi-core/src/consts.rs +++ b/crates/nomi-core/src/consts.rs @@ -1,14 +1,25 @@ pub const DOT_NOMI_DIR: &str = "./.nomi"; pub const DOT_NOMI_TEMP_DIR: &str = "./.nomi/temp"; pub const DOT_NOMI_CONFIGS_DIR: &str = "./.nomi/configs"; -pub const DOT_NOMI_PROFILES_CONFIG: &str = "./.nomi/configs/Profiles.toml"; pub const DOT_NOMI_SETTINGS_CONFIG: &str = "./.nomi/configs/Settings.toml"; pub const DOT_NOMI_LOGS_DIR: &str = "./.nomi/logs"; pub const DOT_NOMI_JAVA_DIR: &str = "./.nomi/java"; pub const DOT_NOMI_JAVA_EXECUTABLE: &str = "./.nomi/java/jdk-22.0.1/bin/java"; pub const DOT_NOMI_DATA_PACKS_DIR: &str = "./.nomi/datapacks"; -pub const MINECRAFT_DIR: &str = "./minecraft"; +pub const LIBRARIES_DIR: &str = "./libraries"; +pub const ASSETS_DIR: &str = "./assets"; + +pub const INSTANCES_DIR: &str = "./instances"; +/// Path to instance's config file with respect to instance's directory. +/// +/// # Example +/// +/// ```rust +/// # use std::path::Path; +/// Path::new("./instances/example").join(INSTANCE_CONFIG) +/// ``` +pub const INSTANCE_CONFIG: &str = ".nomi/Instance.toml"; pub const NOMI_VERSION: &str = "0.2.0"; pub const NOMI_NAME: &str = "Nomi"; diff --git a/crates/nomi-core/src/downloads/downloaders/assets.rs b/crates/nomi-core/src/downloads/downloaders/assets.rs index 74adc40..bb54d28 100644 --- a/crates/nomi-core/src/downloads/downloaders/assets.rs +++ b/crates/nomi-core/src/downloads/downloaders/assets.rs @@ -63,13 +63,17 @@ impl Downloader for Chunk { let (helper_sender, mut helper_receiver) = tokio::sync::mpsc::channel(100); let downloader = self.set.with_helper(helper_sender); Box::new(downloader).download(sender).await; + while let Some(result) = helper_receiver.recv().await { match result.0 { Ok(_) => self.ok += 1, Err(_) => self.err += 1, } } - info!("Downloaded Chunk OK: {} ERR: {}", self.ok, self.err); + + if self.ok != 0 && self.err != 0 { + info!("Downloaded Chunk OK: {} ERR: {}", self.ok, self.err); + } } } diff --git a/crates/nomi-core/src/downloads/progress.rs b/crates/nomi-core/src/downloads/progress.rs index ebd21b8..5f5a122 100644 --- a/crates/nomi-core/src/downloads/progress.rs +++ b/crates/nomi-core/src/downloads/progress.rs @@ -22,6 +22,7 @@ impl ProgressSender

for std::sync::mpsc::Sender

{ pub struct MappedSender { inner: Box>, mapper: Box T + Sync + Send>, + side_effect: Option>, } #[async_trait::async_trait] @@ -29,6 +30,10 @@ impl ProgressSender for MappedSender { async fn update(&self, data: I) { let mapped = (self.mapper)(data); self.inner.update(mapped).await; + + if let Some(side_effect) = self.side_effect.as_ref() { + (side_effect)(); + } } } @@ -40,8 +45,15 @@ impl MappedSender { Self { inner: sender, mapper: Box::new(mapper), + side_effect: None, } } + + #[must_use] + pub fn with_side_effect(mut self, side_effect: impl Fn() + Send + Sync + 'static) -> Self { + self.side_effect = Some(Box::new(side_effect)); + self + } } impl MappedSender> { @@ -49,6 +61,18 @@ impl MappedSender> { Self { inner: sender, mapper: Box::new(|value| Box::new(value) as Box), + side_effect: None, + } + } + + pub fn new_progress_mapper_with_side_effect( + sender: Box>>, + side_effect: impl Fn() + Send + Sync + 'static, + ) -> Self { + Self { + inner: sender, + mapper: Box::new(|value| Box::new(value) as Box), + side_effect: Some(Box::new(side_effect)), } } } diff --git a/crates/nomi-core/src/game_paths.rs b/crates/nomi-core/src/game_paths.rs index c0a0283..2c164ec 100644 --- a/crates/nomi-core/src/game_paths.rs +++ b/crates/nomi-core/src/game_paths.rs @@ -1,21 +1,76 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -use crate::MINECRAFT_DIR; +use crate::{ + instance::{Instance, InstanceProfileId}, + ASSETS_DIR, LIBRARIES_DIR, +}; #[derive(Debug, Clone)] pub struct GamePaths { + /// Game root directory. This is usually corresponds to the instance directory. pub game: PathBuf, + + /// Assets directory. pub assets: PathBuf, - pub version: PathBuf, + + /// Profile directory. + pub profile: PathBuf, + + /// Libraries directory. pub libraries: PathBuf, } -impl Default for GamePaths { - fn default() -> Self { +impl GamePaths { + pub fn from_id(id: InstanceProfileId) -> Self { + Self::from_instance_path(Instance::path_from_id(id.instance()), id.profile()) + } + + pub fn from_instance_path(instance: impl AsRef, profile_id: usize) -> Self { + let path = instance.as_ref(); + + Self { + game: path.to_path_buf(), + assets: ASSETS_DIR.into(), + profile: path.join("profiles").join(format!("{profile_id}")), + libraries: LIBRARIES_DIR.into(), + } + } + + pub fn make_absolute(self) -> anyhow::Result { + let current_dir = std::env::current_dir()?; + + let make_path_absolute = |path: PathBuf| if path.is_absolute() { path } else { current_dir.join(path) }; + + Ok(Self { + game: make_path_absolute(self.game), + assets: make_path_absolute(self.assets), + profile: make_path_absolute(self.profile), + libraries: make_path_absolute(self.libraries), + }) + } + + pub fn profile_config(&self) -> PathBuf { + self.profile.join("Profile.toml") + } + + pub fn manifest_file(&self, game_version: &str) -> PathBuf { + self.profile.join(format!("{game_version}.json")) + } + + pub fn natives_dir(&self) -> PathBuf { + self.profile.join("natives") + } + + pub fn version_jar_file(&self, game_version: &str) -> PathBuf { + self.profile.join(format!("{game_version}.jar")) + } + + pub fn minecraft(game_version: &str) -> Self { + const MINECRAFT_DIR: &str = "./minecraft"; Self { game: MINECRAFT_DIR.into(), assets: PathBuf::from(MINECRAFT_DIR).join("assets"), - version: PathBuf::from(MINECRAFT_DIR).join("versions").join("NOMI_DEFAULT"), + profile: PathBuf::from(MINECRAFT_DIR).join("versions").join(game_version), libraries: PathBuf::from(MINECRAFT_DIR).join("libraries"), } } diff --git a/crates/nomi-core/src/instance/builder_ext.rs b/crates/nomi-core/src/instance/builder_ext.rs index a83e434..3fe17f8 100644 --- a/crates/nomi-core/src/instance/builder_ext.rs +++ b/crates/nomi-core/src/instance/builder_ext.rs @@ -1,4 +1,4 @@ -use crate::loaders::{fabric::Fabric, forge::Forge, vanilla::Vanilla}; +use crate::loaders::{combined::VanillaCombinedDownloader, vanilla::Vanilla, ToLoaderProfile}; use super::launch::{LaunchInstanceBuilder, LaunchSettings}; @@ -8,19 +8,25 @@ pub trait LaunchInstanceBuilderExt { const _: Option> = None; +// Unique case where we do not have a profile. +// TODO: Maybe make a profile for Vanilla and get rid of manifest? impl LaunchInstanceBuilderExt for Vanilla { fn insert(&self, builder: LaunchInstanceBuilder) -> LaunchInstanceBuilder { builder } } -impl LaunchInstanceBuilderExt for Fabric { +// If the generic is `()` that means we are downloading `Vanilla` +impl LaunchInstanceBuilderExt for VanillaCombinedDownloader<()> { fn insert(&self, builder: LaunchInstanceBuilder) -> LaunchInstanceBuilder { - builder.profile(self.to_profile()) + builder } } -impl LaunchInstanceBuilderExt for Forge { +impl LaunchInstanceBuilderExt for L +where + L: ToLoaderProfile, +{ fn insert(&self, builder: LaunchInstanceBuilder) -> LaunchInstanceBuilder { builder.profile(self.to_profile()) } diff --git a/crates/nomi-core/src/instance/launch.rs b/crates/nomi-core/src/instance/launch.rs index ba73dd1..430dc93 100644 --- a/crates/nomi-core/src/instance/launch.rs +++ b/crates/nomi-core/src/instance/launch.rs @@ -10,11 +10,11 @@ use serde::{Deserialize, Serialize}; use tokio::process::Command; use tokio_stream::StreamExt; use tokio_util::codec::{FramedRead, LinesCodec}; -use tracing::{debug, error, info, trace, warn}; +use tracing::{error, info}; use crate::{ - downloads::Assets, fs::read_json_config, + game_paths::GamePaths, markers::Undefined, repository::{ java_runner::JavaRunner, @@ -25,8 +25,8 @@ use crate::{ use self::arguments::ArgumentsBuilder; use super::{ + loader::LoaderProfile, logs::{GameLogsEvent, GameLogsWriter}, - profile::LoaderProfile, }; pub mod arguments; @@ -38,16 +38,9 @@ pub const CLASSPATH_SEPARATOR: &str = ";"; #[cfg(not(windows))] pub const CLASSPATH_SEPARATOR: &str = ":"; -#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone, Hash)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct LaunchSettings { - pub assets: PathBuf, - pub java_bin: JavaRunner, - pub game_dir: PathBuf, - pub libraries_dir: PathBuf, - pub manifest_file: PathBuf, - pub natives_dir: PathBuf, - pub version_jar_file: PathBuf, - + pub java_runner: Option, pub version: String, pub version_type: VersionType, } @@ -60,51 +53,6 @@ pub struct LaunchInstance { } impl LaunchInstance { - #[tracing::instrument(skip(self), err)] - pub async fn delete(&self, delete_client: bool, delete_libraries: bool, delete_assets: bool) -> anyhow::Result<()> { - let manifest = read_json_config::(&self.settings.manifest_file).await?; - let arguments_builder = ArgumentsBuilder::new(self, &manifest).with_classpath(); - - if delete_client { - let _ = tokio::fs::remove_file(&self.settings.version_jar_file) - .await - .inspect(|()| { - debug!("Removed client successfully: {}", &self.settings.version_jar_file.display()); - }) - .inspect_err(|_| { - warn!("Cannot remove client: {}", &self.settings.version_jar_file.display()); - }); - } - - if delete_libraries { - for library in arguments_builder.classpath_as_slice() { - let _ = tokio::fs::remove_file(library) - .await - .inspect(|()| trace!("Removed library successfully: {}", library.display())) - .inspect_err(|_| warn!("Cannot remove library: {}", library.display())); - } - } - - if delete_assets { - let assets = read_json_config::(dbg!(&self - .settings - .assets - .join("indexes") - .join(format!("{}.json", manifest.asset_index.id)))) - .await?; - for asset in assets.objects.values() { - let path = &self.settings.assets.join("objects").join(&asset.hash[0..2]).join(&asset.hash); - - let _ = tokio::fs::remove_file(path) - .await - .inspect(|()| trace!("Removed asset successfully: {}", path.display())) - .inspect_err(|e| warn!("Cannot remove asset: {}. Error: {e}", path.display())); - } - } - - Ok(()) - } - pub fn loader_profile(&self) -> Option<&LoaderProfile> { self.loader_profile.as_ref() } @@ -117,10 +65,10 @@ impl LaunchInstance { &mut self.jvm_args } - fn process_natives(&self, natives: &[PathBuf]) -> anyhow::Result<()> { + fn process_natives(natives_dir: &Path, natives: &[PathBuf]) -> anyhow::Result<()> { for lib in natives { let reader = OpenOptions::new().read(true).open(lib)?; - std::fs::create_dir_all(&self.settings.natives_dir)?; + std::fs::create_dir_all(natives_dir)?; let mut archive = zip::ZipArchive::new(reader)?; @@ -138,7 +86,7 @@ impl LaunchInstance { }) .try_for_each(|lib| { let mut file = archive.by_name(&lib)?; - let mut out = File::create(self.settings.natives_dir.join(lib))?; + let mut out = File::create(natives_dir.join(lib))?; io::copy(&mut file, &mut out)?; Ok::<_, anyhow::Error>(()) @@ -149,12 +97,22 @@ impl LaunchInstance { } #[tracing::instrument(skip(self, logs_writer), err)] - pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner, logs_writer: &dyn GameLogsWriter) -> anyhow::Result<()> { - let manifest = read_json_config::(&self.settings.manifest_file).await?; + pub async fn launch( + &self, + paths: GamePaths, + user_data: UserData, + java_runner: &JavaRunner, + logs_writer: &dyn GameLogsWriter, + ) -> anyhow::Result<()> { + let paths = paths.make_absolute()?; + + let manifest = read_json_config::(paths.manifest_file(&self.settings.version)).await?; - let arguments_builder = ArgumentsBuilder::new(self, &manifest).with_classpath().with_userdata(user_data); + let arguments_builder = ArgumentsBuilder::new(&paths, self, &manifest).build_classpath().with_userdata(user_data); - self.process_natives(arguments_builder.get_native_libs())?; + let natives_dir = paths.natives_dir(); + let native_libs = arguments_builder.get_native_libs().to_vec(); + tokio::task::spawn_blocking(move || Self::process_natives(&natives_dir, &native_libs)).await??; let custom_jvm_arguments = arguments_builder.custom_jvm_arguments(); let manifest_jvm_arguments = arguments_builder.manifest_jvm_arguments(); @@ -170,30 +128,21 @@ impl LaunchInstance { let loader_game_arguments = loader_arguments.game_arguments(); let mut command = Command::new(java_runner.get()); - let command = command + let command = dbg!(command .args(custom_jvm_arguments) .args(loader_jvm_arguments) .args(manifest_jvm_arguments) .arg(main_class) .args(manifest_game_arguments) .args(loader_game_arguments) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - // Works incorrectly so let's ignore it for now. - // It will work when the instances are implemented. - // .current_dir(std::fs::canonicalize(MINECRAFT_DIR)?) - - // if matches!(manifest.arguments, Arguments::Old(_)) { - // let mut cp = arguments_builder.classpath_as_str().to_string(); - // cp.push_str(CLASSPATH_SEPARATOR); - // cp.push_str("./.nomi/launchwrapper-1.12.jar"); - // command.env("CLASSPATH", cp); - // } + .current_dir(&paths.game)) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); let mut child = command.spawn()?; let stdout = child.stdout.take().expect("child did not have a handle to stdout"); - let stderr = child.stderr.take().expect("child did not have a handle to stdout"); + let stderr = child.stderr.take().expect("child did not have a handle to stderr"); // let mut stdout_reader = BufReader::new(stdout).lines(); // let mut stderr_reader = BufReader::new(stderr).lines(); diff --git a/crates/nomi-core/src/instance/launch/arguments.rs b/crates/nomi-core/src/instance/launch/arguments.rs index ccfa2f0..9f845de 100644 --- a/crates/nomi-core/src/instance/launch/arguments.rs +++ b/crates/nomi-core/src/instance/launch/arguments.rs @@ -4,9 +4,10 @@ use itertools::Itertools; use tracing::info; use crate::{ + game_paths::GamePaths, instance::{ launch::{macros::replace, rules::is_library_passes}, - profile::LoaderProfile, + loader::LoaderProfile, }, markers::Undefined, maven_data::MavenArtifact, @@ -26,6 +27,7 @@ pub enum WithClasspath {} pub struct ArgumentsBuilder<'a, S = Undefined, U = Undefined> { instance: &'a LaunchInstance, manifest: &'a Manifest, + paths: &'a GamePaths, classpath: Vec, classpath_string: String, native_libs: Vec, @@ -65,10 +67,11 @@ impl<'a, 'b> LoaderArguments<'a, 'b> { } impl<'a> ArgumentsBuilder<'a, Undefined, Undefined> { - pub fn new(instance: &'a LaunchInstance, manifest: &'a Manifest) -> ArgumentsBuilder<'a, Undefined, Undefined> { + pub fn new(paths: &'a GamePaths, instance: &'a LaunchInstance, manifest: &'a Manifest) -> ArgumentsBuilder<'a, Undefined, Undefined> { ArgumentsBuilder { instance, manifest, + paths, classpath: Vec::new(), classpath_string: String::new(), native_libs: Vec::new(), @@ -80,11 +83,12 @@ impl<'a> ArgumentsBuilder<'a, Undefined, Undefined> { } impl<'a, U> ArgumentsBuilder<'a, Undefined, U> { - pub fn with_classpath(self) -> ArgumentsBuilder<'a, WithClasspath, U> { + pub fn build_classpath(self) -> ArgumentsBuilder<'a, WithClasspath, U> { let (classpath, native_libs) = self.classpath(); ArgumentsBuilder { instance: self.instance, manifest: self.manifest, + paths: self.paths, user_data: self.user_data, classpath_string: itertools::intersperse(classpath.iter().map(|p| p.display().to_string()), CLASSPATH_SEPARATOR.to_string()) .collect::(), @@ -101,6 +105,7 @@ impl<'a, S> ArgumentsBuilder<'a, S, Undefined> { ArgumentsBuilder { instance: self.instance, manifest: self.manifest, + paths: self.paths, user_data, classpath_string: self.classpath_string, classpath: self.classpath, @@ -148,10 +153,13 @@ impl<'a> ArgumentsBuilder<'a, WithClasspath, WithUserData> { |JvmArguments(jvm), _| jvm.clone(), |_| { vec![ - format!("-Djava.library.path={}", &self.instance.settings.natives_dir.display()), + format!("-Djava.library.path={}", self.paths.natives_dir().display()), "-Dminecraft.launcher.brand=${launcher_name}".into(), "-Dminecraft.launcher.version=${launcher_version}".into(), - format!("-Dminecraft.client.jar={}", &self.instance.settings.version_jar_file.display()), + format!( + "-Dminecraft.client.jar={}", + self.paths.version_jar_file(&self.instance.settings.version).display() + ), "-cp".to_string(), self.classpath_as_str().to_owned(), ] @@ -168,11 +176,11 @@ impl<'a> ArgumentsBuilder<'a, WithClasspath, WithUserData> { fn parse_args_from_str(&self, source: &str) -> String { replace!(source, - "${assets_root}" => &path_to_string(&self.instance.settings.assets), - "${game_assets}" => &path_to_string(&self.instance.settings.assets), - "${game_directory}" => &path_to_string(&self.instance.settings.game_dir), - "${natives_directory}" => &path_to_string(&self.instance.settings.natives_dir), - "${library_directory}" => &path_to_string(&self.instance.settings.libraries_dir), + "${assets_root}" => &path_to_string(&self.paths.assets), + "${game_assets}" => &path_to_string(&self.paths.assets), + "${game_directory}" => &path_to_string(&self.paths.game), + "${natives_directory}" => &path_to_string(self.paths.natives_dir()), + "${library_directory}" => &path_to_string(&self.paths.libraries), "${launcher_name}" => NOMI_NAME, "${launcher_version}" => NOMI_VERSION, "${auth_access_token}" => self.user_data @@ -239,7 +247,7 @@ impl<'a, S, U> ArgumentsBuilder<'a, S, U> { } } - let mut classpath = vec![Some(self.instance.settings.version_jar_file.clone())]; + let mut classpath = vec![Some(self.paths.version_jar_file(&self.instance.settings.version))]; let mut native_libs = vec![]; self.manifest @@ -268,13 +276,13 @@ impl<'a, S, U> ArgumentsBuilder<'a, S, U> { }) }) .and_then(|artifact| artifact.path.as_ref()) - .map(|path| self.instance.settings.libraries_dir.join(path)), + .map(|path| self.paths.libraries.join(path)), lib.downloads .classifiers .as_ref() .and_then(|natives| match_natives(natives)) .and_then(|native_lib| native_lib.path.as_ref()) - .map(|path| self.instance.settings.libraries_dir.join(path)), + .map(|path| self.paths.libraries.join(path)), ) }) .for_each(|(lib, native)| { @@ -291,7 +299,7 @@ impl<'a, S, U> ArgumentsBuilder<'a, S, U> { .loader_profile .as_ref() .map(|p| &p.libraries) - .map(|libs| libs.iter().map(|lib| self.instance.settings.libraries_dir.join(&lib.jar))) + .map(|libs| libs.iter().map(|lib| self.paths.libraries.join(&lib.jar))) { classpath.extend(libs); } diff --git a/crates/nomi-core/src/instance/loader.rs b/crates/nomi-core/src/instance/loader.rs new file mode 100644 index 0000000..91c046e --- /dev/null +++ b/crates/nomi-core/src/instance/loader.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + configs::profile::Loader, + repository::{simple_args::SimpleArgs, simple_lib::SimpleLib}, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct LoaderProfile { + pub loader: Loader, + pub main_class: String, + pub args: SimpleArgs, + pub libraries: Vec, +} diff --git a/crates/nomi-core/src/instance/version_marker.rs b/crates/nomi-core/src/instance/marker.rs similarity index 75% rename from crates/nomi-core/src/instance/version_marker.rs rename to crates/nomi-core/src/instance/marker.rs index 9759d5f..ef3e327 100644 --- a/crates/nomi-core/src/instance/version_marker.rs +++ b/crates/nomi-core/src/instance/marker.rs @@ -4,11 +4,11 @@ use crate::downloads::traits::{DownloadResult, Downloader}; use super::builder_ext::LaunchInstanceBuilderExt; -pub trait Version: LaunchInstanceBuilderExt + Downloader + Debug + Send + Sync { +pub trait ProfileDownloader: LaunchInstanceBuilderExt + Downloader + Debug + Send + Sync { fn into_downloader(self: Box) -> Box>; } -impl Version for T +impl ProfileDownloader for T where T: LaunchInstanceBuilderExt + Downloader + Debug + Send + Sync + 'static, { diff --git a/crates/nomi-core/src/instance/mod.rs b/crates/nomi-core/src/instance/mod.rs index f1fa6c6..7e2756d 100644 --- a/crates/nomi-core/src/instance/mod.rs +++ b/crates/nomi-core/src/instance/mod.rs @@ -1,52 +1,182 @@ -use typed_builder::TypedBuilder; - pub mod builder_ext; pub mod launch; +pub mod loader; pub mod logs; -pub mod profile; -pub mod version_marker; +pub mod marker; +mod profile; + +use std::path::{Path, PathBuf}; -use crate::{downloads::downloaders::assets::AssetsDownloader, game_paths::GamePaths, state::get_launcher_manifest}; +pub use profile::*; +use serde::{Deserialize, Serialize}; +use tracing::{error, warn}; -use self::{ - launch::{LaunchInstance, LaunchInstanceBuilder, LaunchSettings}, - version_marker::Version, +use crate::{ + configs::profile::{Loader, VersionProfile}, + fs::{read_toml_config_sync, write_toml_config, write_toml_config_sync}, + INSTANCES_DIR, INSTANCE_CONFIG, }; -#[derive(Debug, TypedBuilder)] +/// Loads all instances in the [`INSTANCES_DIR`](crate::consts::INSTANCES_DIR) +pub fn load_instances() -> anyhow::Result> { + let dir = std::fs::read_dir(INSTANCES_DIR)?; + + let mut instances = Vec::new(); + + for entry in dir { + let Ok(entry) = entry.inspect_err(|error| error!(%error, "Cannot read instance directory")) else { + continue; + }; + + if !entry.path().is_dir() { + continue; + } + + let path = entry.path().join(INSTANCE_CONFIG); + + let instance = read_toml_config_sync::(path)?; + + instances.push(instance); + } + + Ok(instances) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct Instance { - instance: Box, - pub game_paths: GamePaths, - pub version: String, - pub name: String, + name: String, + id: usize, + main_profile: Option, + profiles: Vec, } impl Instance { - pub fn instance(self) -> Box { - self.instance + pub fn new(name: impl Into, id: usize) -> Self { + Self { + name: name.into(), + id, + main_profile: None, + profiles: Vec::new(), + } } - pub async fn assets(&self) -> anyhow::Result { - let manifest = get_launcher_manifest().await?; - let version_manifest = manifest.get_version_manifest(&self.version).await?; + pub fn set_main_profile(&mut self, main_profile_id: InstanceProfileId) { + self.main_profile = Some(main_profile_id); + } - AssetsDownloader::new( - version_manifest.asset_index.url, - version_manifest.asset_index.id, - self.game_paths.assets.join("objects"), - self.game_paths.assets.join("indexes"), - ) - .await + pub fn add_profile(&mut self, payload: ProfilePayload) { + self.profiles.push(payload); } - #[must_use] - pub fn launch_instance(&self, settings: LaunchSettings, jvm_args: Option>) -> LaunchInstance { - let builder = LaunchInstanceBuilder::new().settings(settings); - let builder = match jvm_args { - Some(jvm) => builder.jvm_args(jvm), - None => builder, - }; + pub fn find_profile(&self, id: InstanceProfileId) -> Option<&ProfilePayload> { + self.profiles().iter().find(|p| p.id == id) + } + + pub fn find_profile_mut(&mut self, id: InstanceProfileId) -> Option<&mut ProfilePayload> { + self.profiles_mut().iter_mut().find(|p| p.id == id) + } + + pub fn remove_profile(&mut self, id: InstanceProfileId) -> Option { + let opt = self.profiles.iter().position(|p| p.id == id).map(|idx| self.profiles.remove(idx)); + + if opt.is_none() { + warn!(?id, "Cannot find a profile to remove"); + } + + opt + } + + /// Generate id for the next profile in this instance + pub fn next_id(&self) -> InstanceProfileId { + match &self.profiles.iter().max_by_key(|profile| profile.id.1) { + Some(profile) => InstanceProfileId::new(profile.id.0, profile.id.1 + 1), + None => InstanceProfileId::new(self.id, 0), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn id(&self) -> usize { + self.id + } + + pub fn main_profile(&self) -> Option { + self.main_profile + } + + pub fn main_profile_mut(&mut self) -> &mut Option { + &mut self.main_profile + } + + pub fn profiles(&self) -> &[ProfilePayload] { + &self.profiles + } + + pub fn profiles_mut(&mut self) -> &mut Vec { + &mut self.profiles + } + + pub async fn write(&self) -> anyhow::Result<()> { + write_toml_config(&self, self.path().join(".nomi/Instance.toml")).await + } + + pub fn write_blocking(&self) -> anyhow::Result<()> { + write_toml_config_sync(&self, self.path().join(".nomi/Instance.toml")) + } + + pub fn path(&self) -> PathBuf { + Self::path_from_id(self.id) + } + + pub fn path_from_id(id: usize) -> PathBuf { + PathBuf::from(INSTANCES_DIR).join(format!("{id}")) + } +} + +/// Represent a unique identifier of a profile. +/// +/// First number is the instance id and the second number is the profile id. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +pub struct InstanceProfileId(usize, usize); + +impl InstanceProfileId { + pub const ZERO: Self = Self(0, 0); + + pub fn new(instance: usize, profile: usize) -> Self { + Self(instance, profile) + } + + pub fn instance(&self) -> usize { + self.0 + } + + pub fn profile(&self) -> usize { + self.1 + } +} + +/// Information about profile. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct ProfilePayload { + pub id: InstanceProfileId, + pub name: String, + pub loader: Loader, + pub version: String, + pub is_downloaded: bool, + pub path: PathBuf, +} - self.instance.insert(builder).build() +impl ProfilePayload { + pub fn from_version_profile(profile: &VersionProfile, path: &Path) -> Self { + Self { + id: profile.id, + name: profile.name.clone(), + loader: profile.loader().clone(), + version: profile.version().to_owned(), + is_downloaded: profile.is_downloaded(), + path: path.to_path_buf(), + } } } diff --git a/crates/nomi-core/src/instance/profile.rs b/crates/nomi-core/src/instance/profile.rs index 91c046e..ef74597 100644 --- a/crates/nomi-core/src/instance/profile.rs +++ b/crates/nomi-core/src/instance/profile.rs @@ -1,14 +1,80 @@ -use serde::{Deserialize, Serialize}; +use tracing::{debug, warn}; +use typed_builder::TypedBuilder; -use crate::{ - configs::profile::Loader, - repository::{simple_args::SimpleArgs, simple_lib::SimpleLib}, +use crate::{downloads::downloaders::assets::AssetsDownloader, game_paths::GamePaths, state::get_launcher_manifest}; + +use super::{ + launch::{LaunchInstance, LaunchInstanceBuilder, LaunchSettings}, + marker::ProfileDownloader, }; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct LoaderProfile { - pub loader: Loader, - pub main_class: String, - pub args: SimpleArgs, - pub libraries: Vec, +#[derive(Debug, TypedBuilder)] +pub struct Profile { + downloader: Box, + pub game_paths: GamePaths, + pub version: String, + pub name: String, +} + +impl Profile { + pub fn downloader(self) -> Box { + self.downloader + } + + pub async fn assets(&self) -> anyhow::Result { + let manifest = get_launcher_manifest().await?; + let version_manifest = manifest.get_version_manifest(&self.version).await?; + + AssetsDownloader::new( + version_manifest.asset_index.url, + version_manifest.asset_index.id, + self.game_paths.assets.join("objects"), + self.game_paths.assets.join("indexes"), + ) + .await + } + + #[must_use] + pub fn launch_instance(&self, settings: LaunchSettings, jvm_args: Option>) -> LaunchInstance { + let builder = LaunchInstanceBuilder::new().settings(settings); + let builder = match jvm_args { + Some(jvm) => builder.jvm_args(jvm), + None => builder, + }; + + self.downloader.insert(builder).build() + } +} + +#[tracing::instrument] +pub async fn delete_profile(paths: GamePaths, game_version: &str) { + let path = paths.version_jar_file(game_version); + let _ = tokio::fs::remove_file(&path) + .await + .inspect(|()| { + debug!("Removed client successfully: {}", &path.display()); + }) + .inspect_err(|_| { + warn!("Cannot remove client: {}", &path.display()); + }); + + let path = &paths.profile_config(); + let _ = tokio::fs::remove_file(&path) + .await + .inspect(|()| { + debug!(path = %&path.display(), "Removed profile config successfully"); + }) + .inspect_err(|_| { + warn!(path = %&path.display(), "Cannot profile config"); + }); + + let path = &paths.profile; + let _ = tokio::fs::remove_dir(&path) + .await + .inspect(|()| { + debug!(path = %&path.display(), "Removed profile directory successfully"); + }) + .inspect_err(|_| { + warn!(path = %&path.display(), "Cannot profile directory"); + }); } diff --git a/crates/nomi-core/src/loaders/combined.rs b/crates/nomi-core/src/loaders/combined.rs new file mode 100644 index 0000000..c4a7da3 --- /dev/null +++ b/crates/nomi-core/src/loaders/combined.rs @@ -0,0 +1,110 @@ +use std::{fmt::Debug, future::Future}; + +use crate::{ + downloads::{ + progress::ProgressSender, + traits::{DownloadResult, Downloader}, + DownloadQueue, + }, + game_paths::GamePaths, + instance::marker::ProfileDownloader, + PinnedFutureWithBounds, +}; + +use super::{vanilla::Vanilla, ToLoaderProfile}; + +pub struct VanillaCombinedDownloader { + version: String, + game_paths: GamePaths, + vanilla: Vanilla, + loader: T, +} + +impl Debug for VanillaCombinedDownloader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VanillaCombinedDownloader") + .field("version", &self.version) + .field("game_paths", &self.game_paths) + .field("vanilla", &self.vanilla) + .field("loader", &"(loader)") + .finish() + } +} + +impl VanillaCombinedDownloader<()> { + pub async fn new(game_version: impl Into, game_paths: GamePaths) -> anyhow::Result { + let version = game_version.into(); + let vanilla = Vanilla::new(&version, game_paths.clone()).await?; + + Ok(Self { + version, + game_paths, + vanilla, + loader: (), + }) + } + + pub async fn with_loader(self, fun: F) -> anyhow::Result> + where + F: FnOnce(String, GamePaths) -> Fut, + Fut: Future>, + T: ProfileDownloader, + { + let loader = (fun)(self.version.clone(), self.game_paths.clone()).await?; + + Ok(VanillaCombinedDownloader { + version: self.version, + game_paths: self.game_paths, + vanilla: self.vanilla, + loader, + }) + } +} + +impl ToLoaderProfile for VanillaCombinedDownloader { + fn to_profile(&self) -> crate::instance::loader::LoaderProfile { + self.loader.to_profile() + } +} + +#[async_trait::async_trait] +impl Downloader for VanillaCombinedDownloader { + type Data = DownloadResult; + + fn total(&self) -> u32 { + self.vanilla.total() + self.loader.total() + } + + async fn download(self: Box, sender: &dyn ProgressSender) { + let downloader = DownloadQueue::new().with_downloader(self.vanilla).with_downloader(self.loader); + let downloader = Box::new(downloader); + downloader.download(sender).await; + } + + fn io(&self) -> PinnedFutureWithBounds> { + let vanilla_io = self.vanilla.io(); + let loader_io = self.loader.io(); + + Box::pin(async move { + vanilla_io.await?; + loader_io.await + }) + } +} + +#[async_trait::async_trait] +impl Downloader for VanillaCombinedDownloader<()> { + type Data = DownloadResult; + + fn total(&self) -> u32 { + self.vanilla.total() + } + + async fn download(self: Box, sender: &dyn ProgressSender) { + Box::new(self.vanilla).download(sender).await; + } + + fn io(&self) -> PinnedFutureWithBounds> { + self.vanilla.io() + } +} diff --git a/crates/nomi-core/src/loaders/fabric.rs b/crates/nomi-core/src/loaders/fabric.rs index f016a3d..48dd7f3 100644 --- a/crates/nomi-core/src/loaders/fabric.rs +++ b/crates/nomi-core/src/loaders/fabric.rs @@ -15,7 +15,7 @@ use crate::{ }, fs::write_to_file, game_paths::GamePaths, - instance::profile::LoaderProfile, + instance::loader::LoaderProfile, maven_data::{MavenArtifact, MavenData}, repository::{ fabric_meta::FabricVersions, @@ -27,6 +27,8 @@ use crate::{ PinnedFutureWithBounds, }; +use super::ToLoaderProfile; + #[derive(Debug)] pub struct Fabric { pub game_version: String, @@ -87,8 +89,10 @@ impl Fabric { libraries_downloader, }) } +} - pub fn to_profile(&self) -> LoaderProfile { +impl ToLoaderProfile for Fabric { + fn to_profile(&self) -> LoaderProfile { LoaderProfile { loader: Loader::Fabric { version: Some(self.fabric_version.clone()), @@ -132,7 +136,7 @@ impl Downloader for Fabric { } fn io(&self) -> PinnedFutureWithBounds> { - let version_path = self.game_paths.version.clone(); + let version_path = self.game_paths.profile.clone(); let profile = self.profile.clone(); let id = self.profile.id.clone(); diff --git a/crates/nomi-core/src/loaders/forge.rs b/crates/nomi-core/src/loaders/forge.rs index f0a29a9..3679a82 100644 --- a/crates/nomi-core/src/loaders/forge.rs +++ b/crates/nomi-core/src/loaders/forge.rs @@ -22,7 +22,7 @@ use crate::{ DownloadQueue, FileDownloader, LibrariesDownloader, LibrariesMapper, }, game_paths::GamePaths, - instance::{launch::CLASSPATH_SEPARATOR, profile::LoaderProfile}, + instance::{launch::CLASSPATH_SEPARATOR, loader::LoaderProfile}, loaders::vanilla::VanillaLibrariesMapper, maven_data::{MavenArtifact, MavenData}, repository::{ @@ -34,6 +34,8 @@ use crate::{ PinnedFutureWithBounds, DOT_NOMI_TEMP_DIR, }; +use super::ToLoaderProfile; + const FORGE_REPO_URL: &str = "https://maven.minecraftforge.net"; const _NEO_FORGE_REPO_URL: &str = "https://maven.neoforged.net/releases/"; @@ -59,21 +61,13 @@ pub struct Forge { game_version: String, forge_version: String, game_paths: GamePaths, + java_runner: JavaRunner, library_data: Option, processors_data: Option, } impl Forge { - pub fn to_profile(&self) -> LoaderProfile { - LoaderProfile { - loader: Loader::Forge, - main_class: self.profile.main_class().to_string(), - args: self.profile.simple_args(), - libraries: self.profile.simple_libraries(), - } - } - #[tracing::instrument(skip_all, err)] pub async fn get_versions(game_version: impl Into) -> anyhow::Result> { let game_version = game_version.into(); @@ -186,7 +180,12 @@ impl Forge { } #[tracing::instrument(skip(version), fields(game_version) err)] - pub async fn new(version: impl Into, forge_version: ForgeVersion, game_paths: GamePaths) -> anyhow::Result { + pub async fn new( + version: impl Into, + forge_version: ForgeVersion, + game_paths: GamePaths, + java_runner: JavaRunner, + ) -> anyhow::Result { let game_version: String = version.into(); tracing::Span::current().record("game_version", &game_version); @@ -281,12 +280,24 @@ impl Forge { game_version, forge_version, game_paths, + java_runner, library_data, processors_data, }) } } +impl ToLoaderProfile for Forge { + fn to_profile(&self) -> LoaderProfile { + LoaderProfile { + loader: Loader::Forge, + main_class: self.profile.main_class().to_string(), + args: self.profile.simple_args(), + libraries: self.profile.simple_libraries(), + } + } +} + fn forge_installer_path(game_version: &str, forge_version: &str) -> PathBuf { Path::new(DOT_NOMI_TEMP_DIR).join(format!("{game_version}-{forge_version}.jar")) } @@ -358,7 +369,7 @@ impl Downloader for Forge { if let Some(processors_data) = processors_data { processors_data - .run_processors(&java_runner, &game_version, &forge_version, &game_paths) + .run_processors(&java_runner, &game_version, &forge_version, game_paths) .await?; } @@ -377,7 +388,7 @@ impl Downloader for Forge { self.game_version.clone(), self.forge_version.clone(), self.installer_path(), - JavaRunner::nomi_default(), + self.java_runner.clone(), self.game_paths.clone(), self.processors_data.clone(), self.library_data.clone(), @@ -412,7 +423,7 @@ impl ProcessorsData { client = "client", server = "" "MINECRAFT_JAR" : - client = game_paths.version.join(format!("{game_version}.jar")).to_string_lossy(), + client = game_paths.version_jar_file(game_version).to_string_lossy(), server = "" "MINECRAFT_VERSION": client = game_version, @@ -438,7 +449,7 @@ impl ProcessorsData { .join(CLASSPATH_SEPARATOR) } - #[tracing::instrument] + #[tracing::instrument(err)] async fn get_processor_main_class(processor_jar: PathBuf) -> anyhow::Result { tokio::task::spawn_blocking(|| { let file = std::fs::File::open(processor_jar)?; @@ -501,17 +512,15 @@ impl ProcessorsData { java_runner: &JavaRunner, game_version: &str, forge_version: &str, - game_paths: &GamePaths, + game_paths: GamePaths, ) -> anyhow::Result<()> { - self.apply_data_rules(game_version, forge_version, game_paths); + // let game_paths = game_paths + // .make_absolute() + // .inspect_err(|error| error!(%error, "Failed to make `game_paths` absolute"))?; + + self.apply_data_rules(game_version, forge_version, &game_paths); - let total = self - .processors - .iter() - .filter(|p| p.sides.as_ref().is_some_and(|sides| sides.iter().any(|s| s == "client"))) - .count(); let mut ok = 0; - let mut err = 0; for mut processor in self.processors { if processor.sides.as_ref().is_some_and(|sides| !sides.iter().any(|s| s == "client")) { @@ -528,19 +537,19 @@ impl ProcessorsData { let output = dbg!(Command::new(java_runner.get()).arg("-cp").arg(classpath).arg(main_class).args(arguments)) .output() - .await?; + .await + .inspect_err(|error| error!(%error, "Cannot run the command"))?; if output.status.success() { ok += 1; info!("Processor finished successfully"); } else { - err += 1; let error = String::from_utf8_lossy(&output.stderr); - error!(error = %error, "Processor failed"); + bail!("Processor failed, error: {error}") } } - info!(total, ok, err, "Finished processors execution"); + info!(ok, "Finished processors execution"); Ok(()) } @@ -770,6 +779,8 @@ pub struct ForgeOldLibrary { mod tests { use tracing::{debug, Level}; + use crate::instance::InstanceProfileId; + use super::*; #[tokio::test] @@ -780,10 +791,24 @@ mod tests { #[tokio::test] async fn create_forge_test() { - let recommended = Forge::new("1.7.10", ForgeVersion::Recommended, GamePaths::default()).await.unwrap(); + let recommended = Forge::new( + "1.7.10", + ForgeVersion::Recommended, + GamePaths::from_id(InstanceProfileId::ZERO), + JavaRunner::from_environment(), + ) + .await + .unwrap(); println!("{recommended:#?}"); - let latest = Forge::new("1.19.2", ForgeVersion::Latest, GamePaths::default()).await.unwrap(); + let latest = Forge::new( + "1.19.2", + ForgeVersion::Latest, + GamePaths::from_id(InstanceProfileId::ZERO), + JavaRunner::from_environment(), + ) + .await + .unwrap(); println!("{latest:#?}"); } @@ -793,7 +818,14 @@ mod tests { debug!("Test"); - let recommended = Forge::new("1.7.10", ForgeVersion::Recommended, GamePaths::default()).await.unwrap(); + let recommended = Forge::new( + "1.7.10", + ForgeVersion::Recommended, + GamePaths::from_id(InstanceProfileId::ZERO), + JavaRunner::from_environment(), + ) + .await + .unwrap(); println!("{recommended:#?}"); let io = recommended.io(); diff --git a/crates/nomi-core/src/loaders/mod.rs b/crates/nomi-core/src/loaders/mod.rs index bf21a6b..59baf14 100644 --- a/crates/nomi-core/src/loaders/mod.rs +++ b/crates/nomi-core/src/loaders/mod.rs @@ -1,3 +1,10 @@ +use crate::instance::loader::LoaderProfile; + +pub mod combined; pub mod fabric; pub mod forge; pub mod vanilla; + +pub trait ToLoaderProfile { + fn to_profile(&self) -> LoaderProfile; +} diff --git a/crates/nomi-core/src/loaders/vanilla.rs b/crates/nomi-core/src/loaders/vanilla.rs index 9cb36ae..6a5c0ac 100644 --- a/crates/nomi-core/src/loaders/vanilla.rs +++ b/crates/nomi-core/src/loaders/vanilla.rs @@ -52,7 +52,7 @@ impl Vanilla { .with_downloader( FileDownloader::new( manifest.downloads.client.url.clone(), - game_paths.version.join(format!("{}.jar", manifest.id)), + game_paths.profile.join(format!("{}.jar", manifest.id)), ) .into_retry(), ); @@ -122,7 +122,7 @@ impl Downloader for Vanilla { } fn io(&self) -> PinnedFutureWithBounds> { - let versions_path = self.game_paths.version.clone(); + let versions_path = self.game_paths.profile.clone(); let manifest_id = self.manifest.id.clone(); let manifest_res = serde_json::to_string_pretty(&self.manifest); diff --git a/crates/nomi-core/src/maven_data.rs b/crates/nomi-core/src/maven_data.rs index 6af9ef6..2350695 100644 --- a/crates/nomi-core/src/maven_data.rs +++ b/crates/nomi-core/src/maven_data.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display, path::PathBuf}; +use std::{fmt::Display, path::PathBuf, sync::LazyLock}; use itertools::Itertools; use regex::Regex; @@ -57,11 +57,13 @@ pub struct MavenArtifact { impl MavenArtifact { #[must_use] - #[allow(clippy::missing_panics_doc)] pub fn new(artifact: &str) -> Self { - // PANICS: This will never panic because the pattern is valid. - let regex = Regex::new(r"(?P[^:]*):(?P[^:]*):(?P[^@:]*)(?::(?P.*))?(?:@(?P.*))?").unwrap(); - regex.captures(artifact).map_or_else( + static REGEX: LazyLock = LazyLock::new(|| { + // PANICS: This will never panic because the pattern is valid. + Regex::new(r"(?P[^:]*):(?P[^:]*):(?P[^@:]*)(?::(?P.*))?(?:@(?P.*))?").unwrap() + }); + + REGEX.captures(artifact).map_or_else( || { error!(artifact, "No values captured. Using provided artifact as a group"); MavenArtifact { diff --git a/crates/nomi-core/tests/download_test.rs b/crates/nomi-core/tests/download_test.rs index c6fd803..110216f 100644 --- a/crates/nomi-core/tests/download_test.rs +++ b/crates/nomi-core/tests/download_test.rs @@ -1,4 +1,4 @@ -use nomi_core::{downloads::traits::Downloader, game_paths::GamePaths, instance::Instance, loaders::vanilla::Vanilla}; +use nomi_core::{downloads::traits::Downloader, game_paths::GamePaths, instance::Profile, loaders::vanilla::Vanilla}; use tracing::Level; #[tokio::test] @@ -11,20 +11,20 @@ async fn download_test() { let game_paths = GamePaths { game: "./minecraft".into(), assets: "./minecraft/assets".into(), - version: ("./minecraft/versions/1.18.2".into()), + profile: ("./minecraft/versions/1.18.2".into()), libraries: "./minecraft/libraries".into(), }; - let instance = Instance::builder() + let instance = Profile::builder() .version("1.18.2".into()) - .instance(Box::new(Vanilla::new("1.18.2", game_paths.clone()).await.unwrap())) + .downloader(Box::new(Vanilla::new("1.18.2", game_paths.clone()).await.unwrap())) .game_paths(game_paths) .name("1.18.2-test".into()) .build(); Box::new(instance.assets().await.unwrap()).download(&tx).await; - let version = instance.instance(); + let version = instance.downloader(); let fut = version.io(); version.download(&tx).await; diff --git a/crates/nomi-core/tests/fabric_test.rs b/crates/nomi-core/tests/fabric_test.rs index a9cffef..0a0efda 100644 --- a/crates/nomi-core/tests/fabric_test.rs +++ b/crates/nomi-core/tests/fabric_test.rs @@ -3,7 +3,7 @@ use nomi_core::{ instance::{ launch::{arguments::UserData, LaunchSettings}, logs::PrintLogs, - Instance, + Profile, }, loaders::fabric::Fabric, repository::java_runner::JavaRunner, @@ -17,14 +17,14 @@ async fn vanilla_test() { let game_paths = GamePaths { game: "./minecraft".into(), assets: "./minecraft/assets".into(), - version: "./minecraft/versions/1.20".into(), + profile: "./minecraft/versions/1.20".into(), libraries: "./minecraft/libraries".into(), }; - let builder = Instance::builder() + let builder = Profile::builder() .version("1.20".into()) .game_paths(game_paths.clone()) - .instance(Box::new(Fabric::new("1.20", None::, game_paths).await.unwrap())) + .downloader(Box::new(Fabric::new("1.20", None::, game_paths.clone()).await.unwrap())) // .instance(Inner::vanilla("1.20").await.unwrap()) .name("1.20-fabric-test".into()) .build(); @@ -34,20 +34,14 @@ async fn vanilla_test() { // _assets.download().await.unwrap(); // builder.download().await.unwrap(); - let mc_dir = std::env::current_dir().unwrap().join("minecraft"); - let settings = LaunchSettings { - assets: mc_dir.join("assets"), - game_dir: mc_dir.clone(), - java_bin: JavaRunner::default(), - libraries_dir: mc_dir.clone().join("libraries"), - manifest_file: mc_dir.clone().join("versions/1.20/1.20.json"), - natives_dir: mc_dir.clone().join("versions/1.20/natives"), - version_jar_file: mc_dir.join("versions/1.20/1.20.jar"), + java_runner: None, version: "1.20".to_string(), version_type: nomi_core::repository::manifest::VersionType::Release, }; let l = builder.launch_instance(settings, None); - l.launch(UserData::default(), &JavaRunner::default(), &PrintLogs).await.unwrap(); + l.launch(game_paths, UserData::default(), &JavaRunner::default(), &PrintLogs) + .await + .unwrap(); } diff --git a/crates/nomi-core/tests/forge_new_test.rs b/crates/nomi-core/tests/forge_new_test.rs index 8ce2682..952821e 100644 --- a/crates/nomi-core/tests/forge_new_test.rs +++ b/crates/nomi-core/tests/forge_new_test.rs @@ -1,42 +1,38 @@ -use std::path::PathBuf; use nomi_core::{ configs::profile::{ProfileState, VersionProfile}, + downloads::traits::Downloader, game_paths::GamePaths, instance::{ launch::{arguments::UserData, LaunchSettings}, logs::PrintLogs, - Instance, + InstanceProfileId, Profile, }, loaders::forge::{Forge, ForgeVersion}, repository::java_runner::JavaRunner, - DOT_NOMI_JAVA_EXECUTABLE, MINECRAFT_DIR, }; #[tokio::test] async fn forge_test() { let _guard = tracing::subscriber::set_default(tracing_subscriber::fmt().pretty().finish()); - let current = std::env::current_dir().unwrap(); - let (tx, _) = tokio::sync::mpsc::channel(100); - let game_paths = GamePaths { - version: PathBuf::from(MINECRAFT_DIR).join("versions").join("forge-test"), - ..Default::default() - }; + let game_paths = GamePaths::from_id(InstanceProfileId::ZERO); - let instance = Instance::builder() + let instance = Profile::builder() .name("forge-test".into()) - .version("1.20.1".into()) + .version("1.19.2".into()) .game_paths(game_paths.clone()) - .instance(Box::new(Forge::new("1.20.1", ForgeVersion::Recommended, game_paths).await.unwrap())) - // .instance(Box::new(Vanilla::new("1.20.1", game_paths.clone()).await.unwrap())) + .downloader(Box::new( + Forge::new("1.19.2", ForgeVersion::Recommended, game_paths.clone(), JavaRunner::from_environment()) + .await + .unwrap(), + )) + // .downloader(Box::new(Vanilla::new("1.19.2", game_paths.clone()).await.unwrap())) .build(); - let mc_dir = current.join("minecraft"); - - // let vanilla = Box::new(Vanilla::new("1.20.1", game_paths.clone()).await.unwrap()); + // let vanilla = Box::new(Vanilla::new("1.19.2", game_paths.clone()).await.unwrap()); // let io = vanilla.io(); // vanilla.download(&tx).await; @@ -44,25 +40,20 @@ async fn forge_test() { // io.await.unwrap(); let settings = LaunchSettings { - assets: mc_dir.join("assets"), - game_dir: mc_dir.clone(), - java_bin: JavaRunner::default(), - libraries_dir: mc_dir.clone().join("libraries"), - manifest_file: mc_dir.clone().join("versions/forge-test/1.20.1.json"), - natives_dir: mc_dir.clone().join("versions/forge-test/natives"), - version_jar_file: mc_dir.join("versions/forge-test/1.20.1.jar"), - version: "1.20.1".to_string(), + java_runner: None, + + version: "1.19.2".to_string(), version_type: nomi_core::repository::manifest::VersionType::Release, }; let launch = instance.launch_instance(settings, None); - // let assets = instance.assets().await.unwrap(); - // let assets_io = assets.io(); - // Box::new(assets).download(&tx).await; - // assets_io.await.unwrap(); + let assets = instance.assets().await.unwrap(); + let assets_io = assets.io(); + Box::new(assets).download(&tx).await; + assets_io.await.unwrap(); - let instance = instance.instance(); + let instance = instance.downloader(); let io_fut = instance.io(); instance.download(&tx).await; @@ -70,17 +61,13 @@ async fn forge_test() { io_fut.await.unwrap(); let profile = VersionProfile::builder() - .id(1) + .id(InstanceProfileId::ZERO) .name("forge-test".into()) .state(ProfileState::downloaded(launch)) .build(); dbg!(profile) - .launch( - UserData::default(), - &JavaRunner::path(PathBuf::from(DOT_NOMI_JAVA_EXECUTABLE)), - &PrintLogs, - ) + .launch(game_paths, UserData::default(), &JavaRunner::from_environment(), &PrintLogs) .await .unwrap(); } diff --git a/crates/nomi-core/tests/forge_old_test.rs b/crates/nomi-core/tests/forge_old_test.rs index 76dc734..8eea8fb 100644 --- a/crates/nomi-core/tests/forge_old_test.rs +++ b/crates/nomi-core/tests/forge_old_test.rs @@ -7,36 +7,32 @@ use nomi_core::{ instance::{ launch::{arguments::UserData, LaunchSettings}, logs::PrintLogs, - Instance, + InstanceProfileId, Profile, }, loaders::forge::{Forge, ForgeVersion}, repository::java_runner::JavaRunner, - MINECRAFT_DIR, }; #[tokio::test] async fn forge_test() { let _guard = tracing::subscriber::set_default(tracing_subscriber::fmt().finish()); - let current = std::env::current_dir().unwrap(); - let (tx, _) = tokio::sync::mpsc::channel(100); - let game_paths = GamePaths { - version: PathBuf::from(MINECRAFT_DIR).join("versions").join("forge-test"), - ..Default::default() - }; + let game_paths = GamePaths::from_id(InstanceProfileId::ZERO); - let instance = Instance::builder() + let instance = Profile::builder() .name("forge-test".into()) .version("1.7.10".into()) .game_paths(game_paths.clone()) - .instance(Box::new(Forge::new("1.7.10", ForgeVersion::Recommended, game_paths).await.unwrap())) + .downloader(Box::new( + Forge::new("1.7.10", ForgeVersion::Recommended, game_paths.clone(), JavaRunner::from_environment()) + .await + .unwrap(), + )) // .instance(Box::new(Vanilla::new("1.7.10", game_paths.clone()).await.unwrap())) .build(); - let mc_dir = current.join("minecraft"); - // let vanilla = Box::new(Vanilla::new("1.7.10", game_paths.clone()).await.unwrap()); // let io = vanilla.io(); @@ -45,13 +41,7 @@ async fn forge_test() { // io.await.unwrap(); let settings = LaunchSettings { - assets: mc_dir.join("assets"), - game_dir: mc_dir.clone(), - java_bin: JavaRunner::default(), - libraries_dir: mc_dir.clone().join("libraries"), - manifest_file: mc_dir.clone().join("versions/forge-test/1.7.10.json"), - natives_dir: mc_dir.clone().join("versions/forge-test/natives"), - version_jar_file: mc_dir.join("versions/forge-test/1.7.10.jar"), + java_runner: None, version: "1.7.10".to_string(), version_type: nomi_core::repository::manifest::VersionType::Release, }; @@ -63,7 +53,7 @@ async fn forge_test() { Box::new(assets).download(&tx).await; assets_io.await.unwrap(); - let instance = instance.instance(); + let instance = instance.downloader(); let io_fut = instance.io(); instance.download(&tx).await; @@ -71,13 +61,14 @@ async fn forge_test() { io_fut.await.unwrap(); let profile = VersionProfile::builder() - .id(1) + .id(InstanceProfileId::ZERO) .name("forge-test".into()) .state(ProfileState::downloaded(launch)) .build(); dbg!(profile) .launch( + game_paths, UserData::default(), &JavaRunner::path(PathBuf::from( "E:/programming/code/nomi/crates/nomi-core/.nomi/java/jdk8u422-b05/bin/javaw.exe", diff --git a/crates/nomi-core/tests/full_fabric_test.rs b/crates/nomi-core/tests/full_fabric_test.rs index c5ad768..85c9179 100644 --- a/crates/nomi-core/tests/full_fabric_test.rs +++ b/crates/nomi-core/tests/full_fabric_test.rs @@ -5,7 +5,7 @@ use nomi_core::{ instance::{ launch::{arguments::UserData, LaunchSettings}, logs::PrintLogs, - Instance, + InstanceProfileId, Profile, }, loaders::fabric::Fabric, repository::java_runner::JavaRunner, @@ -15,34 +15,24 @@ use nomi_core::{ async fn full_fabric_test() { let _guard = tracing::subscriber::set_default(tracing_subscriber::fmt().finish()); - let current = std::env::current_dir().unwrap(); - let (tx, _) = tokio::sync::mpsc::channel(100); let game_paths = GamePaths { game: "./minecraft".into(), assets: "./minecraft/assets".into(), - version: "./minecraft/versions/Full-fabric-test".into(), + profile: "./minecraft/versions/Full-fabric-test".into(), libraries: "./minecraft/libraries".into(), }; - let instance = Instance::builder() + let instance = Profile::builder() .name("Full-fabric-test".into()) .version("1.19.4".into()) .game_paths(game_paths.clone()) - .instance(Box::new(Fabric::new("1.19.4", None::, game_paths).await.unwrap())) + .downloader(Box::new(Fabric::new("1.19.4", None::, game_paths.clone()).await.unwrap())) .build(); - let mc_dir = current.join("minecraft"); - let settings = LaunchSettings { - assets: mc_dir.join("assets"), - game_dir: mc_dir.clone(), - java_bin: JavaRunner::default(), - libraries_dir: mc_dir.clone().join("libraries"), - manifest_file: mc_dir.clone().join("versions/Full-fabric-test/1.19.4.json"), - natives_dir: mc_dir.clone().join("versions/Full-fabric-test/natives"), - version_jar_file: mc_dir.join("versions/Full-fabric-test/1.19.4.jar"), + java_runner: None, version: "1.19.4".to_string(), version_type: nomi_core::repository::manifest::VersionType::Release, }; @@ -51,7 +41,7 @@ async fn full_fabric_test() { Box::new(instance.assets().await.unwrap()).download(&tx).await; - let instance = instance.instance(); + let instance = instance.downloader(); let ui_fut = instance.io(); instance.download(&tx).await; @@ -59,13 +49,13 @@ async fn full_fabric_test() { ui_fut.await.unwrap(); let profile = VersionProfile::builder() - .id(1) + .id(InstanceProfileId::ZERO) .name("Full-fabric-test".into()) .state(ProfileState::downloaded(launch)) .build(); dbg!(profile) - .launch(UserData::default(), &JavaRunner::default(), &PrintLogs) + .launch(game_paths, UserData::default(), &JavaRunner::default(), &PrintLogs) .await .unwrap(); } diff --git a/crates/nomi-core/tests/instance_test.rs b/crates/nomi-core/tests/instance_test.rs new file mode 100644 index 0000000..be7b499 --- /dev/null +++ b/crates/nomi-core/tests/instance_test.rs @@ -0,0 +1,66 @@ +use nomi_core::{ + configs::profile::{ProfileState, VersionProfile}, + downloads::traits::Downloader, + fs::write_toml_config, + game_paths::GamePaths, + instance::{ + launch::{arguments::UserData, LaunchSettings}, + logs::PrintLogs, + Instance, Profile, ProfilePayload, + }, + loaders::vanilla::Vanilla, + repository::{java_runner::JavaRunner, manifest::VersionType}, +}; + +#[tokio::test] +async fn instance_test() { + tracing::subscriber::set_global_default(tracing_subscriber::fmt().pretty().finish()).unwrap(); + + let mut instance = Instance::new("cool-instance", 0); + + let paths = GamePaths::from_instance_path(instance.path(), 0); + let profile = Profile::builder() + .game_paths(paths.clone()) + .downloader(Box::new(Vanilla::new("1.19.2", paths.clone()).await.unwrap())) + .name("Cool name".into()) + .version("1.19.2".into()) + .build(); + + let launch_instance = profile.launch_instance( + LaunchSettings { + java_runner: None, + version: "1.19.2".to_owned(), + version_type: VersionType::Release, + }, + None, + ); + + let (tx, _) = tokio::sync::mpsc::channel(1); + + let assets = profile.assets().await.unwrap(); + let io = assets.io(); + Box::new(assets).download(&tx).await; + io.await.unwrap(); + + let downloader = profile.downloader(); + let io = downloader.io(); + downloader.download(&tx).await; + io.await.unwrap(); + + let version_profile = VersionProfile { + id: instance.next_id(), + name: "Based".to_owned(), + state: ProfileState::downloaded(launch_instance), + }; + + instance.add_profile(ProfilePayload::from_version_profile(&version_profile, &paths.profile_config())); + + write_toml_config(&version_profile, paths.profile_config()).await.unwrap(); + + instance.write().await.unwrap(); + + version_profile + .launch(paths, UserData::default(), &JavaRunner::from_environment(), &PrintLogs) + .await + .unwrap(); +} diff --git a/crates/nomi-core/tests/vanilla_test.rs b/crates/nomi-core/tests/vanilla_test.rs index cea103f..d458029 100644 --- a/crates/nomi-core/tests/vanilla_test.rs +++ b/crates/nomi-core/tests/vanilla_test.rs @@ -1,4 +1,4 @@ -use nomi_core::{instance::launch::LaunchSettings, repository::java_runner::JavaRunner}; +use nomi_core::instance::launch::LaunchSettings; #[tokio::test] async fn vanilla_test() { @@ -27,16 +27,8 @@ async fn vanilla_test() { // // assets.download().await.unwrap(); // // builder.download().await.unwrap(); - let mc_dir = std::env::current_dir().unwrap().join("minecraft"); - let _settings = LaunchSettings { - assets: mc_dir.join("assets"), - game_dir: mc_dir.clone(), - java_bin: JavaRunner::default(), - libraries_dir: mc_dir.clone().join("libraries"), - manifest_file: mc_dir.clone().join("versions/1.20/1.19.4.json"), - natives_dir: mc_dir.clone().join("versions/1.20/natives"), - version_jar_file: mc_dir.join("versions/1.20/1.20.jar"), + java_runner: None, version: "1.20".to_string(), version_type: nomi_core::repository::manifest::VersionType::Release, };