diff --git a/packages/oneclient_app/src/hooks/queries/migration.rs b/packages/oneclient_app/src/hooks/queries/migration.rs index 2ee691de..6d436cae 100644 --- a/packages/oneclient_app/src/hooks/queries/migration.rs +++ b/packages/oneclient_app/src/hooks/queries/migration.rs @@ -1,5 +1,7 @@ use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query}; -use oneclient_core::{LauncherError, MigrationDetection, detect_migration}; +use oneclient_core::{ + LauncherError, LauncherState, MigrationDetection, detect_migration, resolve_migration_chain, +}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct MigrationQuery; @@ -13,7 +15,28 @@ impl QueryCapability for MigrationQuery { type Keys = MigrationKeys; async fn run(&self, _keys: &Self::Keys) -> Result { - detect_migration().await + let Some(mut detection) = detect_migration().await? else { + return Ok(None); + }; + + // remote cluster migrations run on startup (so before we even get to launcher migrations) + // this pretty much tries to migrate the source instance to the latest version in the migration chain, + // so that we can import it into the correct cluster + let rules = LauncherState::get()?.versions.migrations().await; + if !rules.is_empty() { + for instance in &mut detection.instances { + if instance.mc_version.is_empty() { + continue; + } + let resolved = + resolve_migration_chain(&instance.mc_version, instance.mc_loader, &rules); + if resolved != instance.mc_version { + instance.target_mc_version = Some(resolved); + } + } + } + + Ok(Some(detection)) } } diff --git a/packages/oneclient_app/src/view/app/settings/developer.rs b/packages/oneclient_app/src/view/app/settings/developer.rs index c4d2c8d7..5cb498ff 100644 --- a/packages/oneclient_app/src/view/app/settings/developer.rs +++ b/packages/oneclient_app/src/view/app/settings/developer.rs @@ -3,7 +3,7 @@ use freya::router::RouterContext; use super::settings_page; use crate::Route; -use crate::components::{Button, IconType, link_button, toggle}; +use crate::components::{IconType, link_button, toggle}; use crate::hooks::{use_browser_compat, use_dispatch, use_settings_snapshot}; use crate::view::app::settings::{section_header, settings_row}; @@ -32,7 +32,6 @@ impl Component for SettingsDeveloper { dispatch.set_settings(next); }); - let show_dev = use_state(|| false); let browser_compat = use_browser_compat(); settings_page() diff --git a/packages/oneclient_app/src/view/onboarding/migration.rs b/packages/oneclient_app/src/view/onboarding/migration.rs index 16d8cbd1..0a957600 100644 --- a/packages/oneclient_app/src/view/onboarding/migration.rs +++ b/packages/oneclient_app/src/view/onboarding/migration.rs @@ -32,11 +32,9 @@ pub(crate) fn matching_new_cluster_id( instance: &SourceInstance, new: &[ClusterBundles], ) -> Option { + let want = instance.import_version(); new.iter() - .find(|cb| { - cb.cluster.mc_version == instance.mc_version - && cb.cluster.mc_loader == instance.mc_loader - }) + .find(|cb| cb.cluster.mc_version == want && cb.cluster.mc_loader == instance.mc_loader) .map(|cb| cb.cluster.id) } diff --git a/packages/oneclient_core/src/lib.rs b/packages/oneclient_core/src/lib.rs index 1f16a66e..95b599a9 100644 --- a/packages/oneclient_core/src/lib.rs +++ b/packages/oneclient_core/src/lib.rs @@ -76,4 +76,6 @@ pub use settings::ProfileUpdate; pub use state::LauncherServices; pub use state::LauncherState; pub use version::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version}; -pub use versions::{RemoteMigration, VersionMetadata, VersionsManager, VersionsManifest}; +pub use versions::{ + RemoteMigration, VersionMetadata, VersionsManager, VersionsManifest, resolve_migration_chain, +}; diff --git a/packages/oneclient_core/src/migration/mod.rs b/packages/oneclient_core/src/migration/mod.rs index 3667254d..dcf6aa88 100644 --- a/packages/oneclient_core/src/migration/mod.rs +++ b/packages/oneclient_core/src/migration/mod.rs @@ -46,6 +46,8 @@ pub struct SourceInstance { pub instance_id: i64, pub folder_name: String, pub mc_version: String, + /// Used for any "migrated" clusters (e.g. 26.1 fabric -> 26.1.2 fabric) + pub target_mc_version: Option, pub mc_loader: GameLoader, /// Bundle categories the user had installed on this instance, e.g. /// `["HUD", "Performance"]`. Empty when the source has no category concept. @@ -60,6 +62,13 @@ pub struct MigrationDetection { pub instances: Vec, } +impl SourceInstance { + #[must_use] + pub fn import_version(&self) -> &str { + self.target_mc_version.as_deref().unwrap_or(&self.mc_version) + } +} + /// Where an imported game directory should land in *this* launcher. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ImportTarget { @@ -108,6 +117,23 @@ mod tests { assert_eq!(MigrationSource::ALL.last(), Some(&MigrationSource::Vanilla)); } + #[test] + fn import_version_prefers_resolved_target() { + let mut instance = SourceInstance { + instance_id: 1, + folder_name: "26.1 fabric".to_string(), + mc_version: "26.1".to_string(), + target_mc_version: None, + mc_loader: GameLoader::Fabric, + categories: Vec::new(), + has_game_dir: true, + }; + assert_eq!(instance.import_version(), "26.1"); + + instance.target_mc_version = Some("26.1.2".to_string()); + assert_eq!(instance.import_version(), "26.1.2"); + } + #[test] fn ids_round_trip() { for source in MigrationSource::ALL.iter().copied() { diff --git a/packages/oneclient_core/src/migration/oneclient_v1.rs b/packages/oneclient_core/src/migration/oneclient_v1.rs index 2fd56470..846eaced 100644 --- a/packages/oneclient_core/src/migration/oneclient_v1.rs +++ b/packages/oneclient_core/src/migration/oneclient_v1.rs @@ -98,6 +98,7 @@ async fn detect_inner(root: &Path, db_path: &Path) -> LauncherResult LauncherResult> { instance_id: 0, folder_name: ROOT_INSTANCE_NAME.to_string(), mc_version: String::new(), + target_mc_version: None, mc_loader: GameLoader::Vanilla, categories: Vec::new(), has_game_dir: true, diff --git a/packages/oneclient_core/src/plus.rs b/packages/oneclient_core/src/plus.rs index 7e3b4427..6b00d961 100644 --- a/packages/oneclient_core/src/plus.rs +++ b/packages/oneclient_core/src/plus.rs @@ -89,6 +89,8 @@ fn build_client() -> Result { reqwest::Client::builder() .tcp_keepalive(Some(Duration::from_secs(15))) .connect_timeout(Duration::from_secs(10)) + // reqwest-websocket only supports Http <1.1 + .http1_only() .tls_backend_rustls() .user_agent(format!( "OneClient {} ({})", diff --git a/packages/oneclient_core/src/versions/manifest.rs b/packages/oneclient_core/src/versions/manifest.rs index f779cb34..0df78144 100644 --- a/packages/oneclient_core/src/versions/manifest.rs +++ b/packages/oneclient_core/src/versions/manifest.rs @@ -1,6 +1,9 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use crate::api_config::meta_url_base; +use crate::packages::domain::GameLoader; use crate::version::{VersionKey, format_mc_version}; #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] @@ -66,6 +69,41 @@ pub struct MigrationTarget { pub mc_version: String, } +/// Follow the remote migration chain forward from `mc_version` for the given +/// loader, returning the final target version after every applicable rule has +/// been applied. Returns the input unchanged when no rule matches. +/// +/// This mirrors what [`crate::clusters::apply_remote_migrations`] does to +/// existing cluster rows, so callers that only know an *old* version (e.g. the +/// OneClient v1 importer) can find the cluster the migrations have already moved +/// that version to. Multi-hop chains (`a -> b -> c`) are followed; the loop is +/// bounded by the rule count so a cyclic/self rule can't spin forever. +#[must_use] +pub fn resolve_migration_chain( + mc_version: &str, + loader: GameLoader, + rules: &[RemoteMigration], +) -> String { + let mut current = mc_version.to_string(); + + for _ in 0..=rules.len() { + let Some(rule) = rules.iter().find(|rule| { + rule.from.mc_version == current + && GameLoader::from_str(&rule.from.loader).is_ok_and(|l| l == loader) + }) else { + break; + }; + + if rule.to.mc_version == current { + break; + } + + current = rule.to.mc_version.clone(); + } + + current +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct VersionMetadata { pub major_version: u32, @@ -236,6 +274,75 @@ mod tests { assert_eq!(metadata[1].mc_version().as_deref(), Some("26.1.2")); } + fn rule(id: &str, from: &str, loader: &str, to: &str) -> RemoteMigration { + RemoteMigration { + id: id.to_string(), + from: MigrationSource { + mc_version: from.to_string(), + loader: loader.to_string(), + }, + to: MigrationTarget { + mc_version: to.to_string(), + }, + } + } + + #[test] + fn chain_resolves_single_hop() { + let rules = [rule("a", "26.1", "fabric", "26.1.2")]; + assert_eq!( + resolve_migration_chain("26.1", GameLoader::Fabric, &rules), + "26.1.2" + ); + } + + #[test] + fn chain_follows_multiple_hops() { + let rules = [ + rule("a", "26.1", "fabric", "26.1.2"), + rule("b", "26.1.2", "fabric", "26.1.3"), + ]; + assert_eq!( + resolve_migration_chain("26.1", GameLoader::Fabric, &rules), + "26.1.3" + ); + } + + #[test] + fn chain_requires_matching_loader() { + let rules = [rule("a", "26.1", "forge", "26.1.2")]; + assert_eq!( + resolve_migration_chain("26.1", GameLoader::Fabric, &rules), + "26.1" + ); + } + + #[test] + fn chain_returns_input_when_no_rule_matches() { + let rules = [rule("a", "21.1", "fabric", "21.1.2")]; + assert_eq!( + resolve_migration_chain("26.1", GameLoader::Fabric, &rules), + "26.1" + ); + } + + #[test] + fn chain_terminates_on_self_and_cyclic_rules() { + let self_rule = [rule("a", "26.1", "fabric", "26.1")]; + assert_eq!( + resolve_migration_chain("26.1", GameLoader::Fabric, &self_rule), + "26.1" + ); + + let cycle = [ + rule("a", "26.1", "fabric", "26.2"), + rule("b", "26.2", "fabric", "26.1"), + ]; + // Must not loop forever; lands on one of the two, bounded by rule count. + let out = resolve_migration_chain("26.1", GameLoader::Fabric, &cycle); + assert!(out == "26.1" || out == "26.2"); + } + #[test] fn migrations_parse() { let manifest: VersionsManifest = serde_json::from_str( diff --git a/packages/oneclient_core/src/versions/mod.rs b/packages/oneclient_core/src/versions/mod.rs index 49b7cc27..e9ace6ba 100644 --- a/packages/oneclient_core/src/versions/mod.rs +++ b/packages/oneclient_core/src/versions/mod.rs @@ -4,4 +4,5 @@ mod manifest; pub use manager::VersionsManager; pub use manifest::{ MigrationSource, MigrationTarget, RemoteMigration, VersionMetadata, VersionsManifest, + resolve_migration_chain, };