Revert "feat: Added migration from Lunar Client"
This reverts commit 08fe999691.
This commit is contained in:
parent
0cf43816bf
commit
1243dbcd75
19 changed files with 170 additions and 1313 deletions
|
|
@ -17,9 +17,7 @@ pub enum BridgeCommand {
|
||||||
RecordSeenVersion {
|
RecordSeenVersion {
|
||||||
version: String,
|
version: String,
|
||||||
},
|
},
|
||||||
SetOnboardingSeen {
|
MarkOnboardingSeen,
|
||||||
seen: bool,
|
|
||||||
},
|
|
||||||
AcceptTos {
|
AcceptTos {
|
||||||
terms_version: u32,
|
terms_version: u32,
|
||||||
privacy_version: u32,
|
privacy_version: u32,
|
||||||
|
|
|
||||||
|
|
@ -315,9 +315,9 @@ impl CoreBridgeRuntime {
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
BridgeCommand::SetOnboardingSeen { seen } => {
|
BridgeCommand::MarkOnboardingSeen => {
|
||||||
mutate_settings(snapshots, |settings| {
|
mutate_settings(snapshots, |settings| {
|
||||||
settings.seen_onboarding = seen;
|
settings.seen_onboarding = true;
|
||||||
})?;
|
})?;
|
||||||
let state = LauncherState::get()?;
|
let state = LauncherState::get()?;
|
||||||
let snapshot = state.settings.read().clone();
|
let snapshot = state.settings.read().clone();
|
||||||
|
|
@ -436,11 +436,6 @@ impl CoreBridgeRuntime {
|
||||||
let state = LauncherState::get()?;
|
let state = LauncherState::get()?;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let notifier = state.services.notifier.clone();
|
let notifier = state.services.notifier.clone();
|
||||||
|
|
||||||
if let Err(err) = oneclient_core::import_migration_settings(source).await {
|
|
||||||
tracing::warn!(error = %err, "failed to import launcher settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
match oneclient_core::import_migration_game_dir(source, &folder_name, target)
|
match oneclient_core::import_migration_game_dir(source, &folder_name, target)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use oneclient_core::MigrationSource;
|
|
||||||
use oneclient_core::packages::ProviderId;
|
use oneclient_core::packages::ProviderId;
|
||||||
use oneclient_db::models::ClusterId;
|
use oneclient_db::models::ClusterId;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct ImportSelection {
|
|
||||||
pub source: MigrationSource,
|
|
||||||
pub folder_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ActiveClusterState(pub State<Option<ClusterId>>);
|
pub struct ActiveClusterState(pub State<Option<ClusterId>>);
|
||||||
|
|
||||||
|
|
@ -81,12 +74,16 @@ pub struct OnboardingSelectionState {
|
||||||
/// keeps re-deriving `selected` from the catalog, so a bundle list that
|
/// keeps re-deriving `selected` from the catalog, so a bundle list that
|
||||||
/// settles late still gets the right defaults.
|
/// settles late still gets the right defaults.
|
||||||
pub user_touched: State<bool>,
|
pub user_touched: State<bool>,
|
||||||
|
/// Bundle categories the user had in their v1 install, once the migration
|
||||||
|
/// step has been answered. `None` means "no migration decision yet".
|
||||||
pub migrated_categories: State<Option<Vec<String>>>,
|
pub migrated_categories: State<Option<Vec<String>>>,
|
||||||
pub language: State<String>,
|
pub language: State<String>,
|
||||||
pub reduce_motion: State<bool>,
|
pub reduce_motion: State<bool>,
|
||||||
pub predownload: State<bool>,
|
pub predownload: State<bool>,
|
||||||
pub setup_started: State<bool>,
|
pub setup_started: State<bool>,
|
||||||
pub import_selection: State<Option<ImportSelection>>,
|
/// v1-migration: source folder to import files from (`None` = don't import).
|
||||||
|
pub import_folder: State<Option<String>>,
|
||||||
|
/// v1-migration: import into the matching cluster's own dir instead of shared.
|
||||||
pub import_dedicated: State<bool>,
|
pub import_dedicated: State<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ impl BridgeDispatch {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_onboarding_seen(&self, seen: bool) {
|
pub fn mark_onboarding_seen(&self) {
|
||||||
self.send(BridgeCommand::SetOnboardingSeen { seen });
|
self.send(BridgeCommand::MarkOnboardingSeen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accept_tos(&self, terms_version: u32, privacy_version: u32) {
|
pub fn accept_tos(&self, terms_version: u32, privacy_version: u32) {
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ mod view_state;
|
||||||
pub use view_state::{PersistedView, use_view_state};
|
pub use view_state::{PersistedView, use_view_state};
|
||||||
|
|
||||||
pub use active_cluster::{
|
pub use active_cluster::{
|
||||||
ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserUiState, ImportSelection,
|
ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserUiState, LinkConfirmState,
|
||||||
LinkConfirmState, OnboardingSelectionState, use_active_cluster_id, use_browser_compat,
|
OnboardingSelectionState, use_active_cluster_id, use_browser_compat, use_browser_state_store,
|
||||||
use_browser_state_store, use_link_confirm, use_onboarding_selection,
|
use_link_confirm, use_onboarding_selection, use_provide_active_cluster,
|
||||||
use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state,
|
use_provide_browser_compat, use_provide_browser_state, use_provide_link_confirm,
|
||||||
use_provide_link_confirm, use_provide_onboarding_selection,
|
use_provide_onboarding_selection,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use dispatch::BridgeDispatch;
|
pub use dispatch::BridgeDispatch;
|
||||||
|
|
@ -25,7 +25,7 @@ pub use queries::{
|
||||||
category_list, changelog_error, changelog_groups, changelog_is_loading, cluster_content_items,
|
category_list, changelog_error, changelog_groups, changelog_is_loading, cluster_content_items,
|
||||||
content_type_for_slug, has_migration_data, invalidate_cluster_queries, invalidate_java_queries,
|
content_type_for_slug, has_migration_data, invalidate_cluster_queries, invalidate_java_queries,
|
||||||
invalidate_logs_queries, invalidate_profile_queries, invalidate_screenshots_queries,
|
invalidate_logs_queries, invalidate_profile_queries, invalidate_screenshots_queries,
|
||||||
java_runtimes, loader_versions, login_code_already_handled, migration_detections,
|
java_runtimes, loader_versions, login_code_already_handled, migration_detection,
|
||||||
mutation_error, mutation_is_pending, onboarding_bundles_items, package_meta_batch,
|
mutation_error, mutation_is_pending, onboarding_bundles_items, package_meta_batch,
|
||||||
pick_version_metadata, project_detail, provider_versions, reset_login_code_dedup, search_items,
|
pick_version_metadata, project_detail, provider_versions, reset_login_code_dedup, search_items,
|
||||||
search_pending, search_total, terms_document, terms_error, terms_is_loading, try_account,
|
search_pending, search_total, terms_document, terms_error, terms_is_loading, try_account,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
|
use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
|
||||||
use oneclient_core::{LauncherError, MigrationDetection, detect_migrations};
|
use oneclient_core::{LauncherError, MigrationDetection, detect_migration};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct MigrationQuery;
|
pub struct MigrationQuery;
|
||||||
|
|
@ -10,34 +8,28 @@ pub struct MigrationQuery;
|
||||||
pub struct MigrationKeys;
|
pub struct MigrationKeys;
|
||||||
|
|
||||||
impl QueryCapability for MigrationQuery {
|
impl QueryCapability for MigrationQuery {
|
||||||
type Ok = Vec<MigrationDetection>;
|
type Ok = Option<MigrationDetection>;
|
||||||
type Err = LauncherError;
|
type Err = LauncherError;
|
||||||
type Keys = MigrationKeys;
|
type Keys = MigrationKeys;
|
||||||
|
|
||||||
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
||||||
detect_migrations().await
|
detect_migration().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_migration() -> UseQuery<MigrationQuery> {
|
pub fn use_migration() -> UseQuery<MigrationQuery> {
|
||||||
use_query(
|
use_query(Query::new(MigrationKeys, MigrationQuery))
|
||||||
Query::new(MigrationKeys, MigrationQuery)
|
|
||||||
.stale_time(Duration::from_secs(60 * 60))
|
|
||||||
.clean_time(Duration::from_secs(6 * 60 * 60)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn migration_detections(query: &UseQuery<MigrationQuery>) -> Vec<MigrationDetection> {
|
pub fn migration_detection(query: &UseQuery<MigrationQuery>) -> Option<MigrationDetection> {
|
||||||
let reader = query.read();
|
let reader = query.read();
|
||||||
match &*reader.state() {
|
match &*reader.state() {
|
||||||
QueryStateData::Settled { res: Ok(found), .. } => found.clone(),
|
QueryStateData::Settled { res: Ok(opt), .. } => opt.clone(),
|
||||||
QueryStateData::Loading { res: Some(Ok(found)) } => found.clone(),
|
QueryStateData::Loading { res: Some(Ok(opt)) } => opt.clone(),
|
||||||
_ => Vec::new(),
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_migration_data(query: &UseQuery<MigrationQuery>) -> bool {
|
pub fn has_migration_data(query: &UseQuery<MigrationQuery>) -> bool {
|
||||||
migration_detections(query)
|
migration_detection(query).is_some_and(|d| !d.instances.is_empty())
|
||||||
.iter()
|
|
||||||
.any(|d| !d.instances.is_empty())
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ pub use logs::{
|
||||||
UseUploadLog, invalidate_logs_queries, try_cluster_logs, try_log_content, use_cluster_logs,
|
UseUploadLog, invalidate_logs_queries, try_cluster_logs, try_log_content, use_cluster_logs,
|
||||||
use_log_action, use_log_content, use_upload_log,
|
use_log_action, use_log_content, use_upload_log,
|
||||||
};
|
};
|
||||||
pub use migration::{MigrationQuery, has_migration_data, migration_detections, use_migration};
|
pub use migration::{MigrationQuery, has_migration_data, migration_detection, use_migration};
|
||||||
pub use mutations::{
|
pub use mutations::{
|
||||||
ClusterAction, invalidate_cluster_queries, invalidate_profile_queries, use_cluster_mutation,
|
ClusterAction, invalidate_cluster_queries, invalidate_profile_queries, use_cluster_mutation,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use freya::router::{Outlet, use_route};
|
||||||
use crate::Route;
|
use crate::Route;
|
||||||
use crate::components::OnboardingNavbar;
|
use crate::components::OnboardingNavbar;
|
||||||
use crate::hooks::{
|
use crate::hooks::{
|
||||||
ImportSelection, OnboardingSelectionState, has_migration_data, onboarding_bundles_items,
|
OnboardingSelectionState, has_migration_data, onboarding_bundles_items, use_migration,
|
||||||
use_migration, use_onboarding_bundles, use_provide_onboarding_selection,
|
use_onboarding_bundles, use_provide_onboarding_selection,
|
||||||
};
|
};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::view::onboarding::{
|
use crate::view::onboarding::{
|
||||||
|
|
@ -26,7 +26,7 @@ impl Component for OnboardingShell {
|
||||||
let reduce_motion = use_state(|| false);
|
let reduce_motion = use_state(|| false);
|
||||||
let predownload = use_state(|| true);
|
let predownload = use_state(|| true);
|
||||||
let setup_started = use_state(|| false);
|
let setup_started = use_state(|| false);
|
||||||
let import_selection = use_state(|| None::<ImportSelection>);
|
let import_folder = use_state(|| None::<String>);
|
||||||
let import_dedicated = use_state(|| false);
|
let import_dedicated = use_state(|| false);
|
||||||
use_provide_onboarding_selection(OnboardingSelectionState {
|
use_provide_onboarding_selection(OnboardingSelectionState {
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -36,7 +36,7 @@ impl Component for OnboardingShell {
|
||||||
reduce_motion,
|
reduce_motion,
|
||||||
predownload,
|
predownload,
|
||||||
setup_started,
|
setup_started,
|
||||||
import_selection,
|
import_folder,
|
||||||
import_dedicated,
|
import_dedicated,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use freya::prelude::*;
|
||||||
use freya::router::RouterContext;
|
use freya::router::RouterContext;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, TextInput, toggle};
|
use crate::components::{Button, Icon, IconType, TextInput, toggle};
|
||||||
use crate::hooks::{BridgeDispatch, use_dispatch};
|
use crate::hooks::use_dispatch;
|
||||||
use crate::notifications::{ClusterUpdateSummary, NotificationAction, NotificationActionKind};
|
use crate::notifications::{ClusterUpdateSummary, NotificationAction, NotificationActionKind};
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
@ -12,7 +12,6 @@ pub struct Debug;
|
||||||
|
|
||||||
impl Component for Debug {
|
impl Component for Debug {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let dispatch = use_dispatch();
|
|
||||||
let log_debug_info = use_state(|| false);
|
let log_debug_info = use_state(|| false);
|
||||||
let show_dev_stuff = use_state(|| false);
|
let show_dev_stuff = use_state(|| false);
|
||||||
let seen_onboarding = use_state(|| true);
|
let seen_onboarding = use_state(|| true);
|
||||||
|
|
@ -66,7 +65,7 @@ impl Component for Debug {
|
||||||
.child(divider())
|
.child(divider())
|
||||||
.child(section(
|
.child(section(
|
||||||
"Other",
|
"Other",
|
||||||
vec![action_row(dispatch, vec![
|
vec![action_row(vec![
|
||||||
("Open Dev Tools", IconType::CodeSnippet02),
|
("Open Dev Tools", IconType::CodeSnippet02),
|
||||||
("Open Onboarding", IconType::Rocket02),
|
("Open Onboarding", IconType::Rocket02),
|
||||||
("Open Launcher Data", IconType::Folder),
|
("Open Launcher Data", IconType::Folder),
|
||||||
|
|
@ -337,7 +336,7 @@ fn toggle_row(title: &'static str, on: State<bool>) -> Element {
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_row(dispatch: BridgeDispatch, buttons: Vec<(&'static str, IconType)>) -> Element {
|
fn action_row(buttons: Vec<(&'static str, IconType)>) -> Element {
|
||||||
let mut row = rect().horizontal().width(Size::fill()).spacing(12.);
|
let mut row = rect().horizontal().width(Size::fill()).spacing(12.);
|
||||||
|
|
||||||
for (text, icon) in buttons {
|
for (text, icon) in buttons {
|
||||||
|
|
@ -347,9 +346,7 @@ fn action_row(dispatch: BridgeDispatch, buttons: Vec<(&'static str, IconType)>)
|
||||||
.text(text);
|
.text(text);
|
||||||
|
|
||||||
if text == "Open Onboarding" {
|
if text == "Open Onboarding" {
|
||||||
let dispatch = dispatch.clone();
|
button = button.on_press(|_| {
|
||||||
button = button.on_press(move |_| {
|
|
||||||
dispatch.set_onboarding_seen(false);
|
|
||||||
let _ = RouterContext::get().replace(Route::OnboardingWelcome {});
|
let _ = RouterContext::get().replace(Route::OnboardingWelcome {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ use tokio::sync::mpsc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::hooks::{
|
use crate::hooks::{
|
||||||
BridgeDispatch, ClusterBundles, invalidate_cluster_queries, migration_detections,
|
BridgeDispatch, ClusterBundles, invalidate_cluster_queries, migration_detection,
|
||||||
onboarding_bundles_items, try_default_account, use_current_account, use_dispatch,
|
onboarding_bundles_items, try_default_account, use_current_account, use_dispatch,
|
||||||
use_migration, use_onboarding_bundles, use_onboarding_selection, use_settings_snapshot,
|
use_migration, use_onboarding_bundles, use_onboarding_selection, use_settings_snapshot,
|
||||||
use_versions, versions_metadata,
|
use_versions, versions_metadata,
|
||||||
};
|
};
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::view::onboarding::{find_instance, matching_new_cluster_id, pkg_key, sources_sentence};
|
use crate::view::onboarding::{matching_new_cluster_id, pkg_key};
|
||||||
|
|
||||||
mod backdrop;
|
mod backdrop;
|
||||||
mod progress;
|
mod progress;
|
||||||
|
|
@ -319,17 +319,20 @@ impl Component for OnboardingDownloading {
|
||||||
.unwrap_or_else(|| "Not signed in".to_string());
|
.unwrap_or_else(|| "Not signed in".to_string());
|
||||||
|
|
||||||
let import_dispatch = dispatch.clone();
|
let import_dispatch = dispatch.clone();
|
||||||
let import_selection = selection.import_selection;
|
let import_folder = selection.import_folder;
|
||||||
let import_dedicated = selection.import_dedicated;
|
let import_dedicated = selection.import_dedicated;
|
||||||
let import_detections = migration_detections(&migration_query);
|
let import_detection = migration_detection(&migration_query);
|
||||||
let import_items = items.clone();
|
let import_items = items.clone();
|
||||||
|
|
||||||
let migration_summary =
|
let migration_summary = import_detection.as_ref().map(|detection| {
|
||||||
(!import_detections.is_empty()).then(|| match import_selection.read().clone() {
|
let source_name = detection.source.display_name().to_string();
|
||||||
Some(chosen) => {
|
match import_folder.read().clone() {
|
||||||
let source_name = chosen.source.display_name().to_string();
|
Some(folder) => {
|
||||||
let dedicated = *import_dedicated.read()
|
let dedicated = *import_dedicated.read()
|
||||||
&& find_instance(&import_detections, &chosen)
|
&& detection
|
||||||
|
.instances
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.folder_name == folder)
|
||||||
.and_then(|inst| matching_new_cluster_id(inst, &items))
|
.and_then(|inst| matching_new_cluster_id(inst, &items))
|
||||||
.is_some();
|
.is_some();
|
||||||
let target = if dedicated {
|
let target = if dedicated {
|
||||||
|
|
@ -337,14 +340,11 @@ impl Component for OnboardingDownloading {
|
||||||
} else {
|
} else {
|
||||||
"Shared game directory"
|
"Shared game directory"
|
||||||
};
|
};
|
||||||
(source_name, chosen.folder_name, target.to_string())
|
(source_name, folder, target.to_string())
|
||||||
}
|
}
|
||||||
None => (
|
None => (source_name, "No files imported".to_string(), String::new()),
|
||||||
sources_sentence(&import_detections),
|
}
|
||||||
"No files imported".to_string(),
|
});
|
||||||
String::new(),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return summary_view(
|
return summary_view(
|
||||||
&items,
|
&items,
|
||||||
|
|
@ -357,16 +357,21 @@ impl Component for OnboardingDownloading {
|
||||||
migration_summary,
|
migration_summary,
|
||||||
predownload_state,
|
predownload_state,
|
||||||
move |_| {
|
move |_| {
|
||||||
if let Some(chosen) = import_selection.peek().clone() {
|
if let (Some(detection), Some(folder)) =
|
||||||
|
(import_detection.as_ref(), import_folder.peek().clone())
|
||||||
|
{
|
||||||
let target = if *import_dedicated.peek() {
|
let target = if *import_dedicated.peek() {
|
||||||
find_instance(&import_detections, &chosen)
|
detection
|
||||||
|
.instances
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.folder_name == folder)
|
||||||
.and_then(|inst| matching_new_cluster_id(inst, &import_items))
|
.and_then(|inst| matching_new_cluster_id(inst, &import_items))
|
||||||
.map(|new_cluster_id| ImportTarget::Dedicated { new_cluster_id })
|
.map(|new_cluster_id| ImportTarget::Dedicated { new_cluster_id })
|
||||||
.unwrap_or(ImportTarget::Shared)
|
.unwrap_or(ImportTarget::Shared)
|
||||||
} else {
|
} else {
|
||||||
ImportTarget::Shared
|
ImportTarget::Shared
|
||||||
};
|
};
|
||||||
import_dispatch.import_launcher(chosen.source, chosen.folder_name, target);
|
import_dispatch.import_launcher(detection.source, folder, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_started.set(true);
|
setup_started.set(true);
|
||||||
|
|
@ -862,7 +867,7 @@ fn finish_onboarding(
|
||||||
dispatch.record_seen_version(version);
|
dispatch.record_seen_version(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch.set_onboarding_seen(true);
|
dispatch.mark_onboarding_seen();
|
||||||
let _ = RouterContext::get().replace(Route::Home {});
|
let _ = RouterContext::get().replace(Route::Home {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya::router::RouterContext;
|
use freya::router::RouterContext;
|
||||||
use oneclient_core::{MigrationDetection, SourceInstance};
|
use oneclient_core::SourceInstance;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, ScrollArea};
|
use crate::components::{Button, Icon, IconType, ScrollArea};
|
||||||
use crate::hooks::{
|
use crate::hooks::{
|
||||||
ClusterBundles, ImportSelection, migration_detections, onboarding_bundles_items, use_migration,
|
ClusterBundles, migration_detection, onboarding_bundles_items, use_migration,
|
||||||
use_onboarding_bundles, use_onboarding_selection,
|
use_onboarding_bundles, use_onboarding_selection,
|
||||||
};
|
};
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
|
|
@ -14,9 +14,9 @@ use crate::view::onboarding::{
|
||||||
choice_row, onboarding_illustration, onboarding_slide, step_heading,
|
choice_row, onboarding_illustration, onboarding_slide, step_heading,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn migrated_categories(detections: &[MigrationDetection]) -> Vec<String> {
|
fn migrated_categories(source: &[SourceInstance]) -> Vec<String> {
|
||||||
let mut categories: Vec<String> = Vec::new();
|
let mut categories: Vec<String> = Vec::new();
|
||||||
for instance in detections.iter().flat_map(|d| &d.instances) {
|
for instance in source {
|
||||||
for category in &instance.categories {
|
for category in &instance.categories {
|
||||||
if !categories.iter().any(|c| c.eq_ignore_ascii_case(category)) {
|
if !categories.iter().any(|c| c.eq_ignore_ascii_case(category)) {
|
||||||
categories.push(category.clone());
|
categories.push(category.clone());
|
||||||
|
|
@ -26,28 +26,8 @@ fn migrated_categories(detections: &[MigrationDetection]) -> Vec<String> {
|
||||||
categories
|
categories
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn find_instance<'a>(
|
/// Whether a new cluster exists matching this source instance's version+loader
|
||||||
detections: &'a [MigrationDetection],
|
/// (needed for the "dedicated dir" import option to have a target).
|
||||||
chosen: &ImportSelection,
|
|
||||||
) -> Option<&'a SourceInstance> {
|
|
||||||
detections
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.source == chosen.source)?
|
|
||||||
.instances
|
|
||||||
.iter()
|
|
||||||
.find(|i| i.folder_name == chosen.folder_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sources_sentence(detections: &[MigrationDetection]) -> String {
|
|
||||||
let names: Vec<&str> = detections.iter().map(|d| d.source.display_name()).collect();
|
|
||||||
|
|
||||||
match names.split_last() {
|
|
||||||
None => String::new(),
|
|
||||||
Some((last, [])) => (*last).to_string(),
|
|
||||||
Some((last, rest)) => format!("{} and {last}", rest.join(", ")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn matching_new_cluster_id(
|
pub(crate) fn matching_new_cluster_id(
|
||||||
instance: &SourceInstance,
|
instance: &SourceInstance,
|
||||||
new: &[ClusterBundles],
|
new: &[ClusterBundles],
|
||||||
|
|
@ -69,60 +49,42 @@ impl Component for OnboardingMigration {
|
||||||
let bundles_query = use_onboarding_bundles();
|
let bundles_query = use_onboarding_bundles();
|
||||||
let selection = use_onboarding_selection();
|
let selection = use_onboarding_selection();
|
||||||
let mut categories = selection.migrated_categories;
|
let mut categories = selection.migrated_categories;
|
||||||
let mut import_selection = selection.import_selection;
|
// Shared across the flow; the file import is dispatched later, on Setup.
|
||||||
|
let mut import_folder = selection.import_folder;
|
||||||
let import_dedicated = selection.import_dedicated;
|
let import_dedicated = selection.import_dedicated;
|
||||||
|
|
||||||
let detections = migration_detections(&migration_query);
|
// No detection yet -> render nothing meaningful; the shell only routes
|
||||||
if detections.is_empty() {
|
// here when a source was detected, so this is a transient loading state.
|
||||||
|
let Some(detection) = migration_detection(&migration_query) else {
|
||||||
return rect()
|
return rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.into_element();
|
.into_element();
|
||||||
}
|
};
|
||||||
|
let source_name = detection.source.display_name();
|
||||||
let new_clusters = onboarding_bundles_items(&bundles_query).unwrap_or_default();
|
let new_clusters = onboarding_bundles_items(&bundles_query).unwrap_or_default();
|
||||||
let bundles_loaded = onboarding_bundles_items(&bundles_query).is_some();
|
let bundles_loaded = onboarding_bundles_items(&bundles_query).is_some();
|
||||||
|
|
||||||
let chosen = import_selection.read().clone();
|
let chosen_folder = import_folder.read().clone();
|
||||||
let any_importable = detections
|
let any_importable = detection.instances.iter().any(|c| c.has_game_dir);
|
||||||
.iter()
|
|
||||||
.flat_map(|d| &d.instances)
|
|
||||||
.any(|c| c.has_game_dir);
|
|
||||||
|
|
||||||
let mut source_groups: Vec<Element> = Vec::new();
|
// One card per detected version. Importable cards double as the file-
|
||||||
for detection in &detections {
|
// import selector; non-importable ones stay informational.
|
||||||
let source = detection.source;
|
let mut version_cards: Vec<Element> = Vec::new();
|
||||||
let mut version_cards: Vec<Element> = Vec::new();
|
for instance in &detection.instances {
|
||||||
|
let selected_import = chosen_folder.as_deref() == Some(instance.folder_name.as_str());
|
||||||
|
let dedicated_available = matching_new_cluster_id(instance, &new_clusters).is_some();
|
||||||
|
|
||||||
for instance in &detection.instances {
|
version_cards.push(version_card(
|
||||||
let selected_import = chosen
|
instance,
|
||||||
.as_ref()
|
selected_import,
|
||||||
.is_some_and(|c| c.source == source && c.folder_name == instance.folder_name);
|
*import_dedicated.read(),
|
||||||
let dedicated_available =
|
dedicated_available,
|
||||||
matching_new_cluster_id(instance, &new_clusters).is_some();
|
import_dedicated,
|
||||||
|
{
|
||||||
version_cards.push(version_card(
|
let folder = instance.folder_name.clone();
|
||||||
instance,
|
move |_| import_folder.set(Some(folder.clone()))
|
||||||
selected_import,
|
},
|
||||||
*import_dedicated.read(),
|
|
||||||
dedicated_available,
|
|
||||||
import_dedicated,
|
|
||||||
{
|
|
||||||
let folder_name = instance.folder_name.clone();
|
|
||||||
move |_| {
|
|
||||||
import_selection.set(Some(ImportSelection {
|
|
||||||
source,
|
|
||||||
folder_name: folder_name.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
source_groups.push(source_group(
|
|
||||||
source.display_name(),
|
|
||||||
detection.instances.len(),
|
|
||||||
version_cards,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,27 +95,40 @@ impl Component for OnboardingMigration {
|
||||||
.child(step_heading(
|
.child(step_heading(
|
||||||
"Bring over your setup",
|
"Bring over your setup",
|
||||||
&format!(
|
&format!(
|
||||||
"We found data from {}. Your installed categories are reselected \
|
"We found data from your previous {source_name} install. Your installed \
|
||||||
automatically. Optionally, pick one version to copy its files \
|
categories are reselected automatically. Optionally, pick one version to \
|
||||||
(worlds, config, and packs).",
|
copy its files (worlds, config, and packs)."
|
||||||
sources_sentence(&detections)
|
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
// "Don't import files" is the default choice, pinned above the list.
|
||||||
.maybe_child(any_importable.then(|| {
|
.maybe_child(any_importable.then(|| {
|
||||||
import_choice_card(
|
import_choice_card(
|
||||||
"Don't import files",
|
"Don't import files",
|
||||||
"Start fresh — only your bundle selection carries over.",
|
"Start fresh — only your bundle selection carries over.",
|
||||||
None,
|
None,
|
||||||
chosen.is_none(),
|
chosen_folder.is_none(),
|
||||||
move |_| import_selection.set(None),
|
move |_| import_folder.set(None),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.spacing(16.)
|
.spacing(10.)
|
||||||
.children(source_groups),
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Detected versions")
|
||||||
|
.font_size(13.)
|
||||||
|
.font_weight(FontWeight::MEDIUM)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(8.)
|
||||||
|
.children(version_cards),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_element();
|
.into_element();
|
||||||
|
|
||||||
|
|
@ -192,7 +167,7 @@ impl Component for OnboardingMigration {
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
.child(migration_nav(bundles_loaded, move || {
|
.child(migration_nav(bundles_loaded, move || {
|
||||||
categories.set(Some(migrated_categories(&detections)));
|
categories.set(Some(migrated_categories(&detection.instances)));
|
||||||
|
|
||||||
let _ = RouterContext::get().replace(Route::OnboardingLanguage {});
|
let _ = RouterContext::get().replace(Route::OnboardingLanguage {});
|
||||||
}))
|
}))
|
||||||
|
|
@ -200,86 +175,11 @@ impl Component for OnboardingMigration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source_group(name: &str, instance_count: usize, cards: Vec<Element>) -> Element {
|
/// A detected source instance rendered as a card. Shows version + categories
|
||||||
let counted = if instance_count == 1 {
|
/// (always-reselected info); when the instance has a game dir it also acts as
|
||||||
"1 version".to_string()
|
/// the file-import selector and, when selected, exposes the shared/dedicated
|
||||||
} else {
|
/// target choice inline.
|
||||||
format!("{instance_count} versions")
|
fn version_card(
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::fill())
|
|
||||||
.spacing(10.)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.content(Content::Flex)
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(8.)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(name.to_string())
|
|
||||||
.font_size(13.)
|
|
||||||
.font_weight(FontWeight::MEDIUM)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::flex(1.0))
|
|
||||||
.height(Size::px(1.))
|
|
||||||
.background(colors::component_border()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(counted)
|
|
||||||
.font_size(11.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.vertical()
|
|
||||||
.width(Size::fill())
|
|
||||||
.spacing(8.)
|
|
||||||
.children(cards),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn chip(text: String, on_brand: bool) -> Element {
|
|
||||||
let (bg, border, fg) = if on_brand {
|
|
||||||
(
|
|
||||||
colors::fg_primary().with_a(38),
|
|
||||||
colors::fg_primary().with_a(70),
|
|
||||||
colors::fg_primary(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
colors::component_bg(),
|
|
||||||
colors::component_border(),
|
|
||||||
colors::fg_secondary(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.center()
|
|
||||||
.padding(Gaps::new_symmetric(2., 8.))
|
|
||||||
.corner_radius(CornerRadius::new_all(999.))
|
|
||||||
.background(bg)
|
|
||||||
.border(border_all_color(1., border))
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(text)
|
|
||||||
.font_size(10.)
|
|
||||||
.font_weight(FontWeight::MEDIUM)
|
|
||||||
.color(fg),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn version_card(
|
|
||||||
instance: &SourceInstance,
|
instance: &SourceInstance,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
dedicated: bool,
|
dedicated: bool,
|
||||||
|
|
@ -290,38 +190,18 @@ pub fn version_card(
|
||||||
let importable = instance.has_game_dir;
|
let importable = instance.has_game_dir;
|
||||||
let mut on_select = on_select;
|
let mut on_select = on_select;
|
||||||
|
|
||||||
|
let categories = if instance.categories.is_empty() {
|
||||||
|
"No bundle categories".to_string()
|
||||||
|
} else {
|
||||||
|
instance.categories.join(", ")
|
||||||
|
};
|
||||||
|
let subtitle = format!("{} · {}", instance.mc_version, instance.mc_loader);
|
||||||
|
|
||||||
let (bg, border_color) = if selected {
|
let (bg, border_color) = if selected {
|
||||||
(colors::brand(), colors::brand())
|
(colors::brand(), colors::brand())
|
||||||
} else {
|
} else {
|
||||||
(colors::page_elevated(), colors::component_border())
|
(colors::page_elevated(), colors::component_border())
|
||||||
};
|
};
|
||||||
let subtitle_color = if selected {
|
|
||||||
colors::fg_primary()
|
|
||||||
} else {
|
|
||||||
colors::fg_secondary()
|
|
||||||
};
|
|
||||||
|
|
||||||
let icon_tile = rect()
|
|
||||||
.width(Size::px(34.))
|
|
||||||
.height(Size::px(34.))
|
|
||||||
.center()
|
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
|
||||||
.background(if selected {
|
|
||||||
colors::fg_primary().with_a(38)
|
|
||||||
} else {
|
|
||||||
colors::component_bg()
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Icon::new(if !importable {
|
|
||||||
IconType::FileX02
|
|
||||||
} else if selected {
|
|
||||||
IconType::FolderCheck
|
|
||||||
} else {
|
|
||||||
IconType::Folder
|
|
||||||
})
|
|
||||||
.size(17.)
|
|
||||||
.color(subtitle_color),
|
|
||||||
);
|
|
||||||
|
|
||||||
let header = rect()
|
let header = rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
|
|
@ -329,7 +209,6 @@ pub fn version_card(
|
||||||
.content(Content::Flex)
|
.content(Content::Flex)
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
.spacing(12.)
|
.spacing(12.)
|
||||||
.child(icon_tile)
|
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
|
|
@ -342,61 +221,39 @@ pub fn version_card(
|
||||||
.font_weight(FontWeight::MEDIUM)
|
.font_weight(FontWeight::MEDIUM)
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(label().text(subtitle).font_size(11.).color(if selected {
|
||||||
label()
|
colors::fg_primary()
|
||||||
.text(format!("{} · {}", instance.mc_version, instance.mc_loader))
|
} else {
|
||||||
.font_size(11.)
|
colors::fg_secondary()
|
||||||
.color(subtitle_color),
|
}))
|
||||||
),
|
.child(label().text(categories).font_size(11.).color(if selected {
|
||||||
|
colors::fg_primary()
|
||||||
|
} else {
|
||||||
|
colors::fg_secondary()
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.child(if !importable {
|
// Selection affordance only when the instance actually has files.
|
||||||
label()
|
.maybe_child(importable.then(|| {
|
||||||
.text("No files")
|
if selected {
|
||||||
.font_size(11.)
|
Icon::new(IconType::Check).size(18.).into_element()
|
||||||
.color(colors::fg_secondary())
|
} else {
|
||||||
.into_element()
|
label()
|
||||||
} else if selected {
|
.text("Import files")
|
||||||
Icon::new(IconType::Check).size(18.).into_element()
|
.font_size(12.)
|
||||||
} else {
|
.color(colors::fg_secondary())
|
||||||
label()
|
.into_element()
|
||||||
.text("Import files")
|
}
|
||||||
.font_size(12.)
|
}));
|
||||||
.color(colors::fg_secondary())
|
|
||||||
.into_element()
|
|
||||||
});
|
|
||||||
|
|
||||||
let categories = if instance.categories.is_empty() {
|
|
||||||
label()
|
|
||||||
.text("No bundle categories")
|
|
||||||
.font_size(11.)
|
|
||||||
.color(subtitle_color)
|
|
||||||
.into_element()
|
|
||||||
} else {
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(6.)
|
|
||||||
.children(
|
|
||||||
instance
|
|
||||||
.categories
|
|
||||||
.iter()
|
|
||||||
.map(|c| chip(c.clone(), selected))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut card = rect()
|
let mut card = rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.spacing(10.)
|
.spacing(12.)
|
||||||
.padding(Gaps::new_symmetric(12., 14.))
|
.padding(Gaps::new_symmetric(12., 14.))
|
||||||
.corner_radius(CornerRadius::new_all(10.))
|
.corner_radius(CornerRadius::new_all(10.))
|
||||||
.background(bg)
|
.background(bg)
|
||||||
.border(border_all_color(1., border_color))
|
.border(border_all_color(1., border_color))
|
||||||
.child(header)
|
.child(header);
|
||||||
.child(categories);
|
|
||||||
|
|
||||||
if importable {
|
if importable {
|
||||||
card = card
|
card = card
|
||||||
|
|
@ -531,96 +388,3 @@ fn migration_nav(next_enabled: bool, on_next: impl FnMut() + 'static) -> impl In
|
||||||
)
|
)
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use oneclient_core::MigrationSource;
|
|
||||||
use oneclient_core::packages::domain::GameLoader;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn instance(folder: &str, categories: &[&str]) -> SourceInstance {
|
|
||||||
SourceInstance {
|
|
||||||
instance_id: 0,
|
|
||||||
folder_name: folder.to_string(),
|
|
||||||
mc_version: "1.21.10".to_string(),
|
|
||||||
mc_loader: GameLoader::Fabric,
|
|
||||||
categories: categories.iter().map(|c| c.to_string()).collect(),
|
|
||||||
has_game_dir: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detection(source: MigrationSource, instances: Vec<SourceInstance>) -> MigrationDetection {
|
|
||||||
MigrationDetection {
|
|
||||||
source,
|
|
||||||
root: PathBuf::from("/tmp"),
|
|
||||||
instances,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn both() -> Vec<MigrationDetection> {
|
|
||||||
vec![
|
|
||||||
detection(
|
|
||||||
MigrationSource::OneClientV1,
|
|
||||||
vec![instance("1.21", &["HUD", "QoL"])],
|
|
||||||
),
|
|
||||||
detection(
|
|
||||||
MigrationSource::LunarClient,
|
|
||||||
vec![instance("1.21", &["QoL", "PvP"]), instance("1.8", &["PvP"])],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn categories_pool_across_every_detected_launcher() {
|
|
||||||
let categories = migrated_categories(&both());
|
|
||||||
|
|
||||||
assert_eq!(categories, vec!["HUD", "QoL", "PvP"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn categories_are_not_duplicated_across_launchers() {
|
|
||||||
// "QoL" is present in both detections above.
|
|
||||||
let categories = migrated_categories(&both());
|
|
||||||
|
|
||||||
assert_eq!(categories.iter().filter(|c| *c == "QoL").count(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn same_folder_name_in_two_launchers_resolves_to_the_chosen_one() {
|
|
||||||
// Both launchers have a "1.21"; only the source tells them apart.
|
|
||||||
let chosen = ImportSelection {
|
|
||||||
source: MigrationSource::LunarClient,
|
|
||||||
folder_name: "1.21".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let detections = both();
|
|
||||||
let found = find_instance(&detections, &chosen).expect("instance");
|
|
||||||
|
|
||||||
assert_eq!(found.categories, vec!["QoL", "PvP"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_selection_pointing_at_nothing_resolves_to_none() {
|
|
||||||
let chosen = ImportSelection {
|
|
||||||
source: MigrationSource::OneClientV1,
|
|
||||||
folder_name: "1.8".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(find_instance(&both(), &chosen).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sources_read_as_a_sentence() {
|
|
||||||
let detections = both();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
sources_sentence(&detections),
|
|
||||||
"OneClient and Lunar Client".to_string()
|
|
||||||
);
|
|
||||||
assert_eq!(sources_sentence(&detections[..1]), "OneClient".to_string());
|
|
||||||
assert_eq!(sources_sentence(&[]), String::new());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ pub use bundles::OnboardingBundles;
|
||||||
pub use downloading::{LoadingBackdrop, OnboardingDownloading};
|
pub use downloading::{LoadingBackdrop, OnboardingDownloading};
|
||||||
pub use language::OnboardingLanguage;
|
pub use language::OnboardingLanguage;
|
||||||
pub use migration::OnboardingMigration;
|
pub use migration::OnboardingMigration;
|
||||||
pub(crate) use migration::{find_instance, matching_new_cluster_id, sources_sentence};
|
pub(crate) use migration::matching_new_cluster_id;
|
||||||
pub use preferences::OnboardingPreferences;
|
pub use preferences::OnboardingPreferences;
|
||||||
pub use terms::OnboardingTerms;
|
pub use terms::OnboardingTerms;
|
||||||
pub use welcome::OnboardingWelcome;
|
pub use welcome::OnboardingWelcome;
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
use oneclient_core::migration::lunar_client;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
match lunar_client::detect().await {
|
|
||||||
Ok(Some(detection)) => {
|
|
||||||
println!(
|
|
||||||
"source: {} ({})",
|
|
||||||
detection.source.display_name(),
|
|
||||||
detection.source.id()
|
|
||||||
);
|
|
||||||
println!("root: {}", detection.root.display());
|
|
||||||
println!("instances: {}", detection.instances.len());
|
|
||||||
for i in &detection.instances {
|
|
||||||
println!(
|
|
||||||
" [{}] {} ({} {:?}) game_dir={} categories={:?}",
|
|
||||||
i.instance_id,
|
|
||||||
i.folder_name,
|
|
||||||
i.mc_version,
|
|
||||||
i.mc_loader,
|
|
||||||
i.has_game_dir,
|
|
||||||
i.categories,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => println!("no lunar client install detected"),
|
|
||||||
Err(err) => eprintln!("detection error: {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
match oneclient_core::detect_migrations().await {
|
|
||||||
Ok(detections) if detections.is_empty() => {
|
|
||||||
println!("no migratable launcher detected");
|
|
||||||
}
|
|
||||||
Ok(detections) => {
|
|
||||||
println!("sources: {}", detections.len());
|
|
||||||
for detection in &detections {
|
|
||||||
println!(
|
|
||||||
"\n{} ({}) root={}",
|
|
||||||
detection.source.display_name(),
|
|
||||||
detection.source.id(),
|
|
||||||
detection.root.display()
|
|
||||||
);
|
|
||||||
for i in &detection.instances {
|
|
||||||
println!(
|
|
||||||
" {} ({} {:?}) game_dir={} categories={:?}",
|
|
||||||
i.folder_name, i.mc_version, i.mc_loader, i.has_game_dir, i.categories,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => eprintln!("detection error: {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use oneclient_core::migration::oneclient_v1;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
match oneclient_v1::detect().await {
|
match oneclient_core::detect_migration().await {
|
||||||
Ok(Some(detection)) => {
|
Ok(Some(detection)) => {
|
||||||
println!(
|
println!(
|
||||||
"source: {} ({})",
|
"source: {} ({})",
|
||||||
|
|
@ -23,7 +21,7 @@ async fn main() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => println!("no v1 install detected"),
|
Ok(None) => println!("no migratable launcher detected"),
|
||||||
Err(err) => eprintln!("detection error: {err}"),
|
Err(err) => eprintln!("detection error: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,27 +31,29 @@ pub mod recovery;
|
||||||
pub mod reporting;
|
pub mod reporting;
|
||||||
pub mod screenshots;
|
pub mod screenshots;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
mod state;
|
|
||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod tos;
|
pub mod tos;
|
||||||
|
mod state;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod versions;
|
pub mod versions;
|
||||||
|
|
||||||
pub use bundles::{
|
pub use bundles::{
|
||||||
ApplyBundleUpdatesResult, Bundle, BundleArchive, BundleError, BundleFile, BundleFileKind,
|
apply_bundle_updates, check_bundle_updates, effective_enabled, install_bundle,
|
||||||
BundleManifest, BundleUpdateCheckResult, BundleWithUpdateStatus, BundlesManager,
|
install_cluster_bundles,
|
||||||
FileUpdateStatus, apply_bundle_updates, check_bundle_updates, effective_enabled,
|
|
||||||
get_bundles_with_update_status, install_bundle, install_cluster_bundles,
|
|
||||||
install_package_from_bundle, is_bundle_syncing, list_cluster_bundle_overrides,
|
install_package_from_bundle, is_bundle_syncing, list_cluster_bundle_overrides,
|
||||||
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
||||||
set_bundle_package_overrides,
|
set_bundle_package_overrides,
|
||||||
|
ApplyBundleUpdatesResult, Bundle, BundleArchive, BundleError, BundleFile,
|
||||||
|
BundleFileKind, BundleManifest, BundlesManager, BundleUpdateCheckResult,
|
||||||
|
BundleWithUpdateStatus, FileUpdateStatus, get_bundles_with_update_status,
|
||||||
};
|
};
|
||||||
pub use changelog::{ChangelogGroup, fetch_changelog, parse_changelog};
|
pub use changelog::{fetch_changelog, parse_changelog, ChangelogGroup};
|
||||||
|
pub use tos::{fetch_terms, TermsDocument};
|
||||||
|
pub use discord::{DiscordRpc, Presence};
|
||||||
pub use clusters::{
|
pub use clusters::{
|
||||||
Cluster, ClusterError, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions,
|
Cluster, ClusterError, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions,
|
||||||
ensure_from_bundles, ensure_from_versions, estimate_cluster_download,
|
ensure_from_bundles, ensure_from_versions, estimate_cluster_download,
|
||||||
};
|
};
|
||||||
pub use discord::{DiscordRpc, Presence};
|
|
||||||
pub use error::{LauncherError, LauncherResult};
|
pub use error::{LauncherError, LauncherResult};
|
||||||
pub use game::{GameError, LaunchedGame, get_loader_versions, launch_cluster};
|
pub use game::{GameError, LaunchedGame, get_loader_versions, launch_cluster};
|
||||||
pub use images::ImageCacheStore;
|
pub use images::ImageCacheStore;
|
||||||
|
|
@ -61,19 +63,17 @@ pub use logs::{
|
||||||
};
|
};
|
||||||
pub use metadata::{MetadataError, MetadataStore};
|
pub use metadata::{MetadataError, MetadataStore};
|
||||||
pub use migration::{
|
pub use migration::{
|
||||||
ImportTarget, MigrationDetection, MigrationSource, SourceInstance,
|
detect as detect_migration, import_game_dir as import_migration_game_dir, ImportTarget,
|
||||||
detect_all as detect_migrations, import_game_dir as import_migration_game_dir,
|
MigrationDetection, MigrationSource, SourceInstance,
|
||||||
import_settings as import_migration_settings,
|
};
|
||||||
|
pub use screenshots::{
|
||||||
|
ScreenshotInfo, ScreenshotsError, delete_screenshot, list_cluster_screenshots, load_screenshot,
|
||||||
};
|
};
|
||||||
pub use notification::{GroupedProgressChild, GroupedProgressEvent, GroupedProgressSession};
|
pub use notification::{GroupedProgressChild, GroupedProgressEvent, GroupedProgressSession};
|
||||||
pub use packages::LinkedArtifactInfo;
|
pub use packages::LinkedArtifactInfo;
|
||||||
pub use patch::Patch;
|
pub use patch::Patch;
|
||||||
pub use screenshots::{
|
|
||||||
ScreenshotInfo, ScreenshotsError, delete_screenshot, list_cluster_screenshots, load_screenshot,
|
|
||||||
};
|
|
||||||
pub use settings::ProfileUpdate;
|
pub use settings::ProfileUpdate;
|
||||||
pub use state::LauncherServices;
|
pub use state::LauncherServices;
|
||||||
pub use state::LauncherState;
|
pub use state::LauncherState;
|
||||||
pub use tos::{TermsDocument, fetch_terms};
|
|
||||||
pub use version::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version};
|
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};
|
||||||
|
|
|
||||||
|
|
@ -1,559 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::Row;
|
|
||||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
|
||||||
|
|
||||||
use crate::LauncherResult;
|
|
||||||
use crate::packages::domain::GameLoader;
|
|
||||||
use crate::paths;
|
|
||||||
use crate::settings::{GameSettingsProfile, Resolution, store};
|
|
||||||
|
|
||||||
use super::fs::{copy_tree, dir_has_content};
|
|
||||||
use super::{ImportTarget, MigrationDetection, MigrationSource, SourceInstance};
|
|
||||||
|
|
||||||
const IMPORT_EXCLUDE_TOP: &[&str] = &[
|
|
||||||
"mods",
|
|
||||||
"logs",
|
|
||||||
"optionsLC.txt",
|
|
||||||
"optionsof.txt",
|
|
||||||
"base-modpack.mrpack",
|
|
||||||
"servers.dat_old",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn old_root() -> Option<PathBuf> {
|
|
||||||
let base = directories::BaseDirs::new().map(|d| d.home_dir().join(".lunarclient"));
|
|
||||||
|
|
||||||
let root = match std::env::var("LUNAR_CLIENT_DIR") {
|
|
||||||
Ok(dir) => PathBuf::from(dir),
|
|
||||||
Err(_) => base?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let exists = root.is_dir();
|
|
||||||
tracing::debug!(root = %root.display(), exists, "lunar migration: resolved old root");
|
|
||||||
exists.then_some(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lunar mod ids appear in two spellings across config versions (`MOTION_BLUR` and `motionBlur`)
|
|
||||||
fn normalize_mod_id(id: &str) -> String {
|
|
||||||
id.chars()
|
|
||||||
.filter(|c| c.is_ascii_alphanumeric())
|
|
||||||
.flat_map(char::to_uppercase)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps a Lunar mod onto the OneClient bundle category that ships an equivalent.
|
|
||||||
/// Ids absent from this table have no counterpart and contribute no category.
|
|
||||||
fn category_for_mod(normalized_id: &str) -> Option<&'static str> {
|
|
||||||
let category = match normalized_id {
|
|
||||||
"FPS" | "CPS" | "PING" | "CLOCK" | "PLAYTIME" | "MEMORY" | "STOPWATCH" | "COORDINATES"
|
|
||||||
| "COORDS" | "DIRECTIONHUD" | "ARMORSTATUS" | "KEYSTROKES" | "SCOREBOARD"
|
|
||||||
| "POTIONEFFECTS" | "POTIONCOUNTER" | "ITEMCOUNTER" | "TOTEMCOUNTER" | "ITEMTRACKER"
|
|
||||||
| "DAYCOUNTER" | "F3DISPLAY" | "BOSSBAR" | "TAB" | "TITLES" | "TITLEMOD"
|
|
||||||
| "INVENTORYMOD" | "HORSESTATS" | "PACKDISPLAY" | "RESOURCEDISPLAY" | "SERVERADDRESS"
|
|
||||||
| "SERVERADDRESSMOD" | "TEAMVIEW" | "SHINYPOTS" => "HUD",
|
|
||||||
|
|
||||||
"HITBOX" | "HITCOLOR" | "PVPINFO" | "TIERTAGGER" | "UHCOVERLAY" | "SNAPLOOK"
|
|
||||||
| "HURTCAM" | "DAMAGETINT" | "REACHDISPLAY" | "RANGE" | "COMBO" | "COOLDOWNS"
|
|
||||||
| "OVERLAYMOD" | "TNTCOUNTDOWN" => "PvP",
|
|
||||||
|
|
||||||
"MOTIONBLUR" | "MENUBLUR" | "3DSKINS" | "SKINLAYERS3D" | "GLINTCOLORIZER" | "CROSSHAIR"
|
|
||||||
| "BLOCKOUTLINE" | "FOG" | "FOGMOD" | "FOV" | "FOVMOD" | "ITEMPHYSICS" | "ITEMPHYSIC"
|
|
||||||
| "SCROLLABLETOOLTIPS" | "CHAT" | "NAMETAG" | "NICKHIDER" | "TIMECHANGER"
|
|
||||||
| "WEATHERCHANGER" | "PARTICLECHANGER" | "PARTICLEMOD" | "SATURATION" | "SATURATIONMOD"
|
|
||||||
| "COLORSATURATION" | "LIGHTING" | "2DITEMS" | "ITEMS2D" | "SHULKERPREVIEW"
|
|
||||||
| "AUDIOSUBTITLES" | "SOUNDCHANGER" | "MOBSIZE" | "MOMENTUM" | "MOMENTUMMOD"
|
|
||||||
| "ONESEVENVISUALS" | "PACKORGANIZER" | "ITEMCUSTOMIZER" | "AUTOTEXTACTIONS"
|
|
||||||
| "AUTOTEXTHOTKEY" | "TEXTHOTKEY" => "QoL",
|
|
||||||
|
|
||||||
"ZOOM" | "WAILA" | "FREELOOK" | "MUMBLELINK" | "WORLDEDITCUI" | "CHUNKBORDERS"
|
|
||||||
| "SCREENSHOT" | "REPLAYMOD" | "REWIND" | "RADIO" | "MINIMAP" | "WAYPOINTS" | "MARKERS"
|
|
||||||
| "KILLSOUNDS" | "QUICKPLAY" | "TOGGLESNEAK" | "GUISCALE" | "HYPIXELMOD"
|
|
||||||
| "HYPIXELBEDWARS" => "Utility",
|
|
||||||
|
|
||||||
"SKYBLOCK" | "SKYBLOCKADDONS" | "NEU" => "SkyBlock",
|
|
||||||
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(category)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct LunarModEntry {
|
|
||||||
#[serde(default)]
|
|
||||||
enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct LunarSettingsProfile {
|
|
||||||
name: String,
|
|
||||||
#[serde(default)]
|
|
||||||
default: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug")]
|
|
||||||
async fn active_profile_name(root: &Path) -> Option<String> {
|
|
||||||
let path = root
|
|
||||||
.join("settings")
|
|
||||||
.join("game")
|
|
||||||
.join("profile_manager.json");
|
|
||||||
let raw = polyio::read(&path).await.ok()?;
|
|
||||||
let profiles: Vec<LunarSettingsProfile> = serde_json::from_slice(&raw)
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::warn!(error = %err, "malformed profile_manager.json; no mod settings");
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let chosen = profiles
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.active)
|
|
||||||
.or_else(|| profiles.iter().find(|p| p.default))
|
|
||||||
.or_else(|| profiles.first())?;
|
|
||||||
|
|
||||||
tracing::debug!(profile = %chosen.name, "lunar migration: active settings profile");
|
|
||||||
Some(chosen.name.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The active profile's `mods.json`, as a raw object.
|
|
||||||
async fn active_profile_mods(root: &Path) -> Option<serde_json::Map<String, serde_json::Value>> {
|
|
||||||
let name = active_profile_name(root).await?;
|
|
||||||
let path = root
|
|
||||||
.join("settings")
|
|
||||||
.join("game")
|
|
||||||
.join(name)
|
|
||||||
.join("mods.json");
|
|
||||||
let raw = polyio::read(&path).await.ok()?;
|
|
||||||
|
|
||||||
serde_json::from_slice(&raw)
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::warn!(path = %path.display(), error = %err, "malformed mods.json");
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug")]
|
|
||||||
async fn enabled_categories(root: &Path) -> Vec<String> {
|
|
||||||
let settings_dir = root.join("settings").join("game");
|
|
||||||
let mut categories: Vec<String> = Vec::new();
|
|
||||||
|
|
||||||
let mut entries = match polyio::read_dir(&settings_dir).await {
|
|
||||||
Ok(entries) => entries,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::debug!(error = %err, "lunar migration: no game settings dir; no categories");
|
|
||||||
return categories;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
|
||||||
let mods_file = entry.path().join("mods.json");
|
|
||||||
if !mods_file.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let raw = match polyio::read(&mods_file).await {
|
|
||||||
Ok(raw) => raw,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(path = %mods_file.display(), error = %err, "unreadable mods.json");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Values are heterogeneous (`"version": 14` alongside mod objects), so
|
|
||||||
// anything that does not deserialize into a mod entry is skipped.
|
|
||||||
let parsed: serde_json::Map<String, serde_json::Value> = match serde_json::from_slice(&raw)
|
|
||||||
{
|
|
||||||
Ok(parsed) => parsed,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(path = %mods_file.display(), error = %err, "malformed mods.json");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (mod_id, value) in parsed {
|
|
||||||
let Ok(entry) = serde_json::from_value::<LunarModEntry>(value) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !entry.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(category) = category_for_mod(&normalize_mod_id(&mod_id)) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !categories.iter().any(|c| c == category) {
|
|
||||||
categories.push(category.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.sort();
|
|
||||||
categories
|
|
||||||
}
|
|
||||||
|
|
||||||
fn loader_from_json(raw: &str) -> GameLoader {
|
|
||||||
let loaders: Vec<String> = serde_json::from_str(raw).unwrap_or_default();
|
|
||||||
|
|
||||||
if loaders.iter().any(|l| l.eq_ignore_ascii_case("fabric")) {
|
|
||||||
GameLoader::Fabric
|
|
||||||
} else if loaders.iter().any(|l| l.eq_ignore_ascii_case("quilt")) {
|
|
||||||
GameLoader::Quilt
|
|
||||||
} else if loaders.iter().any(|l| l.eq_ignore_ascii_case("forge")) {
|
|
||||||
GameLoader::Forge
|
|
||||||
} else {
|
|
||||||
GameLoader::Vanilla
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn detect() -> LauncherResult<Option<MigrationDetection>> {
|
|
||||||
let Some(root) = old_root() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let db_path = root.join("db").join("profiles.db");
|
|
||||||
if !db_path.exists() {
|
|
||||||
tracing::debug!(db = %db_path.display(), "lunar migration: no profiles.db, skipping");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
match detect_inner(&root, &db_path).await {
|
|
||||||
Ok(detection) => {
|
|
||||||
tracing::info!(
|
|
||||||
instances = detection.instances.len(),
|
|
||||||
"lunar migration: detected old install"
|
|
||||||
);
|
|
||||||
Ok(Some(detection))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(error = %err, "failed to read lunar profiles.db; skipping migration");
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug")]
|
|
||||||
async fn detect_inner(root: &Path, db_path: &Path) -> LauncherResult<MigrationDetection> {
|
|
||||||
let opts = SqliteConnectOptions::new()
|
|
||||||
.filename(db_path)
|
|
||||||
.read_only(true)
|
|
||||||
.immutable(true);
|
|
||||||
let pool = SqlitePoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect_with(opts)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let profile_rows = sqlx::query("SELECT path, game_version, loaders FROM profiles")
|
|
||||||
.fetch_all(&pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
pool.close().await;
|
|
||||||
|
|
||||||
let categories = enabled_categories(root).await;
|
|
||||||
|
|
||||||
let mut instances = Vec::with_capacity(profile_rows.len());
|
|
||||||
for (index, row) in profile_rows.into_iter().enumerate() {
|
|
||||||
let folder_name: String = row.try_get("path")?;
|
|
||||||
let mc_version: String = row.try_get("game_version")?;
|
|
||||||
let loaders_raw: String = row.try_get("loaders")?;
|
|
||||||
|
|
||||||
let has_game_dir = dir_has_content(&root.join("profiles").join(&folder_name)).await;
|
|
||||||
|
|
||||||
instances.push(SourceInstance {
|
|
||||||
// Lunar keys profiles by uuid; SourceInstance ids are informational.
|
|
||||||
instance_id: index as i64,
|
|
||||||
folder_name,
|
|
||||||
mc_version,
|
|
||||||
mc_loader: loader_from_json(&loaders_raw),
|
|
||||||
categories: categories.clone(),
|
|
||||||
has_game_dir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(MigrationDetection {
|
|
||||||
source: MigrationSource::LunarClient,
|
|
||||||
root: root.to_path_buf(),
|
|
||||||
instances,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct LunarLauncherFile {
|
|
||||||
settings: LunarLauncherSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct LunarLauncherSettings {
|
|
||||||
allocated_memory: Option<u32>,
|
|
||||||
game_resolution: Option<LunarResolution>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct LunarResolution {
|
|
||||||
width: Option<u32>,
|
|
||||||
height: Option<u32>,
|
|
||||||
#[serde(default)]
|
|
||||||
fullscreen: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn import_settings() -> LauncherResult<()> {
|
|
||||||
let Some(root) = old_root() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = root.join("settings").join("launcher.json");
|
|
||||||
if !path.is_file() {
|
|
||||||
tracing::debug!(path = %path.display(), "lunar migration: no launcher.json");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let raw = polyio::read(&path).await?;
|
|
||||||
let parsed: LunarLauncherFile = match serde_json::from_slice(&raw) {
|
|
||||||
Ok(parsed) => parsed,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(error = %err, "malformed lunar launcher.json; keeping current settings");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = crate::state::LauncherState::get()?;
|
|
||||||
let mut global: GameSettingsProfile = state.settings.read().global_game_settings.clone();
|
|
||||||
|
|
||||||
apply_lunar_settings(&mut global, &parsed.settings);
|
|
||||||
|
|
||||||
store::save_global_profile(&state.settings, global).await?;
|
|
||||||
tracing::info!("lunar migration: imported launcher settings");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_lunar_settings(global: &mut GameSettingsProfile, lunar: &LunarLauncherSettings) {
|
|
||||||
if let Some(memory) = lunar.allocated_memory {
|
|
||||||
global.mem_max = Some(memory);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(resolution) = &lunar.game_resolution {
|
|
||||||
if let (Some(width), Some(height)) = (resolution.width, resolution.height) {
|
|
||||||
global.resolution = Some(Resolution::new(width, height));
|
|
||||||
}
|
|
||||||
if let Some(fullscreen) = resolution.fullscreen {
|
|
||||||
global.force_fullscreen = Some(fullscreen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lunar stores the vanilla options it manages in `optionsLC.txt`, pretty much the same as options.txt just in json
|
|
||||||
#[tracing::instrument(level = "debug")]
|
|
||||||
async fn merge_lunar_options(src: &Path, dest: &Path) -> LauncherResult<()> {
|
|
||||||
let lunar_options = src.join("optionsLC.txt");
|
|
||||||
if !lunar_options.is_file() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let raw = polyio::read(&lunar_options).await?;
|
|
||||||
let parsed: serde_json::Map<String, serde_json::Value> = match serde_json::from_slice(&raw) {
|
|
||||||
Ok(parsed) => parsed,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!(error = %err, "malformed optionsLC.txt; keeping vanilla options.txt");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let options_path = dest.join("options.txt");
|
|
||||||
let existing = match polyio::read(&options_path).await {
|
|
||||||
Ok(existing) => String::from_utf8_lossy(&existing).into_owned(),
|
|
||||||
Err(_) => String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let merged = merge_options_text(&existing, &parsed);
|
|
||||||
polyio::write(&options_path, merged.into_bytes()).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Overwrites `options.txt` entries Lunar owns, appends the ones it adds, and
|
|
||||||
/// leaves every other line untouched.
|
|
||||||
fn merge_options_text(
|
|
||||||
existing: &str,
|
|
||||||
lunar: &serde_json::Map<String, serde_json::Value>,
|
|
||||||
) -> String {
|
|
||||||
let overrides: Vec<(String, String)> = lunar
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(key, value)| {
|
|
||||||
// Lunar's own bookkeeping keys are not vanilla options.
|
|
||||||
if key == "lastLaunchedVersion" || key == "version" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let rendered = match value {
|
|
||||||
serde_json::Value::String(s) => s.clone(),
|
|
||||||
serde_json::Value::Bool(b) => b.to_string(),
|
|
||||||
serde_json::Value::Number(n) => n.to_string(),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
Some((key.clone(), rendered))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut out = String::with_capacity(existing.len());
|
|
||||||
let mut written: Vec<&str> = Vec::new();
|
|
||||||
|
|
||||||
for line in existing.lines() {
|
|
||||||
let key = line.split_once(':').map(|(k, _)| k);
|
|
||||||
match key.and_then(|k| overrides.iter().find(|(ok, _)| ok == k)) {
|
|
||||||
Some((k, v)) => {
|
|
||||||
out.push_str(&format!("{k}:{v}\n"));
|
|
||||||
written.push(k);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
out.push_str(line);
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, value) in &overrides {
|
|
||||||
if !written.iter().any(|w| w == key) {
|
|
||||||
out.push_str(&format!("{key}:{value}\n"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(target))]
|
|
||||||
pub async fn import_game_dir(folder_name: &str, target: ImportTarget) -> LauncherResult<()> {
|
|
||||||
let Some(root) = old_root() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let src = root.join("profiles").join(folder_name);
|
|
||||||
if !src.is_dir() {
|
|
||||||
tracing::warn!(
|
|
||||||
folder_name,
|
|
||||||
"lunar profile folder missing; nothing to import"
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let dest = match &target {
|
|
||||||
ImportTarget::Shared => paths::shared_minecraft_dir()?,
|
|
||||||
ImportTarget::Dedicated { new_cluster_id } => {
|
|
||||||
let state = crate::state::LauncherState::get()?;
|
|
||||||
let cluster = crate::clusters::ClusterManager::get(&state, *new_cluster_id).await?;
|
|
||||||
let dir = cluster.dir()?;
|
|
||||||
polyio::create_dir_all(&dir).await?;
|
|
||||||
// Mark dedicated so game_dir() resolves to this cluster's own dir.
|
|
||||||
polyio::write(cluster.dedicated_marker()?, Vec::new()).await?;
|
|
||||||
dir
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
polyio::create_dir_all(&dest).await?;
|
|
||||||
copy_tree(&src, &dest, IMPORT_EXCLUDE_TOP).await?;
|
|
||||||
merge_lunar_options(&src, &dest).await?;
|
|
||||||
|
|
||||||
if let Some(mods) = active_profile_mods(&root).await {
|
|
||||||
super::lunar_mods::apply(&mods, &dest).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mod_ids_normalize_across_spellings() {
|
|
||||||
assert_eq!(
|
|
||||||
normalize_mod_id("MOTION_BLUR"),
|
|
||||||
normalize_mod_id("motionBlur")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
normalize_mod_id("TOGGLE_SNEAK"),
|
|
||||||
normalize_mod_id("toggleSneak")
|
|
||||||
);
|
|
||||||
assert_eq!(normalize_mod_id("HIT_COLOR"), normalize_mod_id("hitColor"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn known_mods_map_to_bundle_categories() {
|
|
||||||
assert_eq!(category_for_mod(&normalize_mod_id("FPS")), Some("HUD"));
|
|
||||||
assert_eq!(category_for_mod(&normalize_mod_id("HITBOX")), Some("PvP"));
|
|
||||||
assert_eq!(
|
|
||||||
category_for_mod(&normalize_mod_id("motionBlur")),
|
|
||||||
Some("QoL")
|
|
||||||
);
|
|
||||||
assert_eq!(category_for_mod(&normalize_mod_id("ZOOM")), Some("Utility"));
|
|
||||||
assert_eq!(
|
|
||||||
category_for_mod(&normalize_mod_id("SKYBLOCK")),
|
|
||||||
Some("SkyBlock")
|
|
||||||
);
|
|
||||||
assert_eq!(category_for_mod(&normalize_mod_id("TOTALLY_UNKNOWN")), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ichor_only_profile_falls_back_to_vanilla() {
|
|
||||||
assert_eq!(loader_from_json(r#"["ichor"]"#), GameLoader::Vanilla);
|
|
||||||
assert_eq!(
|
|
||||||
loader_from_json(r#"["ichor","fabric"]"#),
|
|
||||||
GameLoader::Fabric
|
|
||||||
);
|
|
||||||
assert_eq!(loader_from_json("not json"), GameLoader::Vanilla);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lunar_options_overwrite_matching_keys_and_keep_the_rest() {
|
|
||||||
let existing = "fov:0.0\nrenderDistance:12\nkeep:me\n";
|
|
||||||
let mut lunar = serde_json::Map::new();
|
|
||||||
lunar.insert("fov".into(), serde_json::json!("0.5"));
|
|
||||||
lunar.insert("maxFps".into(), serde_json::json!("170"));
|
|
||||||
lunar.insert("lastLaunchedVersion".into(), serde_json::json!("v1_8"));
|
|
||||||
|
|
||||||
let merged = merge_options_text(existing, &lunar);
|
|
||||||
|
|
||||||
assert!(merged.contains("fov:0.5"));
|
|
||||||
assert!(!merged.contains("fov:0.0"));
|
|
||||||
assert!(merged.contains("renderDistance:12"));
|
|
||||||
assert!(merged.contains("keep:me"));
|
|
||||||
assert!(merged.contains("maxFps:170"));
|
|
||||||
// Lunar bookkeeping is not a vanilla option.
|
|
||||||
assert!(!merged.contains("lastLaunchedVersion"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn allocated_memory_and_resolution_land_on_the_global_profile() {
|
|
||||||
let mut global = GameSettingsProfile {
|
|
||||||
name: "Global".to_string(),
|
|
||||||
java_path: None,
|
|
||||||
resolution: None,
|
|
||||||
force_fullscreen: None,
|
|
||||||
mem_max: None,
|
|
||||||
launch_args: None,
|
|
||||||
launch_env: None,
|
|
||||||
hook_pre: None,
|
|
||||||
hook_wrapper: None,
|
|
||||||
hook_post: None,
|
|
||||||
os_extra: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
apply_lunar_settings(
|
|
||||||
&mut global,
|
|
||||||
&LunarLauncherSettings {
|
|
||||||
allocated_memory: Some(4195),
|
|
||||||
game_resolution: Some(LunarResolution {
|
|
||||||
width: Some(1280),
|
|
||||||
height: Some(720),
|
|
||||||
fullscreen: Some(true),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(global.mem_max, Some(4195));
|
|
||||||
assert_eq!(global.resolution, Some(Resolution::new(1280, 720)));
|
|
||||||
assert_eq!(global.force_fullscreen, Some(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use serde_json::{Map, Value, json};
|
|
||||||
|
|
||||||
use crate::LauncherResult;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub(super) struct ConfigPatch {
|
|
||||||
pub path: &'static str,
|
|
||||||
pub values: Map<String, Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lunar writes numbers as either JSON numbers or strings ("6847" vs 6329)
|
|
||||||
/// depending on how the value was last touched, so both have to be accepted.
|
|
||||||
fn loose_i64(value: Option<&Value>) -> Option<i64> {
|
|
||||||
match value? {
|
|
||||||
Value::Number(n) => n.as_i64(),
|
|
||||||
Value::String(s) => s.trim().parse().ok().or_else(|| {
|
|
||||||
// "0.0"-style values still carry an integer meaning.
|
|
||||||
s.trim().parse::<f64>().ok().map(|f| f as i64)
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn loose_bool(value: Option<&Value>) -> Option<bool> {
|
|
||||||
match value? {
|
|
||||||
Value::Bool(b) => Some(*b),
|
|
||||||
Value::String(s) => match s.trim() {
|
|
||||||
"true" => Some(true),
|
|
||||||
"false" => Some(false),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LunarMod<'a>(&'a Map<String, Value>);
|
|
||||||
|
|
||||||
impl<'a> LunarMod<'a> {
|
|
||||||
fn enabled(&self) -> bool {
|
|
||||||
self.0.get("enabled").and_then(Value::as_bool).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn option(&self, key: &str) -> Option<&'a Value> {
|
|
||||||
self.0.get("options")?.as_object()?.get(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lunar_mod<'a>(mods: &'a Map<String, Value>, id: &str) -> Option<LunarMod<'a>> {
|
|
||||||
Some(LunarMod(mods.get(id)?.as_object()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn translate(mods: &Map<String, Value>) -> Vec<ConfigPatch> {
|
|
||||||
let mut patches = Vec::new();
|
|
||||||
|
|
||||||
if let Some(m) = lunar_mod(mods, "TIME_CHANGER") {
|
|
||||||
let mut values = Map::new();
|
|
||||||
values.insert("isEnabled".into(), json!(m.enabled()));
|
|
||||||
if let Some(time) = loose_i64(m.option("timeChangerTime")) {
|
|
||||||
values.insert("time".into(), json!(time));
|
|
||||||
}
|
|
||||||
patches.push(ConfigPatch {
|
|
||||||
path: "config/polytime.json",
|
|
||||||
values,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(m) = lunar_mod(mods, "WEATHER_CHANGER") {
|
|
||||||
patches.push(ConfigPatch {
|
|
||||||
path: "config/polyweather.json",
|
|
||||||
values: [("isEnabled".to_string(), json!(m.enabled()))]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(m) = lunar_mod(mods, "HURT_CAM") {
|
|
||||||
let suppressed = m.enabled() && loose_bool(m.option("disableHurtCam")).unwrap_or(false);
|
|
||||||
if suppressed {
|
|
||||||
patches.push(ConfigPatch {
|
|
||||||
path: "config/shaketweaks.json",
|
|
||||||
values: [
|
|
||||||
("disableScreenDamage".to_string(), json!(true)),
|
|
||||||
("disableHandDamage".to_string(), json!(true)),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(m) = lunar_mod(mods, "ZOOM")
|
|
||||||
&& let Some(variable) = loose_bool(m.option("variableZoom"))
|
|
||||||
{
|
|
||||||
patches.push(ConfigPatch {
|
|
||||||
path: "config/zoomify.json",
|
|
||||||
values: [("scrollZoom".to_string(), json!(variable))]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
patches
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(values))]
|
|
||||||
async fn merge_into(path: &Path, values: &Map<String, Value>) -> LauncherResult<()> {
|
|
||||||
let mut existing: Map<String, Value> = match polyio::read(path).await {
|
|
||||||
Ok(raw) => serde_json::from_slice(&raw).unwrap_or_else(|err| {
|
|
||||||
tracing::warn!(path = %path.display(), error = %err, "unparsable mod config; rewriting");
|
|
||||||
Map::new()
|
|
||||||
}),
|
|
||||||
Err(_) => Map::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (key, value) in values {
|
|
||||||
existing.insert(key.clone(), value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
polyio::create_dir_all(parent).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendered = serde_json::to_vec_pretty(&Value::Object(existing))?;
|
|
||||||
polyio::write(path, rendered).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(mods))]
|
|
||||||
pub(super) async fn apply(mods: &Map<String, Value>, dest: &Path) -> LauncherResult<()> {
|
|
||||||
let patches = translate(mods);
|
|
||||||
|
|
||||||
for patch in &patches {
|
|
||||||
merge_into(&dest.join(patch.path), &patch.values).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(configs = patches.len(), "lunar migration: wrote mod settings");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn mods(raw: serde_json::Value) -> Map<String, Value> {
|
|
||||||
raw.as_object().expect("object").clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn patch<'a>(patches: &'a [ConfigPatch], path: &str) -> Option<&'a ConfigPatch> {
|
|
||||||
patches.iter().find(|p| p.path == path)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn time_changer_carries_enabled_state_and_tick() {
|
|
||||||
// Lunar writes this tick as a string in some profiles.
|
|
||||||
let patches = translate(&mods(json!({
|
|
||||||
"TIME_CHANGER": { "enabled": true, "options": { "timeChangerTime": "6847" } }
|
|
||||||
})));
|
|
||||||
|
|
||||||
let p = patch(&patches, "config/polytime.json").expect("polytime");
|
|
||||||
assert_eq!(p.values["isEnabled"], json!(true));
|
|
||||||
assert_eq!(p.values["time"], json!(6847));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn numeric_and_string_ticks_agree() {
|
|
||||||
let as_number = translate(&mods(json!({
|
|
||||||
"TIME_CHANGER": { "options": { "timeChangerTime": 6329 } }
|
|
||||||
})));
|
|
||||||
let as_string = translate(&mods(json!({
|
|
||||||
"TIME_CHANGER": { "options": { "timeChangerTime": "6329" } }
|
|
||||||
})));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
patch(&as_number, "config/polytime.json").unwrap().values["time"],
|
|
||||||
patch(&as_string, "config/polytime.json").unwrap().values["time"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_mod_lunar_never_enabled_is_carried_over_as_disabled() {
|
|
||||||
// Lunar omits `enabled` rather than writing false.
|
|
||||||
let patches = translate(&mods(json!({
|
|
||||||
"TIME_CHANGER": { "options": { "timeChangerTime": "0" } }
|
|
||||||
})));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
patch(&patches, "config/polytime.json").unwrap().values["isEnabled"],
|
|
||||||
json!(false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hurt_cam_only_suppresses_shake_when_lunar_did() {
|
|
||||||
let suppressing = translate(&mods(json!({
|
|
||||||
"HURT_CAM": { "enabled": true, "options": { "disableHurtCam": true } }
|
|
||||||
})));
|
|
||||||
let p = patch(&suppressing, "config/shaketweaks.json").expect("shaketweaks");
|
|
||||||
assert_eq!(p.values["disableScreenDamage"], json!(true));
|
|
||||||
assert_eq!(p.values["disableHandDamage"], json!(true));
|
|
||||||
|
|
||||||
// Enabled but not actually disabling the cam -> leave shaketweaks alone.
|
|
||||||
let passive = translate(&mods(json!({
|
|
||||||
"HURT_CAM": { "enabled": true, "options": { "disableHurtCam": false } }
|
|
||||||
})));
|
|
||||||
assert!(patch(&passive, "config/shaketweaks.json").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn zoom_maps_variable_zoom_to_scroll_zoom() {
|
|
||||||
let patches = translate(&mods(json!({
|
|
||||||
"ZOOM": { "options": { "variableZoom": true } }
|
|
||||||
})));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
patch(&patches, "config/zoomify.json").unwrap().values["scrollZoom"],
|
|
||||||
json!(true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_mod_with_no_equivalent_produces_nothing() {
|
|
||||||
let patches = translate(&mods(json!({
|
|
||||||
"TIER_TAGGER": { "enabled": true },
|
|
||||||
"SKYBLOCK": { "enabled": true },
|
|
||||||
})));
|
|
||||||
|
|
||||||
assert!(patches.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn merging_keeps_keys_the_patch_says_nothing_about() {
|
|
||||||
let dir = std::env::temp_dir().join(format!("oneclient-lunar-mods-{}", uuid::Uuid::new_v4()));
|
|
||||||
let path = dir.join("config/zoomify.json");
|
|
||||||
polyio::create_dir_all(path.parent().unwrap()).await.unwrap();
|
|
||||||
polyio::write(&path, br#"{"initialZoom":4,"scrollZoom":false}"#.to_vec())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let values = [("scrollZoom".to_string(), json!(true))]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
merge_into(&path, &values).await.unwrap();
|
|
||||||
|
|
||||||
let raw = polyio::read(&path).await.unwrap();
|
|
||||||
let out: Map<String, Value> = serde_json::from_slice(&raw).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(out["scrollZoom"], json!(true));
|
|
||||||
// Untouched key survives.
|
|
||||||
assert_eq!(out["initialZoom"], json!(4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
mod fs;
|
mod fs;
|
||||||
pub mod lunar_client;
|
|
||||||
mod lunar_mods;
|
|
||||||
pub mod oneclient_v1;
|
pub mod oneclient_v1;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -15,24 +13,20 @@ pub use fs::copy_tree;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub enum MigrationSource {
|
pub enum MigrationSource {
|
||||||
OneClientV1,
|
OneClientV1,
|
||||||
LunarClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MigrationSource {
|
impl MigrationSource {
|
||||||
pub const ALL: &'static [MigrationSource] =
|
pub const ALL: &'static [MigrationSource] = &[MigrationSource::OneClientV1];
|
||||||
&[MigrationSource::OneClientV1, MigrationSource::LunarClient];
|
|
||||||
|
|
||||||
pub fn id(self) -> &'static str {
|
pub fn id(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
MigrationSource::OneClientV1 => "oneclient_v1",
|
MigrationSource::OneClientV1 => "oneclient_v1",
|
||||||
MigrationSource::LunarClient => "lunar_client",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_name(self) -> &'static str {
|
pub fn display_name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
MigrationSource::OneClientV1 => "OneClient",
|
MigrationSource::OneClientV1 => "OneClient",
|
||||||
MigrationSource::LunarClient => "Lunar Client",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,24 +65,20 @@ pub enum ImportTarget {
|
||||||
Dedicated { new_cluster_id: i64 },
|
Dedicated { new_cluster_id: i64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug")]
|
#[tracing::instrument]
|
||||||
pub async fn detect_all() -> LauncherResult<Vec<MigrationDetection>> {
|
pub async fn detect() -> LauncherResult<Option<MigrationDetection>> {
|
||||||
let mut detections = Vec::new();
|
|
||||||
|
|
||||||
for source in MigrationSource::ALL.iter().copied() {
|
for source in MigrationSource::ALL.iter().copied() {
|
||||||
let detection = match source {
|
let detection = match source {
|
||||||
MigrationSource::OneClientV1 => oneclient_v1::detect().await?,
|
MigrationSource::OneClientV1 => oneclient_v1::detect().await?,
|
||||||
MigrationSource::LunarClient => lunar_client::detect().await?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(detection) = detection
|
if let Some(detection) = detection
|
||||||
&& !detection.instances.is_empty()
|
&& !detection.instances.is_empty()
|
||||||
{
|
{
|
||||||
detections.push(detection);
|
return Ok(Some(detection));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(None)
|
||||||
Ok(detections)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
|
|
@ -99,16 +89,5 @@ pub async fn import_game_dir(
|
||||||
) -> LauncherResult<()> {
|
) -> LauncherResult<()> {
|
||||||
match source {
|
match source {
|
||||||
MigrationSource::OneClientV1 => oneclient_v1::import_game_dir(folder_name, target).await,
|
MigrationSource::OneClientV1 => oneclient_v1::import_game_dir(folder_name, target).await,
|
||||||
MigrationSource::LunarClient => lunar_client::import_game_dir(folder_name, target).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copies launcher-wide settings (memory, resolution)
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn import_settings(source: MigrationSource) -> LauncherResult<()> {
|
|
||||||
match source {
|
|
||||||
// v1 shares this launcher's settings file already.
|
|
||||||
MigrationSource::OneClientV1 => Ok(()),
|
|
||||||
MigrationSource::LunarClient => lunar_client::import_settings().await,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue