feat(core): Launcher migrations now take in effect cluster migrations

This commit is contained in:
LynithDev 2026-07-17 18:23:43 +02:00
parent 2718105944
commit 5b24a81fe5
9 changed files with 167 additions and 9 deletions

View file

@ -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<Self::Ok, Self::Err> {
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))
}
}

View file

@ -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()

View file

@ -32,11 +32,9 @@ pub(crate) fn matching_new_cluster_id(
instance: &SourceInstance,
new: &[ClusterBundles],
) -> Option<i64> {
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)
}

View file

@ -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,
};

View file

@ -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<String>,
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<SourceInstance>,
}
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() {

View file

@ -98,6 +98,7 @@ async fn detect_inner(root: &Path, db_path: &Path) -> LauncherResult<MigrationDe
instance_id: cluster_id,
folder_name,
mc_version,
target_mc_version: None,
mc_loader,
categories,
has_game_dir,

View file

@ -92,6 +92,7 @@ pub async fn detect() -> LauncherResult<Option<MigrationDetection>> {
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,

View file

@ -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(

View file

@ -4,4 +4,5 @@ mod manifest;
pub use manager::VersionsManager;
pub use manifest::{
MigrationSource, MigrationTarget, RemoteMigration, VersionMetadata, VersionsManifest,
resolve_migration_chain,
};