feat(core): Launcher migrations now take in effect cluster migrations
This commit is contained in:
parent
2718105944
commit
5b24a81fe5
9 changed files with 167 additions and 9 deletions
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ mod manifest;
|
|||
pub use manager::VersionsManager;
|
||||
pub use manifest::{
|
||||
MigrationSource, MigrationTarget, RemoteMigration, VersionMetadata, VersionsManifest,
|
||||
resolve_migration_chain,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue