use crate::data::{Dependency, User, Version}; use crate::jre::check_jre; use crate::prelude::ModLoader; use crate::state; use crate::state::{ CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate, Credentials, DefaultPage, DependencyType, DeviceToken, DeviceTokenKey, DeviceTokenPair, FileType, Hooks, LinkedData, MemorySettings, ModrinthCredentials, Profile, ProfileInstallStage, TeamMember, Theme, VersionFile, 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 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(java_version)) = check_jre(PathBuf::from(legacy_version.path)).await { 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?; } } } } let mut cached_entries = vec![]; if let Ok(profiles_dir) = std::fs::read_dir( legacy_settings .loaded_config_dir .clone() .unwrap_or_else(|| old_launcher_root.clone()) .join("profiles"), ) { for entry in profiles_dir.flatten() { if !entry.path().is_dir() { continue; } let profile_path = entry.path().join("profile.json"); let profile = if let Ok(profile) = read_json::(&profile_path, &io_semaphore) .await { profile } else { continue; }; for (path, project) in profile.projects { let full_path = legacy_settings .loaded_config_dir .clone() .unwrap_or_else(|| old_launcher_root.clone()) .join("profiles") .join(&profile.path) .join(&path); if !full_path.exists() || !full_path.is_file() { continue; } let sha512 = project.sha512; if let LegacyProjectMetadata::Modrinth { version, members, update_version, .. } = project.metadata { if let Some(file) = version .files .iter() .find(|x| x.hashes.get("sha512") == Some(&sha512)) { if let Some(sha1) = file.hashes.get("sha1") { if let Ok(metadata) = full_path.metadata() { let file_name = format!( "{}/{}", profile.path, path.replace('\\', "/") .replace(".disabled", "") ); cached_entries.push(CacheValue::FileHash( CachedFileHash { path: file_name, size: metadata.len(), hash: sha1.clone(), }, )); } cached_entries.push(CacheValue::File( CachedFile { hash: sha1.clone(), project_id: version.project_id.clone(), version_id: version.id.clone(), }, )); if let Some(update_version) = update_version { let mod_loader: ModLoader = profile.metadata.loader.into(); cached_entries.push( CacheValue::FileUpdate( CachedFileUpdate { hash: sha1.clone(), game_version: profile .metadata .game_version .clone(), loader: mod_loader .as_str() .to_string(), update_version_id: update_version.id.clone(), }, ), ); cached_entries.push(CacheValue::Version( (*update_version).into(), )); } let members = members .into_iter() .map(|x| { let user = User { id: x.user.id, username: x.user.username, avatar_url: x.user.avatar_url, bio: x.user.bio, created: x.user.created, role: x.user.role, badges: 0, }; cached_entries.push(CacheValue::User( user.clone(), )); TeamMember { team_id: x.team_id, user: user.clone(), is_owner: x.role == "Owner", role: x.role, ordering: x.ordering, } }) .collect::>(); cached_entries.push(CacheValue::Team(members)); cached_entries.push(CacheValue::Version( (*version).into(), )); } } } } 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, game_version: profile.metadata.game_version, loader: profile.metadata.loader.into(), 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) }), 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?; } } CachedEntry::upsert_many( &cached_entries .into_iter() .map(|x| { let mut entry = x.get_entry(); entry.expires = Utc::now().timestamp() - entry.type_.expiry(); entry }) .collect::>(), 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, pub username: String, // pub name: Option, pub avatar_url: Option, pub bio: Option, pub created: DateTime, pub role: 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, pub projects: HashMap, } #[derive(Deserialize, Clone, Debug)] struct LegacyProject { pub sha512: String, // pub disabled: bool, pub metadata: LegacyProjectMetadata, // pub file_name: String, } #[derive(Deserialize, Clone, Debug)] #[serde(tag = "type", rename_all = "snake_case")] enum LegacyProjectMetadata { Modrinth { // project: Box, version: Box, members: Vec, update_version: Option>, }, Inferred, Unknown, } // #[derive(Deserialize, Clone, Debug)] // struct LegacyModrinthProject { // pub id: String, // pub slug: Option, // pub project_type: String, // pub team: String, // pub title: String, // pub description: String, // pub body: String, // // pub published: DateTime, // pub updated: DateTime, // // pub client_side: LegacySideType, // pub server_side: LegacySideType, // // pub downloads: u32, // pub followers: u32, // // pub categories: Vec, // pub additional_categories: Vec, // pub game_versions: Vec, // pub loaders: Vec, // // pub versions: Vec, // // pub icon_url: Option, // } #[derive(Deserialize, Clone, Debug)] struct LegacyModrinthVersion { pub id: String, pub project_id: String, pub author_id: String, pub featured: bool, pub name: String, pub version_number: String, pub changelog: String, pub changelog_url: Option, pub date_published: DateTime, pub downloads: u32, pub version_type: String, pub files: Vec, pub dependencies: Vec, pub game_versions: Vec, pub loaders: Vec, } impl From for Version { fn from(value: LegacyModrinthVersion) -> Self { Version { id: value.id, project_id: value.project_id, author_id: value.author_id, featured: value.featured, name: value.name, version_number: value.version_number, changelog: value.changelog, changelog_url: value.changelog_url, date_published: value.date_published, downloads: value.downloads, version_type: value.version_type, files: value .files .into_iter() .map(|x| VersionFile { hashes: x.hashes, url: x.url, filename: x.filename, primary: x.primary, size: x.size, file_type: x.file_type.map(|x| match x { LegacyFileType::RequiredResourcePack => { FileType::RequiredResourcePack } LegacyFileType::OptionalResourcePack => { FileType::OptionalResourcePack } LegacyFileType::Unknown => FileType::Unknown, }), }) .collect::>(), dependencies: value .dependencies .into_iter() .map(|x| Dependency { version_id: x.version_id, project_id: x.project_id, file_name: x.file_name, dependency_type: match x.dependency_type { LegacyDependencyType::Required => { DependencyType::Required } LegacyDependencyType::Optional => { DependencyType::Optional } LegacyDependencyType::Incompatible => { DependencyType::Incompatible } LegacyDependencyType::Embedded => { DependencyType::Embedded } }, }) .collect::>(), game_versions: value.game_versions, loaders: value.loaders, } } } #[derive(Deserialize, Clone, Debug)] struct LegacyModrinthVersionFile { pub hashes: HashMap, pub url: String, pub filename: String, pub primary: bool, pub size: u32, pub file_type: Option, } #[derive(Deserialize, Clone, Debug)] struct LegacyDependency { pub version_id: Option, pub project_id: Option, pub file_name: Option, pub dependency_type: LegacyDependencyType, } #[derive(Deserialize, Clone, Debug)] struct LegacyModrinthTeamMember { pub team_id: String, pub user: LegacyModrinthUser, pub role: String, pub ordering: i64, } #[derive(Deserialize, Copy, Clone, Debug)] #[serde(rename_all = "lowercase")] enum LegacyDependencyType { Required, Optional, Incompatible, Embedded, } // #[derive(Deserialize, Clone, Debug, Eq, PartialEq)] // #[serde(rename_all = "kebab-case")] // enum LegacySideType { // Required, // Optional, // Unsupported, // Unknown, // } #[derive(Deserialize, Copy, Clone, Debug)] #[serde(rename_all = "kebab-case")] enum LegacyFileType { RequiredResourcePack, OptionalResourcePack, Unknown, } #[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, } impl From for ModLoader { fn from(value: LegacyModLoader) -> Self { match value { LegacyModLoader::Vanilla => ModLoader::Vanilla, LegacyModLoader::Forge => ModLoader::Forge, LegacyModLoader::Fabric => ModLoader::Fabric, LegacyModLoader::Quilt => ModLoader::Quilt, LegacyModLoader::NeoForge => ModLoader::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, }