use crate::data::DirectoryInfo; use crate::jre::check_jre; use crate::prelude::ModLoader; use crate::state; use crate::state::{ Credentials, DefaultPage, DeviceToken, DeviceTokenKey, DeviceTokenPair, Hooks, LinkedData, MemorySettings, ModrinthCredentials, Profile, ProfileInstallStage, Theme, WindowSize, }; use crate::util::fetch::{read_json, IoSemaphore}; use chrono::{DateTime, Utc}; use p256::ecdsa::SigningKey; use p256::pkcs8::DecodePrivateKey; use serde::Deserialize; use std::collections::HashMap; use std::path::PathBuf; use tokio::sync::Semaphore; use uuid::Uuid; pub async fn migrate_legacy_data<'a, E>(exec: E) -> crate::Result<()> where E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy, { let mut settings = state::Settings::get(exec).await?; if settings.migrated { return Ok(()); }; let old_launcher_root = if let Some(dir) = default_settings_dir() { dir } else { return Ok(()); }; let old_launcher_root_str = old_launcher_root.to_string_lossy().to_string(); let new_launcher_root = DirectoryInfo::get_initial_settings_dir().ok_or( crate::ErrorKind::FSError( "Could not find valid config dir".to_string(), ), )?; let new_launcher_root_str = new_launcher_root .to_string_lossy() .to_string() .trim_end_matches('/') .trim_end_matches('\\') .to_string(); let io_semaphore = IoSemaphore(Semaphore::new(10)); let settings_path = old_launcher_root.join("settings.json"); if let Ok(legacy_settings) = read_json::(&settings_path, &io_semaphore).await { settings.max_concurrent_writes = legacy_settings.max_concurrent_writes; settings.max_concurrent_downloads = legacy_settings.max_concurrent_downloads; settings.theme = match legacy_settings.theme { LegacyTheme::Dark => Theme::Dark, LegacyTheme::Light => Theme::Light, LegacyTheme::Oled => Theme::Oled, }; settings.default_page = match legacy_settings.default_page { LegacyDefaultPage::Home => DefaultPage::Home, LegacyDefaultPage::Library => DefaultPage::Library, }; settings.collapsed_navigation = legacy_settings.collapsed_navigation; settings.advanced_rendering = legacy_settings.advanced_rendering; settings.native_decorations = legacy_settings.native_decorations; settings.telemetry = !legacy_settings.opt_out_analytics; settings.discord_rpc = !legacy_settings.disable_discord_rpc; settings.developer_mode = legacy_settings.developer_mode; settings.onboarded = legacy_settings.fully_onboarded; settings.extra_launch_args = legacy_settings.custom_java_args; settings.custom_env_vars = legacy_settings.custom_env_args; settings.memory.maximum = legacy_settings.memory.maximum; settings.force_fullscreen = legacy_settings.force_fullscreen; settings.game_resolution.0 = legacy_settings.game_resolution.0; settings.game_resolution.1 = legacy_settings.game_resolution.1; settings.hide_on_process_start = legacy_settings.hide_on_process; settings.hooks.pre_launch = legacy_settings.hooks.pre_launch; settings.hooks.wrapper = legacy_settings.hooks.wrapper; settings.hooks.post_exit = legacy_settings.hooks.post_exit; if let Some(path) = legacy_settings .loaded_config_dir .clone() .and_then(|x| x.to_str().map(|x| x.to_string())) { if path != old_launcher_root_str { settings.custom_dir = Some(path); } } settings.prev_custom_dir = Some(old_launcher_root_str.clone()); for (_, legacy_version) in legacy_settings.java_globals.0 { if let Ok(Some(mut java_version)) = check_jre(PathBuf::from(legacy_version.path)).await { java_version.path = java_version .path .replace(&old_launcher_root_str, &new_launcher_root_str); java_version.upsert(exec).await?; } } let modrinth_auth_path = old_launcher_root.join("caches/metadata/auth.json"); if let Ok(creds) = read_json::( &modrinth_auth_path, &io_semaphore, ) .await { ModrinthCredentials { session: creds.session, expires: creds.expires_at, user_id: creds.user.id, active: true, } .upsert(exec) .await?; } let minecraft_auth_path = old_launcher_root.join("caches/metadata/minecraft_auth.json"); if let Ok(minecraft_auth) = read_json::( &minecraft_auth_path, &io_semaphore, ) .await { let minecraft_users_len = minecraft_auth.users.len(); for (uuid, credential) in minecraft_auth.users { Credentials { id: credential.id, username: credential.username, access_token: credential.access_token, refresh_token: credential.refresh_token, expires: credential.expires, active: minecraft_auth.default_user == Some(uuid) || minecraft_users_len == 1, } .upsert(exec) .await?; } if let Some(device_token) = minecraft_auth.token { if let Ok(private_key) = SigningKey::from_pkcs8_pem(&device_token.private_key) { if let Ok(uuid) = Uuid::parse_str(&device_token.id) { DeviceTokenPair { token: DeviceToken { issue_instant: device_token.token.issue_instant, not_after: device_token.token.not_after, token: device_token.token.token, display_claims: device_token .token .display_claims, }, key: DeviceTokenKey { id: uuid, key: private_key, x: device_token.x, y: device_token.y, }, } .upsert(exec) .await?; } } } } if let Ok(profiles_dir) = std::fs::read_dir( &legacy_settings .loaded_config_dir .unwrap_or(old_launcher_root) .join("profiles"), ) { for entry in profiles_dir.flatten() { if entry.path().is_dir() { let profile_path = entry.path().join("profile.json"); if let Ok(profile) = read_json::(&profile_path, &io_semaphore) .await { Profile { path: profile.path, install_stage: match profile.install_stage { LegacyProfileInstallStage::Installed => { ProfileInstallStage::Installed } LegacyProfileInstallStage::Installing => { ProfileInstallStage::Installing } LegacyProfileInstallStage::PackInstalling => { ProfileInstallStage::PackInstalling } LegacyProfileInstallStage::NotInstalled => { ProfileInstallStage::NotInstalled } }, name: profile.metadata.name, icon_path: profile.metadata.icon.map(|x| { x.replace( &old_launcher_root_str, &new_launcher_root_str, ) }), game_version: profile.metadata.game_version, loader: match profile.metadata.loader { LegacyModLoader::Vanilla => ModLoader::Vanilla, LegacyModLoader::Forge => ModLoader::Forge, LegacyModLoader::Fabric => ModLoader::Fabric, LegacyModLoader::Quilt => ModLoader::Quilt, LegacyModLoader::NeoForge => { ModLoader::NeoForge } }, loader_version: profile .metadata .loader_version .map(|x| x.id), groups: profile.metadata.groups, linked_data: profile.metadata.linked_data.and_then( |x| { if let Some(project_id) = x.project_id { if let Some(version_id) = x.version_id { if let Some(locked) = x.locked { return Some(LinkedData { project_id, version_id, locked, }); } } } None }, ), created: profile.metadata.date_created, modified: profile.metadata.date_modified, last_played: profile.metadata.last_played, submitted_time_played: profile .metadata .submitted_time_played, recent_time_played: profile .metadata .recent_time_played, java_path: profile.java.as_ref().and_then(|x| { x.override_version.clone().map(|x| { x.path.replace( &old_launcher_root_str, &new_launcher_root_str, ) }) }), extra_launch_args: profile .java .as_ref() .and_then(|x| x.extra_arguments.clone()), custom_env_vars: profile .java .and_then(|x| x.custom_env_args), memory: profile .memory .map(|x| MemorySettings { maximum: x.maximum }), force_fullscreen: profile.fullscreen, game_resolution: profile .resolution .map(|x| WindowSize(x.0, x.1)), hooks: Hooks { pre_launch: profile .hooks .as_ref() .and_then(|x| x.pre_launch.clone()), wrapper: profile .hooks .as_ref() .and_then(|x| x.wrapper.clone()), post_exit: profile .hooks .and_then(|x| x.post_exit), }, } .upsert(exec) .await?; } } } } settings.migrated = true; settings.update(exec).await?; } Ok(()) } #[derive(Deserialize, Debug, Clone)] struct LegacySettings { pub theme: LegacyTheme, pub memory: LegacyMemorySettings, #[serde(default)] pub force_fullscreen: bool, pub game_resolution: LegacyWindowSize, pub custom_java_args: Vec, pub custom_env_args: Vec<(String, String)>, pub java_globals: LegacyJavaGlobals, pub hooks: LegacyHooks, pub max_concurrent_downloads: usize, pub max_concurrent_writes: usize, pub collapsed_navigation: bool, #[serde(default)] pub disable_discord_rpc: bool, #[serde(default)] pub hide_on_process: bool, #[serde(default)] pub native_decorations: bool, #[serde(default)] pub default_page: LegacyDefaultPage, #[serde(default)] pub developer_mode: bool, #[serde(default)] pub opt_out_analytics: bool, #[serde(default)] pub advanced_rendering: bool, #[serde(default)] pub fully_onboarded: bool, #[serde(default = "default_settings_dir")] pub loaded_config_dir: Option, } fn default_settings_dir() -> Option { Some(dirs::config_dir()?.join("com.modrinth.theseus")) } #[derive(Debug, Clone, Copy, Deserialize)] #[serde(rename_all = "snake_case")] pub enum LegacyTheme { Dark, Light, Oled, } #[derive(Deserialize, Default, Debug, Clone, Copy)] enum LegacyDefaultPage { #[default] Home, Library, } #[derive(Deserialize, Debug, Clone)] struct LegacyHooks { pub pre_launch: Option, pub wrapper: Option, pub post_exit: Option, } #[derive(Deserialize, Debug, Clone, Copy)] struct LegacyMemorySettings { pub maximum: u32, } #[derive(Deserialize, Debug, Clone, Copy)] struct LegacyWindowSize(pub u16, pub u16); #[derive(Debug, Deserialize, Clone)] struct LegacyJavaGlobals(HashMap); #[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)] struct LegacyJavaVersion { pub path: String, pub version: String, pub architecture: String, } #[derive(Deserialize, Clone, Debug)] struct LegacyModrinthUser { pub id: String, } #[derive(Deserialize, Clone, Debug)] struct LegacyModrinthCredentials { pub session: String, pub expires_at: DateTime, pub user: LegacyModrinthUser, } #[derive(Deserialize, Debug)] struct LegacyMinecraftAuthStore { pub users: HashMap, pub token: Option, pub default_user: Option, } #[derive(Deserialize, Clone, Debug)] struct LegacyCredentials { pub id: Uuid, pub username: String, pub access_token: String, pub refresh_token: String, pub expires: DateTime, } #[derive(Deserialize, Debug)] struct LegacySaveDeviceToken { pub id: String, pub private_key: String, pub x: String, pub y: String, pub token: LegacyDeviceToken, } #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "PascalCase")] struct LegacyDeviceToken { pub issue_instant: DateTime, pub not_after: DateTime, pub token: String, pub display_claims: HashMap, } #[derive(Deserialize, Clone, Debug)] struct LegacyProfile { #[serde(default)] pub install_stage: LegacyProfileInstallStage, #[serde(default)] pub path: String, pub metadata: LegacyProfileMetadata, pub java: Option, pub memory: Option, pub resolution: Option, pub fullscreen: Option, pub hooks: Option, } #[derive(Deserialize, Clone, Debug)] struct LegacyProfileMetadata { pub name: String, pub icon: Option, #[serde(default)] pub groups: Vec, pub game_version: String, #[serde(default)] pub loader: LegacyModLoader, pub loader_version: Option, pub linked_data: Option, #[serde(default)] pub date_created: DateTime, #[serde(default)] pub date_modified: DateTime, pub last_played: Option>, #[serde(default)] pub submitted_time_played: u64, #[serde(default)] pub recent_time_played: u64, } #[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Default)] #[serde(rename_all = "lowercase")] enum LegacyModLoader { #[default] Vanilla, Forge, Fabric, Quilt, NeoForge, } #[derive(Deserialize, Clone, Debug)] struct LegacyLinkedData { pub project_id: Option, pub version_id: Option, #[serde(default = "default_locked")] pub locked: Option, } fn default_locked() -> Option { Some(true) } #[derive(Deserialize, Clone, Debug)] struct LegacyJavaSettings { pub override_version: Option, pub extra_arguments: Option>, pub custom_env_args: Option>, } #[derive(Deserialize, Clone, Debug)] struct LegacyLoaderVersion { pub id: String, } #[derive(Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)] #[serde(rename_all = "snake_case")] enum LegacyProfileInstallStage { Installed, Installing, PackInstalling, #[default] NotInstalled, }