diff options
| -rw-r--r-- | Cargo.lock | 45 | ||||
| -rw-r--r-- | ozone-cli/Cargo.toml | 1 | ||||
| -rw-r--r-- | ozone-cli/src/main.rs | 80 | ||||
| -rw-r--r-- | ozone/src/launcher.rs | 46 | ||||
| -rw-r--r-- | ozone/src/launcher/download.rs | 5 | ||||
| -rw-r--r-- | ozone/src/launcher/jre/download.rs | 2 | ||||
| -rw-r--r-- | ozone/src/util.rs | 2 | ||||
| -rw-r--r-- | ozone/src/util/progress.rs | 123 |
8 files changed, 292 insertions, 12 deletions
@@ -930,6 +930,19 @@ dependencies = [ ] [[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] name = "const-field-offset" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1395,6 +1408,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2590,6 +2609,19 @@ dependencies = [ ] [[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] name = "input" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3100,6 +3132,12 @@ dependencies = [ ] [[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] name = "oauth2" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3538,6 +3576,7 @@ version = "0.1.0" dependencies = [ "clap", "clap_complete", + "indicatif", "lazy_static", "log", "ozone", @@ -5208,6 +5247,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/ozone-cli/Cargo.toml b/ozone-cli/Cargo.toml index db6503b..ad65d92 100644 --- a/ozone-cli/Cargo.toml +++ b/ozone-cli/Cargo.toml @@ -13,3 +13,4 @@ clap_complete = { version = "4.5.44", features = ["unstable-dynamic"] } lazy_static = "1.5.0" regex = "1.11.1" uuid = { version = "1.12.1", features = ["v4"] } +indicatif = "0.17.11" diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs index 11349c8..dda2c32 100644 --- a/ozone-cli/src/main.rs +++ b/ozone-cli/src/main.rs @@ -1,11 +1,14 @@ mod cli; +use std::borrow::Cow; use std::error::Error; use std::path::Path; use std::process::ExitCode; +use std::time::Duration; use log::{error, info, trace}; use clap::Parser; -use ozone::launcher::{Instance, JavaRuntimeSetting, Launcher, Settings, ALT_CLIENT_ID, MAIN_CLIENT_ID}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use ozone::launcher::{Instance, JavaRuntimeSetting, LaunchProgress, Launcher, ProgressIndication, Settings, ALT_CLIENT_ID, MAIN_CLIENT_ID}; use ozone::launcher::version::{VersionList, VersionResult}; use uuid::Uuid; use ozone::auth::{Account, AccountStorage, MsaAccount}; @@ -14,6 +17,51 @@ use crate::cli::{AccountCommand, Cli, InstanceCommand, ProfileSelectArgs, RootCo const ACCOUNT_DB_PATH: &str = "ozone_accounts.json"; +struct IndicatifProgress { + bar: ProgressBar +} + +impl ProgressIndication for IndicatifProgress { + fn set_progress(&self, progress: usize) { + self.bar.set_position(progress as u64); + } + + fn set_max(&self, max: usize) { + self.bar.set_length(max as u64); + } + + fn set_all(&self, progress: usize, max: usize) { + self.bar.update(|s| { + s.set_pos(progress as u64); + s.set_len(max as u64); + }); + } + + fn inc_progress(&self, by: usize) { + self.bar.inc(by as u64); + } + + fn inc_max(&self, by: usize) { + self.bar.inc_length(by as u64); + } + + fn dec_progress(&self, by: usize) { + self.bar.dec(by as u64); + } + + fn dec_max(&self, by: usize) { + self.bar.dec_length(by as u64); + } + + fn set_text(&self, text: Cow<'static, str>) { + self.bar.set_message(text); + } + + fn complete(&self, message: Cow<'static, str>) { + self.bar.finish_with_message(message); + } +} + fn find_account<'a>(auth: &'a AccountStorage, input: &ProfileSelectArgs) -> Result<Vec<(&'a String, &'a Account)>, ()> { if let Some(uuid) = input.uuid { Ok(auth.iter_accounts().filter(|(_, a)| match *a { @@ -467,12 +515,30 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> { println!("Preparing the game files..."); let launcher = Launcher::new(&home, !cli.offline).await?; - let launch = launcher.prepare_launch(inst, Settings::get_instance_path(selection), settings.client_id, account, args.demo).await.map_err(|e| { - error!("error launching: {e}"); - e - })?; + let multi = MultiProgress::new(); + let progress = IndicatifProgress { + bar: multi.add(ProgressBar::no_length() + .with_style(ProgressStyle::with_template("{spinner} Step {pos} of {len}: {msg}").unwrap())) + }; + + let progress_sub = IndicatifProgress { + bar: multi.add(ProgressBar::no_length()) + }; + + progress.bar.enable_steady_tick(Duration::from_millis(100)); + + let lp = LaunchProgress::new(&progress as &dyn ProgressIndication, &progress_sub as &dyn ProgressIndication); + + let launch = launcher.prepare_launch(inst, + Settings::get_instance_path(selection), + settings.client_id, + account, + args.demo, + lp) + .await.inspect_err(|e| { + error!("error launching: {e}"); + })?; - dbg!(&launch); info!("ok!"); println!("Launching the game!"); @@ -487,7 +553,7 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> { #[tokio::main] async fn main() -> ExitCode { // use Warn as the default level to minimize noise on the command line - simple_logger::SimpleLogger::new().env().init().unwrap(); + //simple_logger::SimpleLogger::new().env().init().unwrap(); let arg = Cli::parse(); diff --git a/ozone/src/launcher.rs b/ozone/src/launcher.rs index 98e8143..b21f873 100644 --- a/ozone/src/launcher.rs +++ b/ozone/src/launcher.rs @@ -36,6 +36,7 @@ use crate::version::{Library, OSRestriction, OperatingSystem, DownloadType, Libr use assets::{AssetError, AssetRepository}; use ozone_helpers::ozone_arch_bits; use crate::util::{self, AsJavaPath}; +pub use crate::util::progress::ProgressIndication; pub use constants::{MAIN_CLIENT_ID, ALT_CLIENT_ID}; pub use settings::*; @@ -47,6 +48,7 @@ use crate::auth::Account; use crate::launcher::download::{FileDownload, RemoteChecksumDownload}; use crate::launcher::jre::{JavaRuntimeError, JavaRuntimeRepository}; use crate::launcher::version::VersionError; +use crate::util::progress::StepProgress; use crate::version::manifest::VersionType; #[derive(Debug)] @@ -244,6 +246,17 @@ impl FeatureMatcher<'_> for LaunchFeatureMatcher<'_> { } } +pub struct LaunchProgress<'ind> { + indicator: &'ind dyn ProgressIndication, + indicator_sub: &'ind dyn ProgressIndication +} + +impl<'ind> LaunchProgress<'ind> { + pub fn new(ind: impl Into<&'ind dyn ProgressIndication>, ind_sub: impl Into<&'ind dyn ProgressIndication>) -> Self { + Self { indicator: ind.into(), indicator_sub: ind_sub.into() } + } +} + impl Launcher { pub fn sensible_home() -> Option<PathBuf> { env::var_os("OZONE_HOME") @@ -326,7 +339,21 @@ impl Launcher { /* TODO: * - launch game using JNI */ - pub async fn prepare_launch(&self, instance: &Instance, inst_home: impl AsRef<Path>, client_id: Uuid, account: &Account, force_demo: bool) -> Result<Launch, LaunchError> { + pub async fn prepare_launch<'prog>(&self, instance: &Instance, inst_home: impl AsRef<Path>, client_id: Uuid, account: &Account, force_demo: bool, progress: LaunchProgress<'prog>) -> Result<Launch, LaunchError> { + let steps: StepProgress = progress.indicator.into(); + let step_init = steps.step("Resolve game version"); + let step_prep_libraries = steps.step("Collect required libraries"); + let step_get_libraries = steps.step("Ensure libraries"); + let step_log = steps.step("Set up logging"); + let step_assets = steps.step("Ensure game assets"); + let step_client_jar = steps.step("Ensure client jar"); + let step_natives = steps.step("Extract natives"); + let step_classpath = steps.step("Build classpath"); + let step_java_runtime = steps.step("Ensure java runtime"); + let step_arguments = steps.step("Build argument list"); + steps.update_max(); + + step_init.make_current(); let start = Instant::now(); let feature_matcher = LaunchFeatureMatcher { instance, account, force_demo }; let version_id = &instance.game_version; @@ -358,6 +385,8 @@ impl Launcher { info!("Resolved launch version {}!", ver.id); + step_prep_libraries.make_current(); + let mut extract_jobs = Vec::new(); let mut downloads = IndexMap::new(); @@ -393,10 +422,12 @@ impl Launcher { } } + step_get_libraries.make_current(); + if self.online { info!("Downloading {} libraries...", downloads.len()); let client = util::create_client(); - MultiDownloader::<dyn FileDownload, _, _>::new(downloads.values_mut()).perform(&client).await + MultiDownloader::<dyn FileDownload, _, _>::new(downloads.values_mut()).perform(&client, progress.indicator_sub).await .inspect_err(|e| warn!("library download failed: {e}")) .map_err(|_| LaunchError::LibraryDownloadError)?; } else { @@ -408,6 +439,8 @@ impl Launcher { })?; } + step_log.make_current(); + let log_arg; if let Some(logging) = ver.logging.as_ref().and_then(|l| l.client.as_ref()) { log_arg = Some(self.log_config_ensure(logging).await?); @@ -416,6 +449,7 @@ impl Launcher { } // download assets + step_assets.make_current(); let (asset_idx_name, asset_idx) = if let Some(idx_download) = ver.asset_index.as_ref() { @@ -431,6 +465,7 @@ impl Launcher { }; // download client jar + step_client_jar.make_current(); let client_jar_path; if let Some(client) = ver.downloads.get(&DownloadType::Client) { @@ -449,6 +484,8 @@ impl Launcher { client_jar_path = None; } + step_natives.make_current(); + // clean up old natives let nnatives = self.libraries.clean_old_natives().await?; info!("Cleaned up {} old natives directories.", nnatives); @@ -466,6 +503,7 @@ impl Launcher { }; info!("Building classpath"); + step_classpath.make_current(); let classpath = env::join_paths(downloads.values() .map(|job| job.get_path().as_java_path()) .chain(client_jar_path.iter().map(|p| p.as_path().as_java_path()))) @@ -479,6 +517,7 @@ impl Launcher { trace!("Classpath: {classpath}"); info!("Resolving java runtime environment path"); + step_java_runtime.make_current(); let runtime_path; if let Some(JavaRuntimeSetting::Path(ref jre_path)) = instance.java_runtime { @@ -501,10 +540,10 @@ impl Launcher { return Err(LaunchError::MissingJavaRuntime); }; - debug!("Found runtime exe: {}", runtime_exe_path.display()); info!("Deriving launch arguments"); + step_arguments.make_current(); let info = LaunchInfo { launcher: self, client_id, @@ -539,6 +578,7 @@ impl Launcher { let diff = Instant::now().duration_since(start); info!("Finished preparing launch for {} in {:.02} seconds!", ver.id, diff.as_secs_f32()); + progress.indicator.complete(Cow::Borrowed("Launch preparation complete!")); Ok(Launch { jvm_args, diff --git a/ozone/src/launcher/download.rs b/ozone/src/launcher/download.rs index 6e32e26..cfb246d 100644 --- a/ozone/src/launcher/download.rs +++ b/ozone/src/launcher/download.rs @@ -12,6 +12,7 @@ use reqwest::{Client, RequestBuilder, Response, StatusCode}; use sha1_smol::{Digest, Sha1}; use tokio::fs::File; use tokio::io::AsyncWriteExt; +use crate::launcher::ProgressIndication; use crate::util; use crate::util::IntegrityError; @@ -118,7 +119,7 @@ where } } - pub async fn perform(self, client: &'j Client) -> Result<(), PhaseDownloadError> { + pub async fn perform(self, client: &'j Client, progress: &dyn ProgressIndication) -> Result<(), PhaseDownloadError> { macro_rules! map_err { ($result:expr, $phase:expr, $job:expr) => { match $result { @@ -128,6 +129,7 @@ where } } + progress.set_all(0, self.jobs.size_hint().0); stream::iter(self.jobs.map(Ok)) .try_for_each_concurrent(self.nconcurrent, |job_| async move { let job = job_.borrow_mut(); @@ -145,6 +147,7 @@ where } job.finish().await.map_err(|e| PhaseDownloadError::new(Phase::Finish, e, job))?; + progress.inc_progress(1); Ok(()) }).await diff --git a/ozone/src/launcher/jre/download.rs b/ozone/src/launcher/jre/download.rs index fc457ef..6df1413 100644 --- a/ozone/src/launcher/jre/download.rs +++ b/ozone/src/launcher/jre/download.rs @@ -25,6 +25,8 @@ pub struct LzmaDownloadJob { url: String, path: PathBuf, inflate: bool, + + #[allow(dead_code)] // not used on windows executable: bool, raw_size: Option<usize>, diff --git a/ozone/src/util.rs b/ozone/src/util.rs index b765a17..6020be0 100644 --- a/ozone/src/util.rs +++ b/ozone/src/util.rs @@ -1,4 +1,4 @@ -mod progress; +pub(crate) mod progress; use std::error::Error; use std::fmt::{Display, Formatter}; diff --git a/ozone/src/util/progress.rs b/ozone/src/util/progress.rs index e69de29..ccb610e 100644 --- a/ozone/src/util/progress.rs +++ b/ozone/src/util/progress.rs @@ -0,0 +1,123 @@ +use std::borrow::Cow; +use std::collections::BTreeSet; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; + +pub trait ProgressIndication { + fn set_progress(&self, progress: usize); + fn set_max(&self, max: usize); + fn set_all(&self, progress: usize, max: usize); + + fn inc_progress(&self, by: usize); + fn inc_max(&self, by: usize); + + fn dec_progress(&self, by: usize); + fn dec_max(&self, by: usize); + + fn set_text(&self, text: Cow<'static, str>); + + fn complete(&self, message: Cow<'static, str>); +} + +pub(crate) struct Step<'ind, 'parent> { + parent: &'parent StepProgress<'ind>, + text: Cow<'static, str>, + index: usize +} + +pub(crate) struct StepProgress<'ind> { + inner: &'ind dyn ProgressIndication, + curidx: AtomicUsize +} + +impl<'ind> Into<StepProgress<'ind>> for &'ind dyn ProgressIndication { + fn into(self) -> StepProgress<'ind> { + StepProgress { + inner: self, + curidx: AtomicUsize::new(0) + } + } +} + +impl<'ind> StepProgress<'ind> { + pub(crate) fn step(&self, name: impl Into<Cow<'static, str>>) -> Step<'ind, '_> { + Step { + parent: self, + text: name.into(), + index: self.curidx.fetch_add(1, Ordering::SeqCst) + } + } + + pub(crate) fn update_max(&self) { + self.inner.set_max(self.curidx.load(Ordering::SeqCst)); + } +} + +impl Step<'_, '_> { + pub(crate) fn make_current(&self) { + self.parent.inner.set_progress(self.index); + self.parent.inner.set_text(self.text.clone()); + } +} + +struct ProgressItem<T> { + inner: T, + idx: usize +} + +impl<T> Deref for ProgressItem<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<T> DerefMut for ProgressItem<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +struct ProgressItemTracker<T> { + set: BTreeSet<ProgressItem<T>>, + cur: usize +} + +impl<T> PartialEq for ProgressItem<T> { + fn eq(&self, other: &Self) -> bool { + self.idx.eq(&other.idx) + } +} + +impl<T> PartialOrd for ProgressItem<T> { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl<T> Eq for ProgressItem<T> {} + +impl<T> Ord for ProgressItem<T> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.idx.cmp(&other.idx) + } +} + +impl<T> ProgressItemTracker<T> { + fn push(&mut self, item: T) { + let idx = self.cur; + self.cur += 1; + + self.set.insert(ProgressItem { inner: item, idx }); + } + + fn len(&self) -> usize { + self.set.len() + } + + fn first(&self) -> Option<&T> { + self.set.first().map(|t| &**t) + } +} |
