diff --git a/Cargo.lock b/Cargo.lock index 2111ea36..43a3bc23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1424,9 +1424,9 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -2941,6 +2941,7 @@ dependencies = [ "discord-rich-presence", "dunce", "futures", + "futures-lite", "indicatif", "interpulse", "md-5", diff --git a/Cargo.toml b/Cargo.toml index e3a4d68b..88c918d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ sysinfo = { version = "0.31", features = [ "serde" ] } discord-rich-presence = { version = "0.2" } indicatif = { version = "0.17" } futures = { version = "0.3" } +futures-lite = { version = "2.6" } async-trait = { version = "0.1" } whoami = { version = "1.5" } cc = { version = "1.1" } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 27a56bce..56cec8f4 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -53,6 +53,7 @@ sea-orm = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } futures = { workspace = true } +futures-lite = { workspace = true } uuid = { workspace = true } thiserror = { workspace = true } anyhow = { workspace = true } diff --git a/packages/core/examples/cluster.rs b/packages/core/examples/cluster.rs index 7357ffb8..98208c3d 100644 --- a/packages/core/examples/cluster.rs +++ b/packages/core/examples/cluster.rs @@ -1,9 +1,9 @@ -use onelauncher_core::{api::{self, proxy::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions}; +use onelauncher_core::{api::{self, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::CoreOptions}; use onelauncher_entity::loader::GameLoader; #[tokio::main] async fn main() -> LauncherResult<()> { - initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?; + initialize_core(CoreOptions::default(), ProxyDynamic::new()).await?; println!("syncing clusters"); let missing = api::cluster::sync_clusters().await?; @@ -22,5 +22,8 @@ async fn main() -> LauncherResult<()> { println!("cluster created: {cluster:#?}"); + println!("preparing cluster for launch"); + api::cluster::prepare_cluster(&cluster).await?; + Ok(()) } \ No newline at end of file diff --git a/packages/core/examples/download_minecraft.rs b/packages/core/examples/download_minecraft.rs index dec57156..f703b42a 100644 --- a/packages/core/examples/download_minecraft.rs +++ b/packages/core/examples/download_minecraft.rs @@ -1,6 +1,5 @@ use anyhow::anyhow; use onelauncher_core::{api::{game::metadata::{download_minecraft_ingressed, download_version_info}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, State}}; -use onelauncher_entity::loader::GameLoader; #[tokio::main] async fn main() -> LauncherResult<()> { @@ -8,7 +7,7 @@ async fn main() -> LauncherResult<()> { let state = State::get().await?; let mut metadata = state.metadata.write().await; - let versions = metadata.get_vanilla(GameLoader::Vanilla).await?; + let versions = metadata.get_vanilla_or_fetch().await?; let version = versions.versions.iter().find(|v| v.id == "1.20.4").ok_or_else(|| anyhow!("couldn't find 1.8.9 in version list"))?; let version_info = download_version_info(version, None, None, Some(true)).await?; diff --git a/packages/core/examples/metadata.rs b/packages/core/examples/metadata.rs index 51161b64..e6bd8f49 100644 --- a/packages/core/examples/metadata.rs +++ b/packages/core/examples/metadata.rs @@ -9,12 +9,12 @@ async fn main() -> LauncherResult<()> { let metadata = &mut state.metadata.write().await; println!("Initialized: {}", metadata.initialized()); - println!("Minecraft: {:?}", metadata.get_vanilla(GameLoader::Vanilla).await.is_ok()); - println!("Forge: {:?}", metadata.get_modded(GameLoader::Forge).await.is_ok()); + println!("Minecraft: {:?}", metadata.get_vanilla_or_fetch().await.is_ok()); + println!("Forge: {:?}", metadata.get_modded_or_fetch(GameLoader::Forge).await.is_ok()); println!("Initialized: {}", metadata.initialized()); - println!("NeoForge: {:?}", metadata.get_modded(GameLoader::NeoForge).await.is_ok()); - println!("Fabric: {:?}", metadata.get_modded(GameLoader::Fabric).await.is_ok()); - println!("Quilt: {:?}", metadata.get_modded(GameLoader::Quilt).await.is_ok()); + println!("NeoForge: {:?}", metadata.get_modded_or_fetch(GameLoader::NeoForge).await.is_ok()); + println!("Fabric: {:?}", metadata.get_modded_or_fetch(GameLoader::Fabric).await.is_ok()); + println!("Quilt: {:?}", metadata.get_modded_or_fetch(GameLoader::Quilt).await.is_ok()); Ok(()) } \ No newline at end of file diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs index 80cd0dec..f05b5271 100644 --- a/packages/core/src/api/cluster/dao.rs +++ b/packages/core/src/api/cluster/dao.rs @@ -53,8 +53,13 @@ pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> { let state = State::get().await?; let db = &state.db; - let model = get_cluster_by_id(id).await?.ok_or(DaoError::NotFound)?; - model.delete(db).await?; + let deleted = clusters::Entity::delete_by_id(id) + .exec(db) + .await?; + + if deleted.rows_affected == 0 { + return Err(DaoError::NotFound.into()); + } Ok(()) } diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs index 13ffa9fc..77d69585 100644 --- a/packages/core/src/api/cluster/mod.rs +++ b/packages/core/src/api/cluster/mod.rs @@ -1,16 +1,33 @@ -use onelauncher_entity::clusters; +use interpulse::api::minecraft::VersionInfo; +use onelauncher_entity::cluster_stage::ClusterStage; +use onelauncher_entity::{clusters, java_versions}; use onelauncher_entity::icon::Icon; use onelauncher_entity::loader::GameLoader; +use sea_orm::ActiveValue::Set; +use tokio::process::Command; +use crate::api::game::arguments; +use crate::api::game::metadata::{self, download_minecraft}; +use crate::api::ingress::IngressSendExt; +use crate::api::{java, setting_profiles}; use crate::error::LauncherResult; -use crate::store::Dirs; -use crate::utils::io; +use crate::store::ingress::{IngressType, SubIngress}; +use crate::store::{Dirs, State}; +use crate::utils::io::{self, IOError}; pub mod dao; mod sync; pub use sync::*; +use super::ingress::init_ingress; + +#[derive(Debug, thiserror::Error)] +pub enum ClusterError { + #[error("version '{0}' was not found")] + InvalidVersion(String), +} + #[must_use] pub fn sanitize_name(name: &str) -> String { let mut name = name.to_string(); @@ -79,4 +96,215 @@ pub async fn create_cluster( } } +/// Make a cluster playable (installs all necessary game files and dependencies required to play) +pub async fn prepare_cluster(cluster: &clusters::Model) -> LauncherResult<()> { + const INGRESS_TOTAL: f64 = 100.0; + const INGRESS_TASKS: f64 = 4.0; + const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS; + tracing::debug!("preparing cluster {}", cluster.name); + + if cluster.stage == ClusterStage::Ready { + tracing::info!("cluster is already ready"); + return Ok(()); + } + + let ingress_id = init_ingress( + IngressType::PrepareCluster { + cluster_name: cluster.name.clone(), + }, + "preparing cluster", + INGRESS_TOTAL, + ) + .await?; + + dao::update_cluster_by_id(cluster.id, async |mut cluster| { + cluster.stage = Set(ClusterStage::Downloading); + Ok(cluster) + }) + .await?; + + let result: LauncherResult<()> = { + // TASK 1 + ingress_id.set_ingress_message("fetching data").await?; + let setting_profile = + setting_profiles::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?; + + let state = State::get().await?; + let mut metadata = state.metadata.write().await; + + let version = metadata + .get_vanilla_or_fetch() + .await? + .versions + .iter() + .find(|v| v.id == cluster.mc_version) + .ok_or_else(|| ClusterError::InvalidVersion(cluster.mc_version.clone()))?; + + let loader_version = metadata::get_loader_version( + &cluster.mc_version, + cluster.mc_loader, + cluster.mc_loader_version.as_deref(), + ) + .await?; + + // TASK 2 + let mut version_info = metadata::download_version_info( + version, + loader_version.as_ref(), + Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), + None, + ) + .await?; + + let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? { + version + } else { + // TODO: java runtime not found - should prob download + let java = java::locate_java().await?; + + if java.is_empty() { + tracing::warn!("No Java installations found"); + return Ok(()); + } + + for (path, info) in java { + java::dao::insert_java(path, info).await?; + } + + java::get_recommended_java(&version_info, Some(&setting_profile)).await? + .ok_or_else(|| { + anyhow::anyhow!("No Java installations found, please install a Java runtime") + })? + }; + + // TASK 3 + download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), None).await?; + + // TASK 4 + run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?; + + Ok(()) + }; + + if let Err(err) = result { + tracing::error!("failed to prepare cluster: {}", err); + dao::update_cluster_by_id(cluster.id, async |mut cluster| { + cluster.stage = Set(ClusterStage::NotReady); + Ok(cluster) + }) + .await?; + return Err(err); + } + + dao::update_cluster_by_id(cluster.id, async |mut cluster| { + cluster.stage = Set(ClusterStage::Ready); + Ok(cluster) + }) + .await?; + tracing::debug!("cluster is ready"); + + Ok(()) +} + +/// Run forge processors +async fn run_forge_processors( + cluster: &clusters::Model, + version_info: &mut VersionInfo, + java_version: java_versions::Model, + ingress: &SubIngress<'_>, +) -> LauncherResult<()> { + let Some(processors) = &version_info.processors else { + return Ok(()); + }; + + let dirs = Dirs::get().await?; + let client = dirs + .versions_dir() + .join(format!("{}.jar", &version_info.id)); + let libraries = dirs.libraries_dir(); + + let Some(data) = &mut version_info.data else { + return Ok(()); + }; + + macro_rules! data_entry { + ($dest:expr; $($name:literal: client => $client:expr, server => $server:expr;)+) => { + $(std::collections::HashMap::insert( + $dest, + String::from($name), + interpulse::api::modded::SidedDataEntry { + client: String::from($client), + server: String::from($server), + }, + );)+ + } + } + + data_entry! { + data; + "SIDE": + client => "client", + server => ""; + "MINECRAFT_JAR": + client => client.to_string_lossy(), + server => ""; + "MINECRAFT_VERSION": + client => cluster.mc_version.clone(), + server => ""; + "ROOT": + client => cluster.path.clone(), + server => ""; + "LIBRARY_DIR": + client => libraries.to_string_lossy(), + server => ""; + } + + ingress.set_ingress_message("running forge processors").await?; + let total_length = processors.len(); + for (index, processor) in processors.iter().enumerate() { + if let Some(sides) = &processor.sides { + if !sides.contains(&String::from("client")) { + continue; + } + } + + let mut cp = processor.classpath.clone(); + cp.push(processor.jar.clone()); + + let child = Command::new(&java_version.absolute_path) + .arg("-cp") + .arg(arguments::get_classpath_library( + &libraries, + &cp, + )?) + .arg( + arguments::main_class(arguments::get_library(&libraries, &processor.jar, false)?) + .await? + .ok_or_else(|| { + anyhow::anyhow!("failed to find processor main class for {}", processor.jar) + })?, + ) + .args(arguments::processor_arguments( + &libraries, + &processor.args, + data, + )?) + .output() + .await + .map_err(IOError::from) + .map_err(|err| anyhow::anyhow!("failed to run processor: {err}"))?; + + if !child.status.success() { + return Err(anyhow::anyhow!( + "error occured while running processor: {}", + String::from_utf8_lossy(&child.stderr) + ) + .into()); + } + + ingress.set_ingress_message(&format!("running forge processors {index}/{total_length}")).await?; + } + + Ok(()) +} diff --git a/packages/core/src/api/cluster/sync.rs b/packages/core/src/api/cluster/sync.rs index 4c94c997..5fb084b1 100644 --- a/packages/core/src/api/cluster/sync.rs +++ b/packages/core/src/api/cluster/sync.rs @@ -54,19 +54,23 @@ pub async fn sync_clusters() -> LauncherResult> { // It iterates over the directories in the cluster directory // and checks if they are in the database let cluster_dir = Dirs::get_clusters_dir().await?; - let mut stream = io::read_dir(cluster_dir).await?; - while let Ok(Some(entry)) = stream.next_entry().await { - let path = entry.path(); - if !path.is_dir() || checked_paths.contains(&path) { - // Skip if it's not a directory or if we've already checked it - continue; - } + if cluster_dir.exists() { + let mut stream = io::read_dir(cluster_dir).await?; + while let Ok(Some(entry)) = stream.next_entry().await { + let path = entry.path(); + if !path.is_dir() || checked_paths.contains(&path) { + // Skip if it's not a directory or if we've already checked it + continue; + } - // We have a directory that is not in the database - if let Err(err) = sync_from_fs_to_db(&path).await { - tracing::error!("failed to sync cluster from fs to db: {}", err); - } - } + // We have a directory that is not in the database + if let Err(err) = sync_from_fs_to_db(&path).await { + tracing::error!("failed to sync cluster from fs to db: {}", err); + } + } + } else { + io::create_dir_all(cluster_dir).await?; + } Ok(missing_ids) } diff --git a/packages/core/src/api/game/arguments.rs b/packages/core/src/api/game/arguments.rs new file mode 100644 index 00000000..6049ad5d --- /dev/null +++ b/packages/core/src/api/game/arguments.rs @@ -0,0 +1,369 @@ +//! Programatic Minecraft argument parser which replaces variables in arguments + +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +use futures::AsyncReadExt; +use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType}; +use interpulse::api::modded::SidedDataEntry; +use interpulse::utils::get_path_from_artifact; + +use crate::constants::{self, DUMMY_REPLACE_NEWLINE}; +use crate::error::LauncherResult; +use crate::store::Core; +use crate::utils::io; + +#[allow(clippy::too_many_arguments)] +pub fn java_arguments( + arguments: Option<&[Argument]>, + natives_path: &Path, + libraries_path: &Path, + classpaths: &str, + version: &str, + mem_min: usize, + mem_max: usize, + custom_args: Vec, + java_arch: &str, +) -> LauncherResult> { + let mut parsed = Vec::new(); + if let Some(args) = arguments { + parse_arguments( + args, + &mut parsed, + |a| { + parse_java_argument( + a.to_string(), + natives_path, + libraries_path, + classpaths, + version, + ) + }, + java_arch, + )?; + } else { + parsed.push(format!( + "-Djava.library.path={}", + io::canonicalize(natives_path) + .map_err(|_| anyhow::anyhow!( + "specified natives path {} not found", + natives_path.to_string_lossy() + ))? + .to_string_lossy() + )); + parsed.push("-cp".to_string()); + parsed.push(classpaths.to_string()); + } + + parsed.push(format!("-Xmx{mem_max}M")); + parsed.push(format!("-Xms{mem_min}M")); + + for arg in custom_args { + if !arg.is_empty() { + parsed.push(arg); + } + } + + Ok(parsed) +} + +// #[allow(clippy::too_many_arguments)] +// pub fn minecraft_arguments( +// args: Option<&[Argument]>, +// legacy_args: Option<&str>, +// creds: &MinecraftCredentials, +// version: &str, +// asset_index: &str, +// game_directory: &Path, +// assets_directory: &Path, +// version_type: VersionType, +// resolution: Resolution, +// java_arch: &str, +// ) -> LauncherResult> { +// if let Some(args) = args { +// let mut parsed = Vec::new(); +// parse_arguments( +// args, +// &mut parsed, +// |arg| { +// parse_minecraft_argument( +// arg, +// &creds.access_token, +// &creds.username, +// creds.id, +// version, +// asset_index, +// game_directory, +// assets_directory, +// version_type, +// resolution, +// ) +// }, +// java_arch, +// )?; + +// Ok(parsed) +// } else if let Some(legacy_args) = legacy_args { +// let mut parsed = Vec::new(); +// for arg in legacy_args.split(' ') { +// parsed.push(parse_minecraft_argument( +// &arg.replace(' ', DUMMY_REPLACE_NEWLINE), +// &creds.access_token, +// &creds.username, +// creds.id, +// version, +// asset_index, +// game_directory, +// assets_directory, +// version_type, +// resolution, +// )?); +// } + +// Ok(parsed) +// } else { +// Ok(Vec::new()) +// } +// } + +pub fn processor_arguments, S: ::std::hash::BuildHasher>( + libraries_path: &Path, + args: &[T], + data: &HashMap, +) -> LauncherResult> { + let mut parsed = Vec::new(); + + for arg in args { + let a = &arg.as_ref()[1..arg.as_ref().len() - 1]; + if arg.as_ref().starts_with('{') { + if let Some(entry) = data.get(a) { + parsed.push(if entry.client.starts_with('[') { + get_library( + libraries_path, + &entry.client[1..entry.client.len() - 1], + true, + )? + } else { + entry.client.clone() + }); + } + } else if arg.as_ref().starts_with('[') { + parsed.push(get_library(libraries_path, a, true)?); + } else { + parsed.push(arg.as_ref().to_string()); + } + } + + Ok(parsed) +} + +pub async fn main_class(path: String) -> LauncherResult> { + // let zipfs = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?; + // let mut archive = zip::ZipArchive::new(zipfs) + // .map_err(|_| anyhow::anyhow!("failed to read main class {}", path))?; + // let file = archive + // .by_name("META-INF/MANIFEST.MF") + // .map_err(|_| anyhow::anyhow!("failed to read manifest {}", path))?; + let data = io::read(path).await?; + let mut class_name: Option = None; + + io::read_zip_entries_bytes(data, async |_, entry, entry_reader| { + if entry.dir()? { + return Ok(()); + } + + if entry.filename().as_str()? != "META-INF/MANIFEST.MF" { + return Ok(()); + } + + let mut buf = String::new(); + entry_reader.read_to_string(&mut buf).await?; + + for line in buf.lines() { + let line = line.trim(); + if line.starts_with("Main-Class:") { + if let Some(class) = line.split(':').nth(1) { + class_name = Some(class.to_string()); + return Ok(()); + } + } + } + + Ok(()) + }).await?; + + Ok(class_name) +} + +#[allow(clippy::too_many_arguments)] +pub fn parse_minecraft_argument( + argument: &str, + access_token: &str, + username: &str, + uuid: uuid::Uuid, + version: &str, + asset_index: &str, + game_directory: &Path, + assets_directory: &Path, + version_type: VersionType, + resolution: (u32, u32), +) -> LauncherResult { + #[allow(clippy::literal_string_with_formatting_args)] + Ok(argument + .replace("${accessToken}", access_token) + .replace("${auth_access_token}", access_token) + .replace("${auth_session}", access_token) + .replace("${auth_player_name}", username) + .replace("${auth_xuid}", "0") // TODO: add auth xuid + .replace("${auth_uuid}", &uuid.simple().to_string()) + .replace("${uuid}", &uuid.simple().to_string()) + .replace("${clientid}", &Core::get().msa_client_id) + .replace("${user_properties}", "{}") + .replace("${user_type}", "msa") + .replace("${version_name}", version) + .replace("${assets_index_name}", asset_index) + .replace( + "${game_directory}", + &io::canonicalize(game_directory)?.to_string_lossy(), + ) + .replace( + "${assets_root}", + &io::canonicalize(assets_directory)?.to_string_lossy(), + ) + .replace( + "${game_assets}", + &io::canonicalize(assets_directory)?.to_string_lossy(), + ) + .replace("${version_type}", version_type.as_str()) + .replace("${resolution_width}", &resolution.0.to_string()) + .replace("${resolution_height}", &resolution.1.to_string())) +} + +#[allow(clippy::too_many_arguments)] +fn parse_java_argument( + mut argument: String, + natives_path: &Path, + libraries_path: &Path, + classpaths: &str, + version: &str, +) -> LauncherResult { + argument.retain(|c| !c.is_whitespace()); + Ok(argument + .replace( + "${natives_directory}", + &io::canonicalize(natives_path)?.to_string_lossy(), + ) + .replace( + "${library_directory}", + &io::canonicalize(libraries_path)?.to_string_lossy(), + ) + .replace("${classpath_separator}", constants::CLASSPATH_SEPARATOR) + .replace("${launcher_name}", &Core::get().launcher_name) + .replace("${launcher_version}", &Core::get().launcher_version) + .replace("${version_name}", version) + .replace("${classpath}", classpaths)) +} + +pub fn classpaths( + libraries_path: &Path, + libraries: &[Library], + client_path: &Path, + java_arch: &str, + updated: bool, +) -> LauncherResult { + let mut classpaths = libraries + .iter() + .filter_map(|lib| { + if let Some(rules) = &lib.rules { + if !super::rules::validate_rules(rules, java_arch, updated) { + return None; + } + } + + if !lib.include_in_classpath { + return None; + } + + Some(get_library(libraries_path, &lib.name, false)) + }) + .collect::, _>>()?; + + classpaths.insert( + io::canonicalize(client_path)? + .to_string_lossy() + .to_string(), + ); + + Ok(classpaths + .into_iter() + .collect::>() + .join(constants::CLASSPATH_SEPARATOR)) +} + +pub fn get_classpath_library>( + libraries_path: &Path, + libraries: &[T], +) -> LauncherResult { + let classpaths = libraries + .iter() + .map(|lib| get_library(libraries_path, lib.as_ref(), false)) + .collect::, _>>()?; + + Ok(classpaths.join(constants::CLASSPATH_SEPARATOR)) +} + +pub fn get_library( + libraries_path: &Path, + library: &str, + error_exist: bool, +) -> LauncherResult { + let mut path = libraries_path.to_path_buf(); + path.push(get_path_from_artifact(library)?); + + if !path.exists() && error_exist { + return Ok(path.to_string_lossy().to_string()); + } + + let path = &io::canonicalize(&path)?; + + Ok(path.to_string_lossy().to_string()) +} + +fn parse_arguments( + args: &[Argument], + parsed: &mut Vec, + parse_function: ParseFn, + java_arch: &str, +) -> LauncherResult<()> +where + ParseFn: Fn(&str) -> LauncherResult, +{ + for arg in args { + match arg { + Argument::Normal(a) => { + let p = parse_function(&a.replace(' ', DUMMY_REPLACE_NEWLINE))?; + for split_p in p.split(DUMMY_REPLACE_NEWLINE) { + parsed.push(split_p.to_string()); + } + } + Argument::Ruled { rules, value } => { + if super::rules::validate_rules(rules, java_arch, true) { + match value { + ArgumentValue::Single(arg) => { + parsed.push(parse_function(&arg.replace(' ', DUMMY_REPLACE_NEWLINE))?); + } + ArgumentValue::Many(args) => { + for arg in args { + parsed.push(parse_function( + &arg.replace(' ', DUMMY_REPLACE_NEWLINE), + )?); + } + } + } + } + } + } + } + + Ok(()) +} diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/src/api/game/metadata.rs b/packages/core/src/api/game/metadata.rs index bbe834a6..6a02b652 100644 --- a/packages/core/src/api/game/metadata.rs +++ b/packages/core/src/api/game/metadata.rs @@ -1,35 +1,33 @@ use std::path::PathBuf; use futures::{StreamExt, stream}; -use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, OsRule, Rule, RuleAction, Version, VersionInfo}; +use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, Version, VersionInfo}; use interpulse::api::modded::LoaderVersion; -use regex::Regex; +use onelauncher_entity::loader::GameLoader; use reqwest::Method; -use crate::api::ingress::{init_ingress, send_ingress_ref_opt}; +use crate::api::ingress::{init_ingress, IngressSendExt}; use crate::error::{LauncherError, LauncherResult}; -use crate::store::ingress::{IngressRef, IngressType}; -use crate::store::{Core, Dirs}; +use crate::store::ingress::{IngressType, SubIngress, SubIngressExt}; +use crate::store::metadata::MetadataError; +use crate::store::{Core, Dirs, State}; use crate::utils::crypto::HashAlgorithm; -use crate::utils::ext::OsExt; -use crate::utils::http; -use crate::utils::io; +use crate::utils::os_ext::OsExt; +use crate::utils::{http, io}; pub async fn download_minecraft_ingressed( version: &VersionInfo, java_arch: &str, - force: Option + force: Option, ) -> LauncherResult<()> { let id = init_ingress( IngressType::MinecraftDownload, - &format!("Downloading Minecraft {}", version.id), + &format!("downloading Minecraft {}", version.id), 100.0, ) .await?; - let ingress_ref = IngressRef::new(&id, 100.0); - - download_minecraft(version, java_arch, Some(&ingress_ref), force).await + download_minecraft(version, java_arch, Some(&SubIngress::new(&id, 100.0)), force).await } // MARK: Main @@ -37,19 +35,15 @@ pub async fn download_minecraft_ingressed( pub async fn download_minecraft( version: &VersionInfo, java_arch: &str, - ingress_ref: Option<&IngressRef<'_>>, - force: Option + sub_ingress: Option<&SubIngress<'_>>, + force: Option, ) -> LauncherResult<()> { - const TASKS: f64 = 4.0; - let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / TASKS)); - let ingress_ref = ingress_ref.as_ref(); - - let asset_index = download_assets_index(version, ingress_ref, force).await?; + let asset_index = download_assets_index(version, sub_ingress, force).await?; tokio::try_join! { - download_assets(version.assets == "legacy", &asset_index, ingress_ref, force), - download_client(version, ingress_ref, force), - download_libraries(&version.id, &version.libraries, java_arch, ingress_ref, force), + download_assets(version.assets == "legacy", &asset_index, sub_ingress, force), + download_client(version, sub_ingress, force), + download_libraries(&version.id, &version.libraries, java_arch, sub_ingress, force), }?; Ok(()) @@ -60,14 +54,15 @@ pub async fn download_minecraft( pub async fn download_version_info( version: &Version, loader: Option<&LoaderVersion>, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { + const TASKS: f64 = 3.33; + tracing::debug!("loading version info for version {}", version.id); let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id)); - let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 3.33)); - let ingress_ref = ingress_ref.as_ref(); + ingress.set_ingress_message("fetching version info").await?; tracing::debug!("loading minecraft version info for minecraft version {version_id}"); @@ -86,11 +81,12 @@ pub async fn download_version_info( ); let mut info = - http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress_ref).await?; + http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress) + .await?; if let Some(loader) = loader { let partial: interpulse::api::modded::PartialVersionInfo = - http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress_ref) + http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress) .await?; info = interpulse::api::modded::merge_partial_version(partial, info); @@ -98,12 +94,17 @@ pub async fn download_version_info( info.id.clone_from(&version_id); - io::create_dir_all(&path.parent().unwrap_or_else(|| panic!("couldn't get version path parent"))).await?; + io::create_dir_all( + &path + .parent() + .unwrap_or_else(|| panic!("couldn't get version path parent")), + ) + .await?; io::write(&path, &serde_json::to_vec(&info)?).await?; info }; - send_ingress_ref_opt(ingress_ref).await?; + ingress.send_ingress(ingress.ingress_total().map(|i| i / TASKS).unwrap_or_default()).await?; tracing::debug!("loaded minecraft version info for minecraft version {version_id}"); Ok(result) @@ -113,7 +114,7 @@ pub async fn download_version_info( #[tracing::instrument(skip_all)] pub async fn download_assets_index( version: &VersionInfo, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { tracing::debug!("loading assets index for version {}", version.id); @@ -121,30 +122,34 @@ pub async fn download_assets_index( .await? .join(version.asset_index.id.clone()); - let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0)); - let ingress_ref = ingress_ref.as_ref(); - send_ingress_ref_opt(ingress_ref).await?; + // let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0)); + // let ingress_ref = ingress_ref.as_ref(); + // ingress_ref.map(async |i| send_ingress_ref_message(i, Some("fetching assets index")).await); + let ingress_step = ingress.ingress_total().map(|i| i / 2.0).unwrap_or_default(); + ingress.set_ingress_message("fetching assets index").await?; if path.exists() && !force.unwrap_or(false) { match io::read_json::(&path).await { Ok(data) => { - send_ingress_ref_opt(ingress_ref).await?; + ingress.send_ingress(ingress_step).await?; return Ok(data); } - Err(err) => tracing::error!( - "failed to read assets index from cache: {err:?}, downloading...", - ), + Err(err) => { + tracing::error!("failed to read assets index from cache: {err:?}, downloading...",); + } } } - let data = serde_json::from_slice(&http::download( - Method::GET, - &version.asset_index.url, - path, - None, - ingress_ref, - ) - .await?)?; + let data = serde_json::from_slice( + &http::download( + Method::GET, + &version.asset_index.url, + path, + None, + ingress.map(|i| SubIngress::from_sub(i, ingress_step)).as_ref(), + ) + .await?, + )?; tracing::debug!("loaded assets index for version {}", version.id); @@ -157,7 +162,7 @@ pub async fn download_assets_index( pub async fn download_assets( legacy: bool, assets_index: &AssetsIndex, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { tracing::debug!("loading asssets"); @@ -171,8 +176,7 @@ pub async fn download_assets( io::create_dir_all(&dir).await?; - let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / len as f64)); - let ingress_ref = ingress_ref.as_ref(); + ingress.set_ingress_message("fetching assets").await?; let requests = stream::iter(assets_index.objects.iter().map(|(name, asset)| async { let hash = &asset.hash; @@ -191,9 +195,10 @@ pub async fn download_assets( None, None, Some((HashAlgorithm::Sha1, hash)), - ingress_ref, + ingress.ingress_sub(|total| total / len as f64).as_ref(), ) - .await.map(|_| ()) + .await + .map(|_| ()) } else { // TODO: Possibly check hash? (not sure if this is a good idea here) Ok(()) @@ -217,15 +222,20 @@ pub async fn download_assets( // MARK: Client pub async fn download_client( version: &VersionInfo, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { tracing::debug!("loading client for version {}", version.id); - let client = version.downloads + let client = version + .downloads .get(&DownloadType::Client) .ok_or_else(|| anyhow::anyhow!("no client downloads exist for {}", &version.id))?; - let path = Dirs::get_versions_dir().await?.join(version.id.clone()).join(format!("{}.jar", version.id)); + let path = Dirs::get_versions_dir() + .await? + .join(version.id.clone()) + .join(format!("{}.jar", version.id)); + if path.exists() && !force.unwrap_or(false) { let calculated_hash = HashAlgorithm::Sha1 .hash_file(&path) @@ -240,10 +250,14 @@ pub async fn download_client( tracing::warn!("client hash mismatch, redownloading"); } - let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0)); - let ingress_ref = ingress_ref.as_ref(); - - http::download(Method::GET, &client.url, &path, Some((HashAlgorithm::Sha1, &client.sha1)), ingress_ref).await?; + http::download( + Method::GET, + &client.url, + &path, + Some((HashAlgorithm::Sha1, &client.sha1)), + ingress, + ) + .await?; Ok(path) } @@ -254,7 +268,7 @@ pub async fn download_libraries( version: &str, libraries: &[Library], java_arch: &str, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, force: Option, ) -> LauncherResult { tracing::debug!("loading libraries for version {}", version); @@ -266,13 +280,13 @@ pub async fn download_libraries( io::create_dir_all(&natives_dir).await?; let num_files = libraries.len(); - - let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / num_files as f64)); - let ingress_ref = ingress_ref.as_ref(); + let ingress = ingress.ingress_sub(|total| total / num_files as f64); + let ingress = ingress.as_ref(); + ingress.set_ingress_message("fetching libraries").await?; let requests = stream::iter(libraries.iter().map(|lib| async { if let Some(rules) = &lib.rules { - if !validate_rules(rules, java_arch, lib.natives.is_some()) { + if !super::rules::validate_rules(rules, java_arch, lib.natives.is_some()) { tracing::debug!("skipping library {} due to rules", lib.name); return Ok::<(), LauncherError>(()); } @@ -301,7 +315,7 @@ pub async fn download_libraries( &artifact.url, &path, Some((HashAlgorithm::Sha1, &artifact.sha1)), - ingress_ref, + ingress, ).await?; tracing::trace!("fetched library {} to path {:?}", &lib.name, &path); @@ -315,7 +329,7 @@ pub async fn download_libraries( &url, &path, None, - ingress_ref, + ingress, ).await?; tracing::trace!("fetched library {} to path {:?}", &lib.name, &path); @@ -335,7 +349,7 @@ pub async fn download_libraries( None, Some((HashAlgorithm::Sha1, &native.sha1)), None, - ingress_ref, + ingress, ).await?; let dest = natives_dir.join(version); @@ -362,86 +376,37 @@ pub async fn download_libraries( Ok(failed) } -// MARK: Rules -#[tracing::instrument(level = "debug")] -fn validate_rules(rules: &[Rule], java_version: &str, updated: bool) -> bool { - let mut rule = rules - .iter() - .map(|r| validate_rule(r, java_version, updated)) - .collect::>>(); - if rules - .iter() - .all(|r| matches!(r.action, RuleAction::Disallow)) - { - rule.push(Some(true)); +// MARK: Loader Version +/// Gets the loader version for a given Minecraft version and loader +/// If `loader_version` is `None`, it will return the latest stable version else +/// it will return the specified version if found +pub async fn get_loader_version( + mc_version: &str, + loader: GameLoader, + loader_version: Option<&str>, +) -> LauncherResult> { + if loader == GameLoader::Vanilla { + return Ok(None); } - !(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(Option::is_none)) -} + let state = State::get().await?; + let metadata = state.metadata.read().await; + let manifest = metadata.get_modded(loader)?; -/// Parses a Minecraft library feature or OS rule. -/// Is disallowed -> Don't include it -/// Is not allowed -> Don't include it -/// Is allowed -> Include it -#[tracing::instrument(level = "debug")] -fn validate_rule(rule: &Rule, java_version: &str, updated: bool) -> Option { - let result = match rule { - Rule { - os: Some(os), .. - } => validate_os_rule(os, java_version, updated), - Rule { - features: Some(features), - .. - } => { - !features.is_demo_user.unwrap_or(true) - || features.has_custom_resolution.unwrap_or(false) - || !features.has_quick_plays_support.unwrap_or(true) - || !features.is_quick_play_multiplayer.unwrap_or(true) - || !features.is_quick_play_realms.unwrap_or(true) - || !features.is_quick_play_singleplayer.unwrap_or(true) - } - _ => return Some(true), + let Some(loaders) = manifest.game_versions.iter().find(|it| { + it.id + .replace(interpulse::api::modded::DUMMY_REPLACE_STRING, mc_version) + == mc_version + }) else { + return Err(MetadataError::NoMatchingVersion.into()); }; - match rule.action { - RuleAction::Allow => { - if result { - Some(true) - } else { - Some(false) - } - } - RuleAction::Disallow => { - if result { - Some(false) - } else { - None - } - } - } + let loader_version = loaders + .loaders + .iter() + .find(|it| loader_version.map_or(it.stable, |version| it.id == version)) + .cloned() + .ok_or(MetadataError::NoMatchingLoader)?; + + Ok(Some(loader_version)) } - -#[must_use] -fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool { - let mut rule_match = true; - - if let Some(ref arch) = rule.arch { - rule_match &= !matches!(arch.as_str(), "x86" | "arm"); - } - - if let Some(name) = &rule.name { - if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) { - rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name; - } else { - rule_match &= &Os::native_arch(java_arch) == name; - } - } - - if let Some(version) = &rule.version { - if let Ok(regex) = Regex::new(version.as_str()) { - rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default()); - } - } - - rule_match -} \ No newline at end of file diff --git a/packages/core/src/api/game/mod.rs b/packages/core/src/api/game/mod.rs index 7d9beb3c..ec9174cd 100644 --- a/packages/core/src/api/game/mod.rs +++ b/packages/core/src/api/game/mod.rs @@ -1 +1,4 @@ -pub mod metadata; \ No newline at end of file +pub mod metadata; +pub mod launch; +pub mod arguments; +pub mod rules; \ No newline at end of file diff --git a/packages/core/src/api/game/rules.rs b/packages/core/src/api/game/rules.rs new file mode 100644 index 00000000..73475f72 --- /dev/null +++ b/packages/core/src/api/game/rules.rs @@ -0,0 +1,85 @@ +use interpulse::api::minecraft::{Os, OsRule, Rule, RuleAction}; +use regex::Regex; + +use crate::utils::os_ext::OsExt; + +#[tracing::instrument(level = "debug")] +pub fn validate_rules(rules: &[Rule], java_arch: &str, updated: bool) -> bool { + let mut rule = rules + .iter() + .map(|r| validate_rule(r, java_arch, updated)) + .collect::>>(); + if rules + .iter() + .all(|r| matches!(r.action, RuleAction::Disallow)) + { + rule.push(Some(true)); + } + + !(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(Option::is_none)) +} + +/// Parses a Minecraft library feature or OS rule. +/// Is disallowed -> Don't include it +/// Is not allowed -> Don't include it +/// Is allowed -> Include it +#[tracing::instrument(level = "debug")] +pub fn validate_rule(rule: &Rule, java_arch: &str, updated: bool) -> Option { + let result = match rule { + Rule { os: Some(os), .. } => validate_os_rule(os, java_arch, updated), + Rule { + features: Some(features), + .. + } => { + !features.is_demo_user.unwrap_or(true) + || features.has_custom_resolution.unwrap_or(false) + || !features.has_quick_plays_support.unwrap_or(true) + || !features.is_quick_play_multiplayer.unwrap_or(true) + || !features.is_quick_play_realms.unwrap_or(true) + || !features.is_quick_play_singleplayer.unwrap_or(true) + } + _ => return Some(true), + }; + + match rule.action { + RuleAction::Allow => { + if result { + Some(true) + } else { + Some(false) + } + } + RuleAction::Disallow => { + if result { + Some(false) + } else { + None + } + } + } +} + +#[must_use] +pub fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool { + let mut rule_match = true; + + if let Some(ref arch) = rule.arch { + rule_match &= !matches!(arch.as_str(), "x86" | "arm"); + } + + if let Some(name) = &rule.name { + if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) { + rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name; + } else { + rule_match &= &Os::native_arch(java_arch) == name; + } + } + + if let Some(version) = &rule.version { + if let Ok(regex) = Regex::new(version.as_str()) { + rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default()); + } + } + + rule_match +} \ No newline at end of file diff --git a/packages/core/src/api/ingress.rs b/packages/core/src/api/ingress.rs index a455eef5..ec5c9c04 100644 --- a/packages/core/src/api/ingress.rs +++ b/packages/core/src/api/ingress.rs @@ -1,4 +1,6 @@ -use crate::{store::{ingress::{IngressId, IngressRef, IngressType}, State}, LauncherResult}; +use crate::LauncherResult; +use crate::store::State; +use crate::store::ingress::{IngressId, IngressType, SubIngress}; #[tracing::instrument] pub async fn init_ingress( @@ -9,7 +11,9 @@ pub async fn init_ingress( let state = State::get().await?; let processor = &state.ingress_processor; - let id = processor.create(ingress_type, message.to_string(), total).await?; + let id = processor + .create(ingress_type, message.to_string(), total) + .await?; Ok(id) } @@ -28,69 +32,168 @@ pub async fn init_ingress_opt( } } +/// Sends an ingress update with the given increment and message #[tracing::instrument] -pub async fn send_ingress(id: impl AsRef + std::fmt::Debug, increment: f64) -> LauncherResult<()> { +pub async fn send_ingress_message( + id: &IngressId, + increment: f64, + message: Option<&str>, +) -> LauncherResult<()> { if !State::initialized() { tracing::debug!("attempted to send ingress when state is not initialized"); - return Ok(()) + return Ok(()); } let state = State::get().await?; let processor = &state.ingress_processor; - processor.send(id, increment).await?; + processor + .send(id, increment, message.map(String::from)) + .await?; Ok(()) } -pub async fn send_ingress_opt(id: Option + std::fmt::Debug>, increment: f64) -> LauncherResult<()> { - if let Some(id) = id { - send_ingress(id, increment).await - } else { - Ok(()) +/// Sends an ingress update with the given increment +pub async fn send_ingress( + id: &IngressId, + increment: f64, +) -> LauncherResult<()> { + send_ingress_message(id, increment, None).await +} + +/// Sends an ingress update with the given message (does not increment) +pub async fn set_ingress_message( + id: &IngressId, + message: &str, +) -> LauncherResult<()> { + send_ingress_message(id, 0.0, Some(message)).await +} + +#[async_trait::async_trait] +pub trait IngressSendExt { + async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()>; + async fn send_ingress(&self, increment: f64) -> LauncherResult<()>; + async fn set_ingress_message(&self, message: &str) -> LauncherResult<()>; +} + +#[async_trait::async_trait] +impl IngressSendExt for IngressId { + async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + send_ingress_message(self, increment, message).await + } + + async fn send_ingress(&self, increment: f64) -> LauncherResult<()> { + send_ingress(self, increment).await + } + + async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> { + set_ingress_message(self, message).await } } -pub async fn send_ingress_ref(ingress_ref: &IngressRef<'_>) -> LauncherResult<()> { - let state = State::get().await?; - let processor = &state.ingress_processor; +#[async_trait::async_trait] +impl IngressSendExt for Option<&IngressId> { + async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + if let Some(id) = self { + id.send_ingress_message(increment, message).await + } else { + Ok(()) + } + } - processor.send(ingress_ref, ingress_ref.increment_by).await?; - Ok(()) + async fn send_ingress(&self, increment: f64) -> LauncherResult<()> { + if let Some(id) = self { + id.send_ingress(increment).await + } else { + Ok(()) + } + } + + async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> { + if let Some(id) = self { + id.set_ingress_message(message).await + } else { + Ok(()) + } + } } -pub async fn send_ingress_ref_opt(ingress_ref: Option<&IngressRef<'_>>) -> LauncherResult<()> { - if let Some(ingress_ref) = ingress_ref { - send_ingress_ref(ingress_ref).await - } else { - Ok(()) +#[async_trait::async_trait] +impl IngressSendExt for &SubIngress<'_> { + async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + self.id.send_ingress_message(increment, message).await + } + + async fn send_ingress(&self, increment: f64) -> LauncherResult<()> { + self.id.send_ingress(increment).await + } + + async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> { + self.id.set_ingress_message(message).await + } +} + +#[async_trait::async_trait] +impl IngressSendExt for Option<&SubIngress<'_>> { + async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + if let Some(id) = self { + id.send_ingress_message(increment, message).await + } else { + Ok(()) + } + } + + async fn send_ingress(&self, increment: f64) -> LauncherResult<()> { + if let Some(id) = self { + id.send_ingress(increment).await + } else { + Ok(()) + } + } + + async fn set_ingress_message(&self, message: &str) -> LauncherResult<()> { + if let Some(id) = self { + id.set_ingress_message(message).await + } else { + Ok(()) + } } } #[cfg(test)] pub mod tests { + use crate::api::ingress::{init_ingress, IngressSendExt}; + use crate::api::proxy::ProxyDynamic; + use crate::initialize_core; + use crate::store::CoreOptions; + use crate::store::ingress::IngressType; use std::time::Duration; - use crate::{api::{ingress::{init_ingress, send_ingress}, proxy::ProxyDynamic}, initialize_core, store::{ingress::IngressType, CoreOptions}}; #[tokio::test] pub async fn create_and_update_ingress() -> crate::LauncherResult<()> { - initialize_core(CoreOptions::default(), ProxyDynamic::new()).await?; - let id = init_ingress(IngressType::Download { file_name: "Some-Mod-1.8.9.jar".into() }, "This is a test message", 100.0).await?; + let id = init_ingress( + IngressType::Download { + file_name: "Some-Mod-1.8.9.jar".into(), + }, + "this is a test message", + 100.0, + ) + .await?; tokio::time::sleep(Duration::from_millis(2350)).await; - send_ingress(&id, 10.0).await?; + id.send_ingress(10.0).await?; tokio::time::sleep(Duration::from_millis(2500)).await; - send_ingress(&id, 20.0).await?; + id.send_ingress(20.0).await?; tokio::time::sleep(Duration::from_millis(2500)).await; - send_ingress(&id, 70.0).await?; + id.send_ingress(70.0).await?; tokio::time::sleep(Duration::from_millis(2500)).await; drop(id); Ok(()) } - -} \ No newline at end of file +} diff --git a/packages/core/src/api/java/dao.rs b/packages/core/src/api/java/dao.rs index f2456a7b..e268efc4 100644 --- a/packages/core/src/api/java/dao.rs +++ b/packages/core/src/api/java/dao.rs @@ -1,12 +1,23 @@ use std::path::{Path, PathBuf}; use onelauncher_entity::java_versions; -use sea_orm::{ActiveValue::Set, ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder}; +use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder}; use crate::{error::LauncherResult, store::State}; use super::{JavaError, JavaInfo}; +/// Returns the latest Java version +pub async fn get_latest_java() -> LauncherResult> { + let db = &State::get().await?.db; + + Ok(java_versions::Entity::find() + .order_by(java_versions::Column::MajorVersion, Order::Desc) + .order_by(java_versions::Column::FullVersion, Order::Desc) + .one(db) + .await?) +} + /// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`] pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult { let major_version = if info.java_version[0..2].eq("1.") { @@ -14,13 +25,16 @@ pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult() - }.map_err(JavaError::from)?; + }.or_else( + |_| info.java_version.parse::() + ).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?; Ok(java_versions::ActiveModel { absolute_path: Set(absolute_path.to_string_lossy().to_string()), major_version: Set(major_version), full_version: Set(info.java_version.clone()), vendor_name: Set(info.java_vendor.clone()), + arch: Set(info.os_arch.clone()), ..Default::default() }) } @@ -46,7 +60,7 @@ pub async fn get_java_by_id(id: i32) -> LauncherResult LauncherResult> { +pub async fn get_all_java_by_major(major: i32) -> LauncherResult> { let db = &State::get().await?.db; Ok(java_versions::Entity::find() @@ -56,6 +70,17 @@ pub async fn get_java_by_major(major: i32) -> LauncherResult LauncherResult> { + let db = &State::get().await?.db; + + Ok(java_versions::Entity::find() + .filter(java_versions::Column::MajorVersion.eq(major)) + .order_by(java_versions::Column::FullVersion, Order::Desc) + .one(db) + .await?) +} + /// Accepts a path to a JRE folder and returns the inserted entry pub async fn insert_java(absolute_path: PathBuf, info: JavaInfo) -> LauncherResult { let db = &State::get().await?.db; diff --git a/packages/core/src/api/java/mod.rs b/packages/core/src/api/java/mod.rs index bc8cc9bd..8eebec31 100644 --- a/packages/core/src/api/java/mod.rs +++ b/packages/core/src/api/java/mod.rs @@ -1,15 +1,21 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::collections::HashMap; +use std::path::PathBuf; +use interpulse::api::minecraft::VersionInfo; +use onelauncher_entity::{java_versions, setting_profiles}; use serde::Serialize; -use crate::{api::ingress::{init_ingress, init_ingress_opt, send_ingress, send_ingress_opt}, error::LauncherResult, store::ingress::IngressType, utils::io}; +use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress}; +use crate::error::LauncherResult; +use crate::store::ingress::IngressType; +use crate::utils::io; pub mod dao; #[derive(Debug, thiserror::Error)] pub enum JavaError { - #[error("failed to parse version")] - ParseVersion(#[from] std::num::ParseIntError), + #[error("failed to parse version '{0}'")] + ParseVersion(String, #[source] std::num::ParseIntError), #[error("failed to execute java command")] Execute(#[from] std::io::Error), } @@ -46,16 +52,25 @@ pub struct JavaInfo { const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../../assets/java/JavaInfo.class"); /// Accepts a path to a java executable and returns the [`JavaInfo`] -pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> LauncherResult { - let id = init_ingress_opt(with_ingress, IngressType::JavaCheck, "Checking JRE information", 100.0).await?; +pub async fn check_java_runtime( + absolute_path: &PathBuf, + with_ingress: bool, +) -> LauncherResult { + let id = init_ingress_opt( + with_ingress, + IngressType::JavaCheck, + "checking JRE information", + 100.0, + ) + .await?; let id = id.as_ref(); let dir = io::tempdir()?; let file = dir.path().join("JavaInfo.class"); - send_ingress_opt(id, 25.0).await?; + id.map(async |id| send_ingress(id, 25.0).await); io::write(&file, JAVA_INFO_CLASS).await?; - send_ingress_opt(id, 25.0).await?; + id.map(async |id| send_ingress(id, 25.0).await); let java_info = tokio::process::Command::new(absolute_path) .arg("-cp") @@ -67,9 +82,10 @@ pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> .map_err(JavaError::from)?; let java_info = String::from_utf8_lossy(&java_info.stdout); - send_ingress_opt(id, 50.0).await?; + id.map(async |id| send_ingress(id, 50.0).await); - let info = java_info.lines() + let info = java_info + .lines() .map(|line| { let mut parts = line.splitn(2, '='); let key = parts.next().unwrap_or("unknown"); @@ -80,17 +96,57 @@ pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> .collect::>(); Ok(JavaInfo { - os_arch: info.get("os.arch").cloned().unwrap_or_else(|| String::from("unknown")), - java_version: info.get("java.version").cloned().unwrap_or_else(|| String::from("unknown")), - java_vendor: info.get("java.vendor").cloned().unwrap_or_else(|| String::from("unknown")), + os_arch: info + .get("os.arch") + .cloned() + .unwrap_or_else(|| String::from("unknown")), + java_version: info + .get("java.version") + .cloned() + .unwrap_or_else(|| String::from("unknown")), + java_vendor: info + .get("java.vendor") + .cloned() + .unwrap_or_else(|| String::from("unknown")), }) } +// MARK: Recommended Java Version +/// Gets the recommended java version for the given [`VersionInfo`] and optionally profile (excl. "Global"). +pub async fn get_recommended_java( + info: &VersionInfo, + profile: Option<&setting_profiles::Model>, +) -> LauncherResult> { + // Settings profile is an override (it has highest priority) + if let Some(profile) = profile + && !profile.is_global() + { + if let Some(java_id) = profile.java_id { + if let Ok(Some(java)) = dao::get_java_by_id(java_id).await { + return Ok(Some(java)); + } + } + } + + // Check if the version info has a suggested version + let Some(supported_ver) = &info.java_version else { + return Ok(None); + }; + + #[allow(clippy::cast_possible_wrap)] + dao::get_latest_java_by_major(supported_ver.major_version as i32).await +} + /// Attempts to scan common paths for Java installations. /// /// **Can be heavy on I/O, don't run often!** pub async fn locate_java() -> LauncherResult> { - let id = init_ingress(IngressType::JavaLocate, "Locating java installations", 110.0).await?; + let id = init_ingress( + IngressType::JavaLocate, + "locating java installations", + 110.0, + ) + .await?; let paths = internal_locate_java(); let mut valid = HashMap::new(); @@ -103,7 +159,7 @@ pub async fn locate_java() -> LauncherResult> { send_ingress(&id, step_amount).await?; let Ok(info) = check_java_runtime(&path, false).await else { - tracing::warn!("Java installation at '{}' is not valid", path.display()); + tracing::warn!("java installation at '{}' is not valid", path.display()); continue; }; @@ -171,13 +227,14 @@ fn internal_locate_java() -> Vec { r"/opt/jdk", r"/opt/jdks", r"/app/jdk", - r"~/.jdks", // Intellij downloaded jdks - r"~/.gradle/jdks", // Gradle downloaded jdks + r"~/.jdks", // Intellij downloaded jdks + r"~/.gradle/jdks", // Gradle downloaded jdks r"~/.sdkman/candidates/java", // SDKMAN downloaded jdks ]; for path in paths { - let path = PathBuf::from(path.replace('~', std::env::var("HOME").unwrap_or_default().as_str())); + let path = + PathBuf::from(path.replace('~', std::env::var("HOME").unwrap_or_default().as_str())); if let Ok(children) = std::fs::read_dir(path) { for child in children.flatten() { @@ -187,8 +244,7 @@ fn internal_locate_java() -> Vec { } } } - } found -} \ No newline at end of file +} diff --git a/packages/core/src/api/mod.rs b/packages/core/src/api/mod.rs index ba127915..0b05697c 100644 --- a/packages/core/src/api/mod.rs +++ b/packages/core/src/api/mod.rs @@ -2,4 +2,5 @@ pub mod cluster; pub mod ingress; pub mod game; pub mod proxy; -pub mod java; \ No newline at end of file +pub mod java; +pub mod setting_profiles; \ No newline at end of file diff --git a/packages/core/src/api/setting_profiles.rs b/packages/core/src/api/setting_profiles.rs new file mode 100644 index 00000000..43fcf0fa --- /dev/null +++ b/packages/core/src/api/setting_profiles.rs @@ -0,0 +1,60 @@ +use onelauncher_entity::setting_profiles; +use sea_orm::{ActiveModelTrait, EntityTrait}; + +use crate::{error::{DaoError, LauncherResult}, store::State}; + +pub async fn get_all_profiles() -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + Ok(setting_profiles::Entity::find() + .all(db) + .await?) +} + +pub async fn get_profile_or_default(name: Option<&String>) -> LauncherResult { + if let Some(name) = name { + let profile = get_profile_by_name(name).await?; + if let Some(profile) = profile { + return Ok(profile); + } + } + + let state = State::get().await?; + let settings = &state.settings.read().await; + Ok(settings.global_game_settings.clone()) +} + +pub async fn get_profile_by_name(name: &str) -> LauncherResult> { + let state = State::get().await?; + let db = &state.db; + + Ok(setting_profiles::Entity::find_by_id(name) + .one(db) + .await?) +} + +pub async fn insert_profile(profile: setting_profiles::ActiveModel) -> LauncherResult { + if let Some(name) = profile.name.try_as_ref() { + if name.is_empty() || profile.is_global() { + return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into()); + } + } + + let state = State::get().await?; + let db = &state.db; + + Ok(profile.insert(db).await?) +} + +pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> { + let state = State::get().await?; + let db = &state.db; + + let deleted = setting_profiles::Entity::delete_by_id(name).exec(db).await?; + + if deleted.rows_affected == 0 { + return Err(DaoError::NotFound.into()); + } + Ok(()) +} \ No newline at end of file diff --git a/packages/core/src/constants.rs b/packages/core/src/constants.rs index 3a761612..d79b2950 100644 --- a/packages/core/src/constants.rs +++ b/packages/core/src/constants.rs @@ -80,3 +80,4 @@ pub const ARCH_WIDTH: &str = if cfg!(target_pointer_width = "64") { /// Evaluates to `\r\n` on windows, and `\n` on everything else. pub const LINE_ENDING: &str = if cfg!(windows) { "\r\n" } else { "\n" }; +pub const CLASSPATH_SEPARATOR: &str = if cfg!(windows) { ";" } else { ":" }; diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index 059051d5..00b31244 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -16,6 +16,8 @@ pub enum LauncherError { MetadataError(#[from] crate::store::metadata::MetadataError), #[error(transparent)] DaoError(#[from] DaoError), + #[error(transparent)] + ClusterError(#[from] crate::api::cluster::ClusterError), #[error(transparent)] SerdeError(#[from] serde_json::Error), @@ -42,5 +44,7 @@ pub enum DaoError { #[error("entity was not found")] NotFound, #[error("entity already exists")] - AlreadyExists + AlreadyExists, + #[error("invalid value '{0}' for {1}")] + InvalidValue(String, String), } diff --git a/packages/core/src/store/db.rs b/packages/core/src/store/db.rs index 0d36405a..ab96e24e 100644 --- a/packages/core/src/store/db.rs +++ b/packages/core/src/store/db.rs @@ -1,5 +1,6 @@ use std::{path::PathBuf, time::Duration}; +use onelauncher_migration::MigratorTrait; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use crate::{utils::io::IOError, LauncherResult}; @@ -20,6 +21,8 @@ pub async fn create_pool_from_path(path: PathBuf) -> LauncherResult LauncherResult { + pub async fn create( + &self, + ingress_type: IngressType, + message: String, + total: f64, + ) -> LauncherResult { let mut feeds = self.ingress_feeds.write().await; let uuid = Uuid::new_v4(); @@ -52,80 +58,51 @@ impl IngressProcessor { // Drop the write lock to prevent deadlock drop(feeds); - self.send(&ingress_id, 0.0).await?; + self.send(&ingress_id, 0.0, None).await?; Ok(ingress_id) } - pub async fn send(&self, id: impl AsRef, increment: f64) -> LauncherResult<()> { + pub async fn send( + &self, + id: &IngressId, + increment: f64, + message: Option, + ) -> LauncherResult<()> { let mut feeds = self.ingress_feeds.write().await; - let uuid = &id.as_ref().0; + let uuid = &id.0; let ingress = feeds.get_mut(uuid).ok_or(IngressError::NotFound)?; let proxy = ProxyState::get()?; ingress.current += increment; - proxy.send_ingress(IngressPayload { - id: uuid.to_owned(), - message: ingress.message.clone(), - ingress_type: ingress.ingress_type.clone(), - percent: Some(ingress.current / ingress.total), - total: ingress.total, - }).await?; + if let Some(message) = message { + ingress.message = message; + } + + proxy + .send_ingress(IngressPayload { + id: uuid.to_owned(), + message: ingress.message.clone(), + ingress_type: ingress.ingress_type.clone(), + percent: Some(ingress.current / ingress.total), + total: ingress.total, + }) + .await?; Ok(()) } + pub async fn set_message(&self, id: &IngressId, message: String) -> LauncherResult<()> { + self.send(id, 0.0, Some(message)).await + } + pub async fn remove(&self, uuid: Uuid) -> LauncherResult { let mut feeds = self.ingress_feeds.write().await; Ok(feeds.remove(&uuid).ok_or(IngressError::NotFound)?) } } - -#[derive(Debug, Clone)] -pub struct IngressId(pub Uuid); - -impl AsRef for IngressId { - fn as_ref(&self) -> &Self { - self - } -} - -/// Used to pass around an ingress id and the increment value, usually passed in helper / utility functions -#[derive(Debug, Clone)] -pub struct IngressRef<'a> { - pub ingress_id: &'a IngressId, - pub increment_by: f64, -} - -unsafe impl Send for IngressRef<'_> {} -unsafe impl Sync for IngressRef<'_> {} - -impl<'a> IngressRef<'a> { - #[must_use] - pub const fn new(ingress_id: &'a IngressId, increment_by: f64) -> Self { - Self { - ingress_id, - increment_by, - } - } - - #[must_use] - pub const fn with_increment(&self, increment_by: f64) -> Self { - Self { - ingress_id: self.ingress_id, - increment_by, - } - } -} - -impl AsRef for IngressRef<'_> { - fn as_ref(&self) -> &IngressId { - self.ingress_id - } -} - #[derive(Debug, Clone)] pub struct Ingress { pub id: Uuid, @@ -149,12 +126,84 @@ pub struct IngressPayload { #[onelauncher_macro::specta] #[derive(Serialize, Debug, Clone)] pub enum IngressType { - Download { - file_name: String, - }, + Download { file_name: String }, JavaCheck, JavaLocate, MinecraftDownload, + PrepareCluster { cluster_name: String }, +} + +#[derive(Debug, Clone)] +pub struct IngressId(pub Uuid); + +#[derive(Debug, Clone)] +pub struct SubIngress<'a> { + pub id: &'a IngressId, + pub total: f64, +} + +// TODO: Rewrite the ingress system to use a proper parent <-> child system +impl<'a> SubIngress<'a> { + #[must_use] + pub const fn new(id: &'a IngressId, total: f64) -> Self { + Self { id, total } + } + + #[must_use] + pub const fn from_sub(sub: &'a SubIngress<'_>, total: f64) -> Self { + Self { id: sub.id, total } + } +} + +pub trait SubIngressExt { + type Target<'sub> + where + Self: 'sub; + + type Total; + + fn ingress_total(&self) -> Self::Total; + fn ingress_sub(&self, total: F) -> Self::Target<'_> + where + F: FnOnce(f64) -> f64; +} + +impl SubIngressExt for SubIngress<'_> { + type Target<'sub> + = SubIngress<'sub> + where + Self: 'sub; + + type Total = f64; + + fn ingress_total(&self) -> Self::Total { + self.total + } + + fn ingress_sub(&self, total: F) -> Self::Target<'_> + where + F: FnOnce(f64) -> f64 { + SubIngress::from_sub(self, total(self.total)) + } +} + +impl SubIngressExt for Option<&SubIngress<'_>> { + type Target<'sub> + = Option> + where + Self: 'sub; + + type Total = Option; + + fn ingress_total(&self) -> Self::Total { + self.map(|sub| sub.total) + } + + fn ingress_sub(&self, total: F) -> Self::Target<'_> + where + F: FnOnce(f64) -> f64 { + self.map(|sub| SubIngress::from_sub(sub, total(sub.total))) + } } impl Drop for IngressId { @@ -170,22 +219,29 @@ impl Drop for IngressId { let proxy = ProxyState::get()?; - proxy.send_ingress(IngressPayload { - id: ingress.id, - message: "Completed".into(), - ingress_type: ingress.ingress_type, - percent: None, - total: ingress.total, - }).await?; + proxy + .send_ingress(IngressPayload { + id: ingress.id, + message: "Completed".into(), + ingress_type: ingress.ingress_type, + percent: None, + total: ingress.total, + }) + .await?; - tracing::trace!("exited at {}% for ingress {:?}", ingress.current / ingress.total * 100.0, ingress_uuid); + tracing::trace!( + "exited at {}% for ingress {:?}", + ingress.current / ingress.total * 100.0, + ingress_uuid + ); Ok(()) - }.await; + } + .await; if let Err(e) = result { tracing::error!("failed to finish ingress: {:?}", e); } }); } -} \ No newline at end of file +} diff --git a/packages/core/src/store/metadata.rs b/packages/core/src/store/metadata.rs index a8a6946b..613d2cd0 100644 --- a/packages/core/src/store/metadata.rs +++ b/packages/core/src/store/metadata.rs @@ -41,6 +41,10 @@ pub enum MetadataError { NotVanillaManifest(GameLoader), #[error("failed to parse metadata")] ParseError(#[from] serde_json::Error), + #[error("no matching loader found")] + NoMatchingLoader, + #[error("no matching version found")] + NoMatchingVersion, } impl Metadata { @@ -50,33 +54,38 @@ impl Metadata { } #[must_use] - pub fn initialize() -> Self { + pub fn new() -> Self { Self::default() } - pub async fn get_vanilla(&mut self, loader: GameLoader) -> LauncherResult<&VanillaManifest> { - if loader.is_mod_loader() { - return Err(MetadataError::NotVanillaManifest(loader).into()); - } - + pub async fn get_vanilla_or_fetch(&mut self) -> LauncherResult<&VanillaManifest> { if !self.initialized() { - self.load().await?; + self.initialize().await?; } - match loader { - GameLoader::Vanilla => self.inner.minecraft.as_ref(), - _ => None, - }.ok_or_else(|| MetadataError::FetchError.into()) + self.get_vanilla() } - pub async fn get_modded(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> { - if !loader.is_mod_loader() { + pub async fn get_modded_or_fetch(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> { + if !loader.is_modded() { return Err(MetadataError::NotModdedManifest(loader).into()); } if !self.initialized() { - self.load().await?; + self.initialize().await?; + } + + self.get_modded(loader) + } + + pub fn get_vanilla(&self) -> LauncherResult<&VanillaManifest> { + self.inner.minecraft.as_ref().ok_or_else(|| MetadataError::FetchError.into()) + } + + pub fn get_modded(&self, loader: GameLoader) -> LauncherResult<&ModdedManifest> { + if !loader.is_modded() { + return Err(MetadataError::NotModdedManifest(loader).into()); } match loader { @@ -88,8 +97,8 @@ impl Metadata { }.ok_or_else(|| MetadataError::FetchError.into()) } - - async fn load(&mut self) -> LauncherResult<()> { + #[tracing::instrument(skip_all)] + pub async fn initialize(&mut self) -> LauncherResult<()> { let path = Dirs::get_caches_dir().await?.join("metadata.json"); let mut save_file = false; @@ -178,10 +187,10 @@ impl Metadata { let ($($var),*) = tokio::join!( $( async move { - if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) { + if let Ok(loader) = GameLoader::from_str(stringify!($var)) { fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok() } else { - tracing::error!("failed to fetch {} manifest", stringify!($var)); + tracing::error!("loader '{}' is not supported", stringify!($var)); None } }, diff --git a/packages/core/src/store/settings.rs b/packages/core/src/store/settings.rs index 8dd9dd4b..356d5970 100644 --- a/packages/core/src/store/settings.rs +++ b/packages/core/src/store/settings.rs @@ -20,7 +20,7 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - global_game_settings: default_global_game_settings(), + global_game_settings: setting_profiles::Model::default_global_profile(), allow_parallel_running_clusters: false, discord_enabled: false, enable_gamemode: false, @@ -55,20 +55,4 @@ impl Settings { Ok(()) } -} - -fn default_global_game_settings() -> setting_profiles::Model { - setting_profiles::Model { - name: "Global".into(), - force_fullscreen: Some(false), - hook_post: None, - hook_pre: None, - hook_wrapper: None, - java_id: None, - launch_args: None, - launch_env: None, - mem_max: Some(3072), - res_h: None, - res_w: None, - } } \ No newline at end of file diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index 1738244d..6e31ac8c 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -40,7 +40,7 @@ impl State { let ingress_processor = IngressProcessor::new(); let settings = Settings::new().await; let db = super::db::create_pool().await?; - let metadata = Metadata::initialize(); + let metadata = Metadata::new(); let rpc = match DiscordRPC::initialize() { Ok(rpc) => Some(rpc), diff --git a/packages/core/src/utils/http.rs b/packages/core/src/utils/http.rs index c646ad6b..0b50126b 100644 --- a/packages/core/src/utils/http.rs +++ b/packages/core/src/utils/http.rs @@ -10,9 +10,9 @@ use serde::Deserialize; use tokio::sync::Semaphore; use tokio_stream::StreamExt; -use crate::api::ingress::send_ingress; +use crate::api::ingress::IngressSendExt; use crate::error::LauncherResult; -use crate::store::ingress::IngressRef; +use crate::store::ingress::{SubIngress, SubIngressExt}; use crate::store::semaphore::SemaphoreStore; use crate::store::Core; @@ -49,7 +49,7 @@ static REQWEST_CLIENT: LazyLock = LazyLock::new(|| { .expect("failed to build reqwest client!") }); -#[tracing::instrument(level = "debug", skip(body, headers, ingress_ref))] +#[tracing::instrument(level = "debug", skip(body, headers, ingress))] #[onelauncher_macro::pin] pub async fn fetch_advanced( method: Method, @@ -57,7 +57,7 @@ pub async fn fetch_advanced( body: Option, hash: Option<(HashAlgorithm, &str)>, headers: Option>, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, ) -> LauncherResult { let semaphore = SemaphoreStore::fetch().await; let permit = semaphore.acquire().await; @@ -95,7 +95,7 @@ pub async fn fetch_advanced( // Drop the fetch permit as we are no longer fetching drop(permit); - let bytes = if let Some(ingress_ref) = ingress_ref { + let bytes = if let Some(ingress) = ingress { let length = res.content_length(); if let Some(total_size) = length { let mut stream = res.bytes_stream(); @@ -106,11 +106,9 @@ pub async fn fetch_advanced( item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?; bytes.append(&mut chunk.to_vec()); - send_ingress( - ingress_ref, - (chunk.len() as f64 / total_size as f64) * ingress_ref.increment_by, - ) - .await?; + ingress.send_ingress( + (chunk.len() as f64 / total_size as f64) * ingress.total, + ).await?; } Ok(Bytes::from(bytes)) @@ -153,9 +151,9 @@ pub async fn fetch_json Deserialize<'de>>( method: Method, url: &str, body: Option, - ingress_ref: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, ) -> LauncherResult { - fetch_json_advanced(method, url, body, None, None, ingress_ref).await + fetch_json_advanced(method, url, body, None, None, ingress).await } pub async fn fetch_json_advanced Deserialize<'de>>( @@ -164,7 +162,7 @@ pub async fn fetch_json_advanced Deserialize<'de>>( body: Option, headers: Option>, hash: Option<(HashAlgorithm, &str)>, - ingress: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, ) -> LauncherResult { let bytes = fetch_advanced(method, url, body, hash, headers, ingress).await?; Ok(serde_json::from_slice(&bytes)?) @@ -177,22 +175,23 @@ pub async fn download_advanced( body: Option, headers: Option>, hash: Option<(HashAlgorithm, &str)>, - ingress: Option<&IngressRef<'_>>, + ingress: Option<&SubIngress<'_>>, ) -> LauncherResult { + const TASKS: f64 = 3.0; + let ingress_step = ingress.ingress_total().map(|total| total / TASKS).unwrap_or_default(); + let bytes = fetch_advanced( method, url, body, hash, headers, - ingress.map(|i| i.with_increment(i.increment_by / 2.0)).as_ref(), + ingress.ingress_sub(|total| total / TASKS).as_ref(), ) .await?; let path = path.as_ref(); - if let Some(ingress_ref) = ingress { - send_ingress(ingress_ref, ingress_ref.increment_by / 4.0).await?; - } + ingress.send_ingress(ingress_step).await?; if let Some(parent) = path.parent() { io::create_dir_all(parent).await?; @@ -200,9 +199,7 @@ pub async fn download_advanced( io::write(path, &bytes).await?; - if let Some(ingress_ref) = ingress { - send_ingress(ingress_ref, ingress_ref.increment_by / 4.0).await?; - } + ingress.send_ingress(ingress_step).await?; Ok(bytes) } @@ -212,7 +209,7 @@ pub async fn download( url: &str, path: impl AsRef, hash: Option<(HashAlgorithm, &str)>, - ingress_ref: Option<&IngressRef<'_>> + ingress: Option<&SubIngress<'_>> ) -> LauncherResult { - download_advanced(method, url, path, None, None, hash, ingress_ref).await + download_advanced(method, url, path, None, None, hash, ingress).await } diff --git a/packages/core/src/utils/io.rs b/packages/core/src/utils/io.rs index 513eb60d..6f9b78e9 100644 --- a/packages/core/src/utils/io.rs +++ b/packages/core/src/utils/io.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; use async_compression::tokio::bufread::GzipDecoder; +use async_zip::StoredZipEntry; +use async_zip::base::read::WithoutEntry; use serde::Serialize; use serde::de::DeserializeOwned; use tempfile::TempDir; @@ -234,24 +236,49 @@ pub fn tempfile() -> Result { Ok(tempfile::tempfile()?) } -/// Unzips a zip archive from a byte array -pub async fn unzip_bytes( +/// Reads a zip archive from a byte array +pub async fn read_zip_entries_bytes( data: Vec, - dest_path: impl AsRef -) -> Result<(), IOError> { + mut f: F, +) -> Result<(), IOError> +where + F: AsyncFnMut( + usize, + &StoredZipEntry, + &mut async_zip::base::read::ZipEntryReader< + '_, + futures_lite::io::Cursor<&[u8]>, + WithoutEntry, + >, + ) -> Result<(), IOError>, +{ let reader = async_zip::base::read::mem::ZipFileReader::new(data).await?; let entries = reader.file().entries(); for index in 0..entries.len() { let entry = entries.get(index).expect("expected more zip entries"); - let path = dest_path.as_ref().join(sanitize_path(entry.filename().as_str()?)); - - let is_dir = entry.dir()?; - let mut entry_reader = reader .reader_without_entry(index) .await - .expect("Failed to read ZipEntry"); + .unwrap_or_else(|_| panic!("expected zip entry at index '{index}'")); + + f(index, entry, &mut entry_reader).await?; + } + + Ok(()) +} + +/// Unzips a zip archive from a byte array +pub async fn unzip_bytes( + data: Vec, + dest_path: impl AsRef, +) -> Result<(), IOError> { + read_zip_entries_bytes(data, async |_, entry, entry_reader| { + let path = dest_path + .as_ref() + .join(sanitize_path(entry.filename().as_str()?)); + + let is_dir = entry.dir()?; if is_dir { create_dir_all(path).await?; @@ -262,16 +289,19 @@ pub async fn unzip_bytes( let file = tokio::fs::File::create(&path).await?; let mut writer = tokio::io::BufWriter::new(file); + let mut buf = vec![0; 8192]; loop { - let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?; + let n = futures::AsyncReadExt::read(entry_reader, &mut buf).await?; if n == 0 { break; } writer.write_all(&buf[..n]).await?; } } - } + + Ok(()) + }).await?; Ok(()) } @@ -288,9 +318,7 @@ pub async fn unzip_file( let entries = reader.file().entries(); for index in 0..entries.len() { - let entry = entries - .get(index) - .expect("expected more zip entries"); + let entry = entries.get(index).expect("expected more zip entries"); let path = dest_path.join(sanitize_path(entry.filename().as_str()?)); let is_dir = entry.dir()?; diff --git a/packages/core/src/utils/mod.rs b/packages/core/src/utils/mod.rs index 02250b70..6a784770 100644 --- a/packages/core/src/utils/mod.rs +++ b/packages/core/src/utils/mod.rs @@ -1,4 +1,4 @@ pub mod crypto; -pub mod ext; +pub mod os_ext; pub mod http; pub mod io; \ No newline at end of file diff --git a/packages/core/src/utils/ext.rs b/packages/core/src/utils/os_ext.rs similarity index 100% rename from packages/core/src/utils/ext.rs rename to packages/core/src/utils/os_ext.rs diff --git a/packages/entity/src/domain/loader.rs b/packages/entity/src/domain/loader.rs index 92f83e2e..11922186 100644 --- a/packages/entity/src/domain/loader.rs +++ b/packages/entity/src/domain/loader.rs @@ -20,8 +20,8 @@ pub enum GameLoader { impl GameLoader { #[must_use] - pub const fn is_mod_loader(&self) -> bool { - matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric) + pub const fn is_modded(&self) -> bool { + !matches!(self, Self::Vanilla) } pub const fn get_format_version(&self) -> usize { @@ -50,7 +50,7 @@ impl TryFrom for GameLoader { fn try_from(s: String) -> Result { Ok(match s.to_lowercase().replace(['_', '.', ' '], "").as_str() { - "vanilla" => Self::Vanilla, + "vanilla" | "minecraft" => Self::Vanilla, "forge" => Self::Forge, "neoforge" | "neo" => Self::NeoForge, "quilt" => Self::Quilt, diff --git a/packages/entity/src/entities/java_versions.rs b/packages/entity/src/entities/java_versions.rs index 0e114730..e9856b52 100644 --- a/packages/entity/src/entities/java_versions.rs +++ b/packages/entity/src/entities/java_versions.rs @@ -16,6 +16,8 @@ pub struct Model { pub absolute_path: String, #[sea_orm(column_type = "Text")] pub full_version: String, + #[sea_orm(column_type = "Text")] + pub arch: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/entity/src/entities/setting_profiles.rs b/packages/entity/src/entities/setting_profiles.rs index 6479b1e7..c68b9e5b 100644 --- a/packages/entity/src/entities/setting_profiles.rs +++ b/packages/entity/src/entities/setting_profiles.rs @@ -53,3 +53,33 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +const GLOBAL_NAME: &str = "Global"; + +impl ActiveModel { + pub fn is_global(&self) -> bool { + self.name.try_as_ref().map(|name| name == GLOBAL_NAME).unwrap_or(false) + } +} + +impl Model { + pub fn is_global(&self) -> bool { + self.name == GLOBAL_NAME + } + + pub fn default_global_profile() -> Self { + Self { + name: GLOBAL_NAME.into(), + force_fullscreen: Some(false), + hook_post: None, + hook_pre: None, + hook_wrapper: None, + java_id: None, + launch_args: None, + launch_env: None, + mem_max: Some(3072), + res_h: None, + res_w: None, + } + } +} diff --git a/packages/migration/src/init.sql b/packages/migration/src/init.sql index 56c7eba7..130e8ef9 100644 --- a/packages/migration/src/init.sql +++ b/packages/migration/src/init.sql @@ -10,6 +10,7 @@ create table java_versions ( vendor_name text not null, absolute_path text not null, full_version text not null, + arch text not null, constraint java_versions_pk primary key (id autoincrement) );