From 44dfa3223346c38297aff02629468f21aa861377 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Sat, 12 Sep 2026 18:11:21 -0400 Subject: [PATCH] Add Ornithe support (#811) * Add Ornithe support * chore: fix the remaining startup issues reltaed to arguments and manifest caching * fix: stop repairing on every launch for natives-only libraries --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com> --- Cargo.lock | 4 +- Cargo.toml | 2 +- packages/oneclient_app/src/view/app/debug.rs | 90 +++++++++- packages/oneclient_cluster/src/error.rs | 3 - packages/oneclient_common/src/domain.rs | 37 +++- .../oneclient_core/src/clusters/prepare.rs | 8 +- packages/oneclient_core/src/game/error.rs | 3 - packages/oneclient_core/src/game/launch.rs | 4 +- packages/oneclient_core/src/verify.rs | 5 +- packages/oneclient_mc/src/arguments.rs | 169 ++++++++++++++---- packages/oneclient_mc/src/install.rs | 104 +++++++++-- packages/oneclient_mc/src/manifest.rs | 37 ++-- 12 files changed, 382 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50ff0fce..7d1c1d28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3231,9 +3231,9 @@ dependencies = [ [[package]] name = "interfrost" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3449eb9b8a587b832db2eb3a45776928ce0927c0efa7d0be9a7f4c619140a12a" +checksum = "76a14453cd2d9454beb9b410dc7b845b2d62ff67a6f42460bce6b9bc6e34d3b9" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 129d8d21..f87daa21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ polyio = {path = "./packages/polyio"} # core (extra) discord-rich-presence = {version = "=1.1.0"} -interfrost = {version = "=1.3.2"} +interfrost = {version = "=1.4.0"} # logging sentry = {version = "=0.48.5", default-features = false, features = [ diff --git a/packages/oneclient_app/src/view/app/debug.rs b/packages/oneclient_app/src/view/app/debug.rs index 552b462a..0c1c13e4 100644 --- a/packages/oneclient_app/src/view/app/debug.rs +++ b/packages/oneclient_app/src/view/app/debug.rs @@ -9,7 +9,7 @@ use oneclient_net::status::{self, ServiceStatus}; use crate::Actions; use crate::components::{Button, Dropdown, Icon, IconType, TextInput, login_dialog, toggle}; -use crate::hooks::use_dispatch; +use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters, use_dispatch}; use crate::notifications::{ ClusterUpdateItem, ClusterUpdateSummary, NotificationAction, NotificationActionKind, OptionalModsGroup, @@ -97,6 +97,8 @@ impl Component for Debug { vec![CorruptionSimulator.into_element()], )) .child(divider()) + .child(section("Clusters", vec![ClusterList.into_element()])) + .child(divider()) .child(section("SQL Console", vec![SqlConsole.into_element()])) .child(divider()) .child(section( @@ -112,6 +114,92 @@ impl Component for Debug { } } +#[derive(PartialEq)] +struct ClusterList; + +impl Component for ClusterList { + fn render(&self) -> impl IntoElement { + let dispatch = use_dispatch(); + let active_id = use_active_cluster_id(); + let clusters = settled_or_loading(&use_clusters()).unwrap_or_default(); + + let mut list = rect().vertical().width(Size::fill()).spacing(8.); + + if clusters.is_empty() { + return list.child( + label() + .text("No clusters in the database.") + .font_size(13.) + .color(colors::fg_secondary()), + ); + } + + for cluster in clusters { + let cluster_id = cluster.id; + let launch = dispatch.clone(); + let mut active_id = active_id; + let loader_version = cluster + .mc_loader_version + .as_deref() + .map_or_else(String::new, |version| format!(" {version}")); + + list = list.child( + rect() + .horizontal() + .width(Size::fill()) + .cross_align(Alignment::Center) + .main_align(Alignment::SpaceBetween) + .spacing(12.) + .padding(Gaps::new_symmetric(10., 14.)) + .corner_radius(CornerRadius::new_all(10.)) + .background(colors::page_elevated()) + .child( + rect() + .vertical() + .child( + label() + .text(cluster.name.clone()) + .font_size(14.) + .color(colors::fg_primary()), + ) + .child( + label() + .text(format!( + "#{cluster_id} · {} · {}{loader_version} · {:?}", + cluster.mc_version, cluster.mc_loader, cluster.stage + )) + .font_size(12.) + .color(colors::fg_secondary()), + ), + ) + .child( + rect() + .horizontal() + .spacing(8.) + .child( + Button::new() + .primary() + .on_press(move |_| launch.launch_cluster(cluster_id)) + .text("Launch"), + ) + .child( + Button::new() + .secondary() + .on_press(move |_| { + *active_id.write() = Some(cluster_id); + let _ = RouterContext::get() + .push(Route::ClusterOverview { cluster_id }); + }) + .text("View"), + ), + ), + ); + } + + list + } +} + #[derive(PartialEq)] struct ToastController; diff --git a/packages/oneclient_cluster/src/error.rs b/packages/oneclient_cluster/src/error.rs index a4bb87de..7e00c5f0 100644 --- a/packages/oneclient_cluster/src/error.rs +++ b/packages/oneclient_cluster/src/error.rs @@ -46,9 +46,6 @@ pub enum ClusterError { #[error("cluster is in {0} stage")] Busy(ClusterStage), - #[error("invalid Minecraft version '{0}'")] - InvalidVersion(String), - #[error("cluster is missing a required Java version")] MissingJavaVersion, diff --git a/packages/oneclient_common/src/domain.rs b/packages/oneclient_common/src/domain.rs index bf25db06..1c1e3351 100644 --- a/packages/oneclient_common/src/domain.rs +++ b/packages/oneclient_common/src/domain.rs @@ -197,7 +197,7 @@ pub enum GameLoader { NeoForge = 2, Quilt = 3, Fabric = 4, - LegacyFabric = 5, + Ornithe = 5, } impl GameLoader { @@ -212,7 +212,7 @@ impl GameLoader { Self::NeoForge => interfrost::api::modded::CURRENT_NEOFORGE_FORMAT_VERSION, Self::Quilt => interfrost::api::modded::CURRENT_QUILT_FORMAT_VERSION, Self::Fabric => interfrost::api::modded::CURRENT_FABRIC_FORMAT_VERSION, - Self::LegacyFabric => interfrost::api::modded::CURRENT_LEGACY_FABRIC_FORMAT_VERSION, + Self::Ornithe => interfrost::api::modded::CURRENT_ORNITHE_FORMAT_VERSION, } } @@ -220,12 +220,19 @@ impl GameLoader { match self { Self::Vanilla => String::from("minecraft"), Self::NeoForge => String::from("neo"), + Self::Ornithe => String::from("ornithe"), _ => self.to_string().to_lowercase().replace(['_', '.', ' '], ""), } } pub const fn modded_loaders() -> &'static [Self] { - &[Self::Forge, Self::NeoForge, Self::Quilt, Self::Fabric] + &[ + Self::Forge, + Self::NeoForge, + Self::Quilt, + Self::Fabric, + Self::Ornithe, + ] } pub fn compatible_with(self, other: Self) -> bool { @@ -235,7 +242,7 @@ impl GameLoader { Self::NeoForge => other == Self::NeoForge, Self::Quilt => matches!(other, Self::Quilt | Self::Fabric), Self::Fabric => other == Self::Fabric, - Self::LegacyFabric => other == Self::LegacyFabric, + Self::Ornithe => other == Self::Ornithe, } } @@ -246,7 +253,7 @@ impl GameLoader { Self::NeoForge => "neoforge", Self::Quilt => "quilt", Self::Fabric => "fabric", - Self::LegacyFabric => "legacy-fabric", + Self::Ornithe => "ornithe", } } } @@ -262,7 +269,7 @@ impl FromStr for GameLoader { "neoforge" | "neo" => Self::NeoForge, "quilt" | "quiltloader" => Self::Quilt, "fabric" | "fabricloader" => Self::Fabric, - "legacyfabric" => Self::LegacyFabric, + "ornithe" => Self::Ornithe, _ => return Err(format!("'{s}' is not a valid game loader")), }, ) @@ -277,7 +284,7 @@ impl Display for GameLoader { Self::NeoForge => "NeoForge", Self::Quilt => "Quilt", Self::Fabric => "Fabric", - Self::LegacyFabric => "Legacy Fabric", + Self::Ornithe => "Fabric (Ornithe)", }) } } @@ -302,3 +309,19 @@ impl Resolution { Self { width, height } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_display_name_is_not_a_metadata_path() { + assert_eq!(GameLoader::Ornithe.to_string(), "Fabric (Ornithe)"); + assert_eq!(GameLoader::Ornithe.get_format_name(), "ornithe"); + assert_eq!(GameLoader::Vanilla.get_format_name(), "minecraft"); + assert_eq!(GameLoader::NeoForge.get_format_name(), "neo"); + assert_eq!(GameLoader::Fabric.get_format_name(), "fabric"); + assert_eq!(GameLoader::Quilt.get_format_name(), "quilt"); + assert_eq!(GameLoader::Forge.get_format_name(), "forge"); + } +} diff --git a/packages/oneclient_core/src/clusters/prepare.rs b/packages/oneclient_core/src/clusters/prepare.rs index 5f4e73f0..a6b40649 100644 --- a/packages/oneclient_core/src/clusters/prepare.rs +++ b/packages/oneclient_core/src/clusters/prepare.rs @@ -163,9 +163,7 @@ pub async fn estimate_cluster_download( let info = { let mut metadata = state.metadata.lock().await; let (version, _index, _updated) = - resolve_minecraft_version(&mut metadata, &state.services.mc(), &mc_version) - .await - .map_err(|_| ClusterError::InvalidVersion(cluster.mc_version.clone()))?; + resolve_minecraft_version(&mut metadata, &state.services.mc(), &mc_version).await?; let loader_version = get_loader_version( &mut metadata, &state.services.mc(), @@ -211,9 +209,7 @@ async fn install_cluster( let mc_version = oneclient_common::version::normalize_mc_version_input(&cluster.mc_version); let (version, _version_index, minecraft_updated) = - resolve_minecraft_version(metadata, &state.services.mc(), &mc_version) - .await - .map_err(|_| ClusterError::InvalidVersion(cluster.mc_version.clone()))?; + resolve_minecraft_version(metadata, &state.services.mc(), &mc_version).await?; let loader_version = get_loader_version( metadata, diff --git a/packages/oneclient_core/src/game/error.rs b/packages/oneclient_core/src/game/error.rs index 2a2234ad..95d5d3f5 100644 --- a/packages/oneclient_core/src/game/error.rs +++ b/packages/oneclient_core/src/game/error.rs @@ -24,9 +24,6 @@ pub enum GameError { #[error("processor main class not found for {0}")] ProcessorMainClass(String), - #[error("invalid game version {0}")] - InvalidVersion(String), - #[error("cluster {0} is already running")] AlreadyRunning(i64), diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index baa10624..ad8765ce 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -148,9 +148,7 @@ async fn start( let mut metadata = state.metadata.lock().await; let (version, _index, updated) = - resolve_minecraft_version(&mut metadata, &state.services.mc(), &mc_version) - .await - .map_err(|_| GameError::InvalidVersion(cluster.mc_version.clone()))?; + resolve_minecraft_version(&mut metadata, &state.services.mc(), &mc_version).await?; let loader_version = get_loader_version( &mut metadata, diff --git a/packages/oneclient_core/src/verify.rs b/packages/oneclient_core/src/verify.rs index 69ffe303..97774788 100644 --- a/packages/oneclient_core/src/verify.rs +++ b/packages/oneclient_core/src/verify.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use oneclient_cluster::ClusterError; use oneclient_content::packages::PackageStore; use oneclient_content::packages::store::artifact_absolute_path; use oneclient_db::dao::artifact as artifact_dao; @@ -109,9 +108,7 @@ async fn run_verify( let (version_info, minecraft_updated) = { let mut metadata = state.metadata.lock().await; let (version, _index, minecraft_updated) = - resolve_minecraft_version(&mut metadata, &state.services.mc(), &mc_version) - .await - .map_err(|_| ClusterError::InvalidVersion(cluster.mc_version.clone()))?; + resolve_minecraft_version(&mut metadata, &state.services.mc(), &mc_version).await?; let loader_version = get_loader_version( &mut metadata, diff --git a/packages/oneclient_mc/src/arguments.rs b/packages/oneclient_mc/src/arguments.rs index d957d70e..4b22f5bb 100644 --- a/packages/oneclient_mc/src/arguments.rs +++ b/packages/oneclient_mc/src/arguments.rs @@ -6,6 +6,7 @@ use interfrost::api::modded::SidedDataEntry; use interfrost::utils::get_path_from_artifact; use oneclient_common::constants::{self, DUMMY_REPLACE_NEWLINE}; +use oneclient_common::paths; use crate::rules::validate_rules; use crate::error::McError; use oneclient_common::Resolution; @@ -41,13 +42,20 @@ pub fn java_arguments( }, java_arch, )?; - } else { + } + + if !parsed + .iter() + .any(|arg| arg.starts_with("-Djava.library.path=")) + { parsed.push(format!( "-Djava.library.path={}", polyio::canonicalize(natives_path) .map_err(|_| McError::LibraryPath(natives_path.display().to_string()))? .display() )); + } + if !parsed.iter().any(|arg| arg == "-cp" || arg == "-classpath") { parsed.push("-cp".to_string()); parsed.push(classpaths.to_string()); } @@ -174,8 +182,28 @@ pub fn minecraft_arguments( resolution: Resolution, java_arch: &str, ) -> McResult> { + let mut parsed = Vec::new(); + + // a legacy version states its arguments as a string and a loader merged onto + // one adds a list on top the game needs both, in that order + if let Some(legacy_args) = legacy_args { + for arg in legacy_args.split(' ') { + parsed.push(parse_minecraft_argument( + &arg.replace(' ', DUMMY_REPLACE_NEWLINE), + access_token, + username, + uuid, + version, + asset_index, + game_directory, + assets_directory, + version_type, + resolution, + )?); + } + } + if let Some(args) = args { - let mut parsed = Vec::new(); parse_arguments( version_updated, args, @@ -196,29 +224,9 @@ pub fn minecraft_arguments( }, 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), - access_token, - username, - uuid, - version, - asset_index, - game_directory, - assets_directory, - version_type, - resolution, - )?); - } - - Ok(parsed) - } else { - Ok(Vec::new()) } + + Ok(parsed) } pub fn append_profile_game_arguments( @@ -343,15 +351,21 @@ pub fn parse_minecraft_argument( "${assets_root}", &polyio::canonicalize(assets_directory)?.display().to_string(), ) - .replace( - "${game_assets}", - &polyio::canonicalize(assets_directory)?.display().to_string(), - ) + .replace("${game_assets}", &legacy_assets_path()?) .replace("${version_type}", version_type.as_str()) .replace("${resolution_width}", &resolution.width.to_string()) .replace("${resolution_height}", &resolution.height.to_string())) } +fn legacy_assets_path() -> McResult { + let dir = paths::legacy_assets_dir()?; + + Ok(polyio::canonicalize(&dir) + .unwrap_or(dir) + .display() + .to_string()) +} + fn parse_java_argument( mut argument: String, natives_path: &Path, @@ -393,6 +407,10 @@ pub fn classpaths( if !lib.include_in_classpath { continue; } + // natives-only libraries (classifiers, no artifact) have no jar on disk + if !crate::install::has_main_artifact(lib) { + continue; + } let (artifact, version) = split_artifact_version(&lib.name); let ver_key = version_key(version); @@ -516,7 +534,11 @@ where #[cfg(test)] mod tests { - use super::{ZGC_MIN_HEAP_MB, is_collector_flag, performance_flags, split_custom_args}; + use super::{ + Library, ZGC_MIN_HEAP_MB, classpaths, is_collector_flag, java_arguments, + minecraft_arguments, performance_flags, split_custom_args, + }; + use oneclient_common::Resolution; #[test] fn blank_input_contributes_nothing() { @@ -786,5 +808,92 @@ mod tests { fn a_java_7_runtime_is_left_untouched() { assert_eq!(flags(7, "amd64", 4096), vec!["-Xms512M"]); } -} + #[test] + fn a_natives_only_library_is_not_a_classpath_entry() { + let libraries: Vec = serde_json::from_str( + r#"[ + { "name": "net.fabricmc:fabric-loader:0.19.5" }, + { "name": "org.lwjgl.lwjgl:lwjgl:2.9.4+legacyfabric.15", + "downloads": { "artifact": { "sha1": "a", "size": 1, "url": "https://maven.legacyfabric.net/lwjgl.jar" } } }, + { "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4+legacyfabric.15", + "downloads": { "classifiers": { "natives-osx": { "sha1": "b", "size": 2, "url": "https://maven.legacyfabric.net/natives-osx.jar" } } }, + "natives": { "osx": "natives-osx" } } + ]"#, + ) + .unwrap(); + + let dir = std::env::temp_dir().join("oneclient-classpath-test"); + let client = dir.join("client.jar"); + for lib in ["net/fabricmc/fabric-loader/0.19.5/fabric-loader-0.19.5.jar", + "org/lwjgl/lwjgl/lwjgl/2.9.4+legacyfabric.15/lwjgl-2.9.4+legacyfabric.15.jar"] { + let path = dir.join(lib); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, b"").unwrap(); + } + std::fs::write(&client, b"").unwrap(); + + let cp = classpaths(&dir, &libraries, &client, "x86", false) + .expect("a jar that was never downloaded must not fail the launch"); + assert!(!cp.contains("lwjgl-platform")); + assert!(cp.contains("fabric-loader") && cp.contains("lwjgl-2.9.4")); + } + + #[test] + fn a_loader_on_a_legacy_version_still_gets_a_classpath() { + // ornithe's manifest declares two -D flags and nothing else + // 1.8.9 has no `arguments` block to supply -cp or the natives path + let loader_jvm: Vec = serde_json::from_str( + r#"["-Dfabric.fixPackageAccess=true", "-Dfabric.gameVersion=1.8.9"]"#, + ) + .unwrap(); + let natives = std::env::temp_dir(); + + let args = java_arguments( + false, + Some(&loader_jvm), + &natives, + &natives, + "/libs/fabric-loader.jar", + "1.8.9", + 2048, + String::new(), + "arm64", + 21, + ) + .unwrap(); + + assert!(args.contains(&"-Dfabric.fixPackageAccess=true".to_string())); + let cp = args.iter().position(|a| a == "-cp").expect("no classpath"); + assert_eq!(args[cp + 1], "/libs/fabric-loader.jar"); + assert!(args.iter().any(|a| a.starts_with("-Djava.library.path="))); + } + + #[test] + fn a_loader_game_argument_does_not_replace_the_legacy_string() { + let loader_game: Vec = + serde_json::from_str(r#"["--fabric"]"#).unwrap(); + let dir = std::env::temp_dir(); + + let args = minecraft_arguments( + false, + Some(&loader_game), + Some("--username ${auth_player_name} --gameDir ${game_directory}"), + "token", + "player", + uuid::Uuid::nil(), + "1.8.9", + "1.8", + &dir, + &dir, + interfrost::api::minecraft::VersionType::Release, + Resolution::default(), + "arm64", + ) + .unwrap(); + + assert_eq!(args.first().unwrap(), "--username"); + assert_eq!(args[1], "player"); + assert_eq!(args.last().unwrap(), "--fabric"); + } +} diff --git a/packages/oneclient_mc/src/install.rs b/packages/oneclient_mc/src/install.rs index 77924ce5..d29184f4 100644 --- a/packages/oneclient_mc/src/install.rs +++ b/packages/oneclient_mc/src/install.rs @@ -50,6 +50,11 @@ impl DownloadPlan { } } +#[must_use] +pub fn uses_legacy_assets(assets: &str) -> bool { + matches!(assets, "legacy" | "pre-1.6") +} + fn asset_object_path(dir: &Path, name: &str, hash: &str, legacy: bool) -> PathBuf { if legacy { dir.join(name.replace('/', std::path::MAIN_SEPARATOR_STR)) @@ -77,6 +82,12 @@ fn library_artifact_size(lib: &Library) -> u64 { .map_or(0, |artifact| u64::from(artifact.size)) } +pub(crate) fn has_main_artifact(lib: &Library) -> bool { + lib.downloads + .as_ref() + .is_none_or(|downloads| downloads.artifact.is_some()) +} + fn native_download<'a>(lib: &'a Library, java_arch: &str) -> Option<&'a LibraryDownload> { let (os_key, classifiers) = lib.natives.as_ref().and_then(|natives| { Some(( @@ -155,7 +166,7 @@ pub async fn plan_downloads( minecraft_updated: bool, force: bool, ) -> McResult { - let legacy = version.assets == "legacy"; + let legacy = uses_legacy_assets(&version.assets); let asset_dir = if legacy { paths::legacy_assets_dir()? } else { @@ -208,11 +219,12 @@ pub async fn plan_downloads( continue; }; - let artifact = force - || !matches_expected_size( - &lib_dir.join(&artifact_path), - library_artifact_size(&lib), - ); + let artifact = has_main_artifact(&lib) + && (force + || !matches_expected_size( + &lib_dir.join(&artifact_path), + library_artifact_size(&lib), + )); let natives = force || !natives_extracted(&natives_dir, &lib, &java_arch); if !artifact && !natives { @@ -426,7 +438,7 @@ pub async fn verify_game_files( java_arch: &str, minecraft_updated: bool, ) -> McResult { - let legacy = version.assets == "legacy"; + let legacy = uses_legacy_assets(&version.assets); let asset_dir = if legacy { paths::legacy_assets_dir()? } else { @@ -599,7 +611,7 @@ pub async fn download_minecraft( download_assets( ctx, progress, - version.assets == "legacy", + uses_legacy_assets(&version.assets), assets, ), download_client(ctx, progress, version, force), @@ -782,7 +794,11 @@ pub async fn download_version_info( .map_err(McError::from)?, }; + let legacy_args = info.minecraft_arguments.clone(); info = interfrost::api::modded::merge_partial_version(partial, info); + if info.minecraft_arguments.is_none() { + info.minecraft_arguments = legacy_args; + } for lib in &mut info.libraries { lib.name = lib.name.replace("${interpulse.gameVersion}", &version.id); @@ -1032,6 +1048,9 @@ pub fn natives_missing( ); } + if !has_main_artifact(lib) { + continue; + } let Ok(rel) = interfrost::utils::get_path_from_artifact(&lib.name) else { continue; }; @@ -1060,6 +1079,9 @@ pub fn libraries_missing( if !lib.include_in_classpath { continue; } + if !has_main_artifact(lib) { + continue; + } let Ok(rel) = interfrost::utils::get_path_from_artifact(&lib.name) else { continue; }; @@ -1098,7 +1120,7 @@ pub fn game_files_missing( minecraft_updated: bool, ) -> McResult { let index = paths::assets_index_dir()?.join(format!("{}.json", version_info.asset_index.id)); - let objects = if version_info.assets == "legacy" { + let objects = if uses_legacy_assets(&version_info.assets) { paths::legacy_assets_dir()? } else { paths::assets_object_dir()? @@ -1424,7 +1446,8 @@ pub async fn resolve_minecraft_version( version_index = manifest.versions.iter().position(|it| it.id == mc_version); } - let version_index = version_index.ok_or(McError::NoMatchingVersion)?; + let version_index = + version_index.ok_or_else(|| McError::InvalidVersion(mc_version.to_string()))?; let versions = &manifest.versions; Ok(( @@ -1482,6 +1505,67 @@ mod tests { (index, objects) } + #[test] + fn a_natives_only_library_has_no_main_jar_to_fetch() { + let natives_only = serde_json::from_str::( + r#"{ + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4+legacyfabric.15", + "downloads": { "classifiers": { "natives-osx": { "sha1": "a", "size": 1, "url": "https://maven.legacyfabric.net/natives-osx.jar" } } }, + "natives": { "osx-arm64": "natives-osx" } + }"#, + ) + .unwrap(); + let with_artifact = serde_json::from_str::( + r#"{ + "name": "org.ow2.asm:asm:9.10.1", + "downloads": { "artifact": { "sha1": "b", "size": 2, "url": "https://maven.fabricmc.net/asm.jar" } } + }"#, + ) + .unwrap(); + let coordinates_only = + serde_json::from_str::(r#"{ "name": "net.fabricmc:fabric-loader:0.19.5" }"#) + .unwrap(); + + assert!(!has_main_artifact(&natives_only)); + assert!(has_main_artifact(&with_artifact)); + assert!(has_main_artifact(&coordinates_only)); + } + + #[test] + fn old_versions_keep_their_assets_in_a_flat_tree() { + assert!(uses_legacy_assets("pre-1.6")); + assert!(uses_legacy_assets("legacy")); + assert!(!uses_legacy_assets("1.8")); + assert!(!uses_legacy_assets("1.14")); + } + + #[test] + fn a_manifest_without_a_wildcard_resolves_per_game_version() { + let manifest = serde_json::from_str::( + r#"{"gameVersions": [ + { "id": "b1.7.3", "stable": true, "loaders": [ + { "id": "0.19.5", "url": "https://meta.example/b1.7.3/0.19.5.json", "stable": true }, + { "id": "0.19.4", "url": "https://meta.example/b1.7.3/0.19.4.json", "stable": false } + ]}, + { "id": "1.8.9", "stable": true, "loaders": [ + { "id": "0.19.5", "url": "https://meta.example/1.8.9/0.19.5.json", "stable": true } + ]} + ]}"#, + ) + .unwrap(); + + let (saw, resolved) = resolve_loader_from_manifest(&manifest, "b1.7.3", None); + assert!(saw); + assert_eq!(resolved.unwrap().id, "0.19.5"); + + let (saw, resolved) = resolve_loader_from_manifest(&manifest, "b1.7.3", Some("0.19.4")); + assert!(saw); + assert_eq!(resolved.unwrap().id, "0.19.4"); + + let (saw, resolved) = resolve_loader_from_manifest(&manifest, "1.21", None); + assert!(!saw && resolved.is_none()); + } + #[test] fn a_complete_assets_tree_needs_no_repair() { let dir = scratch("assets-ok"); diff --git a/packages/oneclient_mc/src/manifest.rs b/packages/oneclient_mc/src/manifest.rs index dd3b8f1c..8272a98d 100644 --- a/packages/oneclient_mc/src/manifest.rs +++ b/packages/oneclient_mc/src/manifest.rs @@ -27,7 +27,7 @@ struct MetadataInner { neo: Option, fabric: Option, quilt: Option, - legacyfabric: Option, + ornithe: Option, } impl MetadataStore { @@ -50,6 +50,10 @@ impl MetadataStore { self.initialize(ctx).await?; } + if self.inner.minecraft.is_none() { + self.refetch_errored(ctx).await; + } + self.get_vanilla() } @@ -87,7 +91,7 @@ impl MetadataStore { GameLoader::NeoForge => self.inner.neo.as_ref(), GameLoader::Fabric => self.inner.fabric.as_ref(), GameLoader::Quilt => self.inner.quilt.as_ref(), - GameLoader::LegacyFabric => self.inner.legacyfabric.as_ref(), + GameLoader::Ornithe => self.inner.ornithe.as_ref(), GameLoader::Vanilla => None, } .ok_or_else(|| McError::FetchError) @@ -164,25 +168,28 @@ impl MetadataStore { check_modded!(neo); check_modded!(fabric); check_modded!(quilt); + check_modded!(ornithe); changed } #[tracing::instrument(level = "debug", skip_all)] pub async fn fetch_all(&mut self, ctx: &McCtx) { - let (minecraft, forge, neo, fabric, quilt) = tokio::join!( + let (minecraft, forge, neo, fabric, quilt, ornithe) = tokio::join!( fetch_vanilla_manifest(ctx), fetch_modded_manifest(ctx, GameLoader::Forge), fetch_modded_manifest(ctx, GameLoader::NeoForge), fetch_modded_manifest(ctx, GameLoader::Fabric), fetch_modded_manifest(ctx, GameLoader::Quilt), + fetch_modded_manifest(ctx, GameLoader::Ornithe), ); - self.inner.minecraft = minecraft.ok(); - self.inner.forge = forge.ok(); - self.inner.neo = neo.ok(); - self.inner.fabric = fabric.ok(); - self.inner.quilt = quilt.ok(); + keep_fetched(&mut self.inner.minecraft, minecraft); + keep_fetched(&mut self.inner.forge, forge); + keep_fetched(&mut self.inner.neo, neo); + keep_fetched(&mut self.inner.fabric, fabric); + keep_fetched(&mut self.inner.quilt, quilt); + keep_fetched(&mut self.inner.ornithe, ornithe); } #[tracing::instrument(level = "debug", skip(self, ctx))] @@ -201,12 +208,8 @@ impl MetadataStore { let mut loaders = Vec::new(); for loader in GameLoader::modded_loaders() { - let manifest = match self.get_modded(*loader) { - Ok(manifest) => manifest, - Err(McError::NotModdedManifest(_)) => { - continue - } - Err(e) => return Err(e), + let Ok(manifest) = self.get_modded(*loader) else { + continue; }; let found = manifest.game_versions.iter().any(|entry| { @@ -229,6 +232,12 @@ impl MetadataStore { } } +fn keep_fetched(slot: &mut Option, fetched: McResult) { + if let Ok(data) = fetched { + *slot = Some(data); + } +} + #[tracing::instrument(level = "debug", skip_all)] async fn fetch_vanilla_manifest(ctx: &McCtx) -> McResult { match fetch_manifest::(ctx, GameLoader::Vanilla).await {