use crate::api::Result; use dashmap::DashMap; use path_util::SafeRelativeUtf8UnixPathBuf; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use theseus::DownloadReason; use theseus::data::{ AppliedContentSetPatch, ContentItem, Dependency, EditInstance as CoreEditInstance, InstanceInstallCandidate, InstanceInstallTarget, InstanceLaunchOverridesPatch, InstanceLink as CoreInstanceLink, InstanceMetadata, LinkedModpackInfo, SharedInstanceAttachment as CoreSharedInstanceAttachment, SharedInstanceRole, }; use theseus::instance::InstallProjectWithDependenciesRequest; use theseus::instance::QuickPlayType; use theseus::prelude::*; use theseus::server_address::ServerAddress; pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("instance") .invoke_handler(tauri::generate_handler![ instance_remove, instance_get, instance_get_many, instance_list, instance_get_projects, instance_get_installed_project_ids, instance_get_install_candidates, instance_content, instance_get_content_items, instance_refresh_content_updates, instance_get_dependencies_as_content_items, instance_get_linked_modpack_info, instance_get_linked_modpack_content, instance_get_optimal_jre_key, instance_get_full_path, instance_get_mod_full_path, instance_check_installed, instance_update_all, instance_update_project, instance_add_project_from_version, instance_install_project_with_dependencies, instance_switch_project_version_with_dependencies, instance_add_project_from_path, instance_is_file_on_modrinth, instance_toggle_disable_project, instance_set_project_locked, instance_remove_project, instance_update_managed_modrinth_version, instance_repair_managed_modrinth, instance_run, instance_kill, instance_edit, instance_edit_icon, instance_share_can_current_user_use, instance_share_get_users, instance_share_invite_users, instance_share_create_invite_link, instance_share_get_invites, instance_share_revoke_invite, instance_share_remove_users, instance_share_get_publish_preview, instance_share_publish, instance_share_unlink, instance_share_unpublish, instance_export_mrpack, instance_get_pack_export_candidates, ]) .build() } #[derive(Serialize, Debug, Clone)] pub struct Instance { pub id: String, pub path: String, pub install_stage: String, pub launcher_feature_version: String, pub name: String, pub icon_path: Option, pub game_version: String, pub protocol_version: Option, pub loader: ModLoader, pub loader_version: Option, pub groups: Vec, pub link: Option, pub shared_instance: Option, pub quarantined: bool, pub update_channel: ReleaseChannel, pub created: chrono::DateTime, pub modified: chrono::DateTime, pub last_played: Option>, pub submitted_time_played: u64, pub recent_time_played: u64, pub java_path: Option, pub extra_launch_args: Option>, pub custom_env_vars: Option>, pub memory: Option, pub force_fullscreen: Option, pub game_resolution: Option, pub hooks: Hooks, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum InstanceLink { ModrinthModpack { project_id: String, version_id: String, }, ServerProject { project_id: String, }, ServerProjectModpack { server_project_id: String, content_project_id: Option, content_version_id: String, project_id: Option, version_id: Option, }, ImportedModpack { project_id: Option, version_id: Option, name: Option, version_number: Option, filename: Option, }, ModrinthHosting { server_id: String, instance_ids: Vec, active_instance_id: Option, }, SharedInstance { modpack_project_id: Option, modpack_version_id: Option, }, } #[derive(Serialize, Debug, Clone)] pub struct SharedInstanceAttachment { pub id: String, pub role: SharedInstanceRole, pub manager_id: Option, pub server_manager_name: Option, pub server_manager_icon_url: Option, pub linked_user_id: Option, pub status: String, pub applied_version: Option, pub latest_version: Option, } impl From for SharedInstanceAttachment { fn from(attachment: CoreSharedInstanceAttachment) -> Self { Self { id: attachment.id.to_string(), role: attachment.role, manager_id: attachment.manager_id, server_manager_name: attachment.server_manager_name, server_manager_icon_url: attachment.server_manager_icon_url, linked_user_id: attachment.linked_user_id, status: attachment.status.as_str().to_string(), applied_version: attachment.applied_version, latest_version: attachment.latest_version, } } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct EditInstance { pub name: Option, pub game_version: Option, pub loader: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub loader_version: Option>, pub groups: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub link: Option>, pub update_channel: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub java_path: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub extra_launch_args: Option>>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub custom_env_vars: Option>>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub memory: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub force_fullscreen: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub game_resolution: Option>, pub hooks: Option, } impl From for Instance { fn from(metadata: InstanceMetadata) -> Self { Self { id: metadata.instance.id, path: metadata.instance.path, install_stage: metadata.instance.install_stage.as_str().to_string(), launcher_feature_version: metadata .instance .launcher_feature_version .as_str() .to_string(), name: metadata.instance.name, icon_path: metadata.instance.icon_path, game_version: metadata.applied_content_set.game_version, protocol_version: metadata.applied_content_set.protocol_version, loader: metadata.applied_content_set.loader, loader_version: metadata.applied_content_set.loader_version, groups: metadata.groups, link: InstanceLink::from_core(metadata.link), shared_instance: metadata.shared_instance.map(Into::into), quarantined: metadata.quarantined, update_channel: metadata.instance.update_channel, created: metadata.instance.created, modified: metadata.instance.modified, last_played: metadata.instance.last_played, submitted_time_played: metadata.instance.submitted_time_played, recent_time_played: metadata.instance.recent_time_played, java_path: metadata.launch_overrides.java_path, extra_launch_args: metadata.launch_overrides.extra_launch_args, custom_env_vars: metadata.launch_overrides.custom_env_vars, memory: metadata.launch_overrides.memory, force_fullscreen: metadata.launch_overrides.force_fullscreen, game_resolution: metadata.launch_overrides.game_resolution, hooks: metadata.launch_overrides.hooks, } } } impl InstanceLink { fn from_core(link: CoreInstanceLink) -> Option { match link { CoreInstanceLink::Unmanaged => None, CoreInstanceLink::ModrinthModpack { project_id, version_id, } => Some(Self::ModrinthModpack { project_id, version_id, }), CoreInstanceLink::ServerProject { project_id } => { Some(Self::ServerProject { project_id }) } CoreInstanceLink::ServerProjectModpack { server_project_id, content_project_id, content_version_id, } => Some(Self::ServerProjectModpack { project_id: Some(server_project_id.clone()), version_id: Some(content_version_id.clone()), server_project_id, content_project_id: Some(content_project_id), content_version_id, }), CoreInstanceLink::ImportedModpack { project_id, version_id, name, version_number, filename, } => Some(Self::ImportedModpack { project_id, version_id, name, version_number, filename, }), CoreInstanceLink::ModrinthHosting { server_id, instance_ids, active_instance_id, } => Some(Self::ModrinthHosting { server_id: server_id.to_string(), instance_ids: instance_ids .into_iter() .map(|id| id.to_string()) .collect(), active_instance_id: active_instance_id.map(|id| id.to_string()), }), CoreInstanceLink::SharedInstance { modpack_project_id, modpack_version_id, } => Some(Self::SharedInstance { modpack_project_id, modpack_version_id, }), } } pub(crate) fn into_core(self) -> Result { match self { Self::ModrinthModpack { project_id, version_id, } => Ok(CoreInstanceLink::ModrinthModpack { project_id, version_id, }), Self::ServerProject { project_id } => { Ok(CoreInstanceLink::ServerProject { project_id }) } Self::ServerProjectModpack { server_project_id, content_project_id, content_version_id, .. } => Ok(CoreInstanceLink::ServerProjectModpack { server_project_id, content_project_id: content_project_id.unwrap_or_default(), content_version_id, }), Self::ImportedModpack { project_id, version_id, name, version_number, filename, } => Ok(CoreInstanceLink::ImportedModpack { project_id, version_id, name, version_number, filename, }), Self::ModrinthHosting { server_id, instance_ids, active_instance_id, } => Ok(CoreInstanceLink::ModrinthHosting { server_id: server_id.parse().map_err(|err| { theseus::Error::from(theseus::ErrorKind::InputError( format!("Invalid server id: {err}"), )) })?, instance_ids: instance_ids .into_iter() .map(|id| { id.parse().map_err(|err| { theseus::Error::from( theseus::ErrorKind::InputError(format!( "Invalid hosted instance id: {err}" )), ) }) }) .collect::, _>>()?, active_instance_id: active_instance_id .map(|id| { id.parse().map_err(|err| { theseus::Error::from( theseus::ErrorKind::InputError(format!( "Invalid active instance id: {err}" )), ) }) }) .transpose()?, }), Self::SharedInstance { modpack_project_id, modpack_version_id, } => Ok(CoreInstanceLink::SharedInstance { modpack_project_id, modpack_version_id, }), } } } fn edit_to_core(edit_instance: EditInstance) -> Result { Ok(CoreEditInstance { install_stage: None, launcher_feature_version: None, name: edit_instance.name, icon_path: None, update_channel: edit_instance.update_channel, groups: edit_instance.groups, link: edit_instance .link .map(|link| match link { Some(link) => link.into_core(), None => Ok(CoreInstanceLink::Unmanaged), }) .transpose()?, launch_overrides: Some(InstanceLaunchOverridesPatch { java_path: edit_instance.java_path, extra_launch_args: edit_instance.extra_launch_args, custom_env_vars: edit_instance.custom_env_vars, memory: edit_instance.memory, force_fullscreen: edit_instance.force_fullscreen, game_resolution: edit_instance.game_resolution, hooks: edit_instance.hooks, }), content_set_patch: Some(AppliedContentSetPatch { source_kind: None, game_version: edit_instance.game_version, protocol_version: Some(None), loader: edit_instance.loader, loader_version: edit_instance.loader_version, }), last_played: None, submitted_time_played: None, recent_time_played: None, }) } #[tauri::command] pub async fn instance_remove(instance_id: &str) -> Result<()> { theseus::instance::remove(instance_id).await?; Ok(()) } #[tauri::command] pub async fn instance_get(instance_id: &str) -> Result> { Ok(theseus::instance::get(instance_id) .await? .map(Instance::from)) } #[tauri::command] pub async fn instance_get_many( instance_ids: Vec, ) -> Result> { let ids = instance_ids.iter().map(|x| &**x).collect::>(); Ok(theseus::instance::get_many(&ids) .await? .into_iter() .map(Instance::from) .collect()) } #[tauri::command] pub async fn instance_list() -> Result> { Ok(theseus::instance::list() .await? .into_iter() .map(Instance::from) .collect()) } #[tauri::command] pub async fn instance_get_projects( instance_id: &str, cache_behaviour: Option, ) -> Result> { Ok(theseus::instance::get_projects(instance_id, cache_behaviour).await?) } #[tauri::command] pub async fn instance_get_installed_project_ids( instance_id: &str, ) -> Result> { Ok(theseus::instance::get_installed_project_ids(instance_id).await?) } #[tauri::command] pub async fn instance_get_install_candidates( project_id: &str, project_type: ProjectType, targets: Vec, ) -> Result> { Ok(theseus::instance::get_install_candidates( project_id, project_type, targets, ) .await?) } #[tauri::command] pub async fn instance_content( instance_id: &str, cache_behaviour: Option, ) -> Result> { instance_get_content_items(instance_id, cache_behaviour).await } #[tauri::command] pub async fn instance_get_content_items( instance_id: &str, cache_behaviour: Option, ) -> Result> { Ok( theseus::instance::get_content_items(instance_id, cache_behaviour) .await?, ) } #[tauri::command] pub async fn instance_refresh_content_updates(instance_id: &str) -> Result<()> { Ok(theseus::instance::refresh_content_updates(instance_id).await?) } #[tauri::command] pub async fn instance_get_dependencies_as_content_items( dependencies: Vec, cache_behaviour: Option, ) -> Result> { Ok(theseus::instance::get_dependencies_as_content_items( dependencies, cache_behaviour, ) .await?) } #[tauri::command] pub async fn instance_get_linked_modpack_info( instance_id: &str, cache_behaviour: Option, ) -> Result> { Ok( theseus::instance::get_linked_modpack_info( instance_id, cache_behaviour, ) .await?, ) } #[tauri::command] pub async fn instance_get_linked_modpack_content( instance_id: &str, cache_behaviour: Option, ) -> Result> { Ok(theseus::instance::get_linked_modpack_content( instance_id, cache_behaviour, ) .await?) } #[tauri::command] pub async fn instance_get_full_path(instance_id: &str) -> Result { Ok(theseus::instance::get_full_path(instance_id).await?) } #[tauri::command] pub async fn instance_get_mod_full_path( instance_id: &str, project_path: &str, ) -> Result { Ok(theseus::instance::get_mod_full_path(instance_id, project_path).await?) } #[tauri::command] pub async fn instance_get_optimal_jre_key( instance_id: &str, ) -> Result> { Ok(theseus::instance::get_optimal_jre_key(instance_id).await?) } #[tauri::command] pub async fn instance_check_installed( instance_id: &str, project_id: &str, ) -> Result { let check_project_id = project_id; if let Ok(projects) = theseus::instance::get_projects(instance_id, None).await { Ok(projects.into_iter().any(|(_, project)| { project .metadata .as_ref() .is_some_and(|metadata| check_project_id == metadata.project_id) })) } else { Ok(false) } } #[tauri::command] pub async fn instance_update_all( instance_id: &str, ) -> Result> { Ok(theseus::instance::update_all_projects(instance_id).await?) } #[tauri::command] pub async fn instance_update_project( instance_id: &str, project_path: &str, ) -> Result { Ok( theseus::instance::update_project(instance_id, project_path, None) .await?, ) } #[tauri::command] pub async fn instance_add_project_from_version( instance_id: &str, version_id: &str, reason: DownloadReason, dependent_on_version_id: Option, ) -> Result { Ok(theseus::instance::add_project_from_version( instance_id, version_id, reason, dependent_on_version_id, ) .await?) } #[tauri::command] pub async fn instance_install_project_with_dependencies( instance_id: &str, request: InstallProjectWithDependenciesRequest, ) -> Result { Ok(theseus::instance::install_project_with_dependencies( instance_id, request, ) .await?) } #[tauri::command] pub async fn instance_switch_project_version_with_dependencies( instance_id: &str, project_path: &str, version_id: &str, ) -> Result { Ok(theseus::instance::switch_project_version_with_dependencies( instance_id, project_path, version_id, ) .await?) } #[tauri::command] pub async fn instance_add_project_from_path( instance_id: &str, project_path: &Path, project_type: Option, ) -> Result { Ok(theseus::instance::add_project_from_path( instance_id, project_path, project_type, ) .await?) } #[tauri::command] pub async fn instance_is_file_on_modrinth(project_path: &Path) -> Result { Ok(theseus::instance::is_file_on_modrinth(project_path).await?) } #[tauri::command] pub async fn instance_toggle_disable_project( instance_id: &str, project_path: &str, desired_enabled: Option, ) -> Result { Ok(theseus::instance::toggle_disable_project( instance_id, project_path, desired_enabled, ) .await?) } #[tauri::command] pub async fn instance_set_project_locked( instance_id: &str, project_path: &str, locked: bool, ) -> Result<()> { theseus::instance::set_project_locked(instance_id, project_path, locked) .await?; Ok(()) } #[tauri::command] pub async fn instance_remove_project( instance_id: &str, project_path: &str, ) -> Result<()> { theseus::instance::remove_project(instance_id, project_path).await?; Ok(()) } #[tauri::command] pub async fn instance_update_managed_modrinth_version( instance_id: String, version_id: String, ) -> Result { Ok(theseus::instance::update_managed_modrinth_version( &instance_id, &version_id, ) .await?) } #[tauri::command] pub async fn instance_repair_managed_modrinth( instance_id: &str, ) -> Result { Ok(theseus::instance::repair_managed_modrinth(instance_id).await?) } #[tauri::command] pub async fn instance_export_mrpack( instance_id: &str, export_location: PathBuf, included_overrides: Vec, excluded_overrides: Vec, version_id: Option, description: Option, name: Option, ) -> Result<()> { theseus::instance::export_mrpack( instance_id, export_location, included_overrides, excluded_overrides, version_id, description, name, ) .await?; Ok(()) } #[tauri::command] pub async fn instance_get_pack_export_candidates( instance_id: &str, parent: Option, ) -> Result> { Ok(theseus::instance::get_pack_export_candidates_for_parent( instance_id, parent, ) .await?) } #[tauri::command] pub async fn instance_run( instance_id: &str, server_address: Option, ) -> Result { let quick_play = match server_address { Some(addr) => QuickPlayType::Server(ServerAddress::Unresolved(addr)), None => QuickPlayType::None, }; Ok(theseus::instance::run(instance_id, quick_play).await?) } #[tauri::command] pub async fn instance_kill(instance_id: &str) -> Result<()> { theseus::instance::kill(instance_id).await?; Ok(()) } #[tauri::command] pub async fn instance_edit( instance_id: &str, edit_instance: EditInstance, ) -> Result<()> { theseus::instance::edit(instance_id, edit_to_core(edit_instance)?).await?; Ok(()) } #[tauri::command] pub async fn instance_edit_icon( instance_id: &str, icon_path: Option<&Path>, ) -> Result<()> { theseus::instance::edit_icon(instance_id, icon_path).await?; Ok(()) } #[tauri::command] pub async fn instance_share_can_current_user_use() -> Result { Ok(theseus::instance::can_active_user_use_shared_instances().await?) } #[tauri::command] pub async fn instance_share_get_users( instance_id: &str, ) -> Result { Ok(theseus::instance::get_shared_instance_users(instance_id).await?) } #[tauri::command] pub async fn instance_share_invite_users( instance_id: &str, user_ids: Vec, ) -> Result { Ok( theseus::instance::invite_shared_instance_users(instance_id, user_ids) .await?, ) } #[tauri::command] pub async fn instance_share_create_invite_link( instance_id: &str, max_age_seconds: Option, max_uses: Option, replace_invite_id: Option, ) -> Result { Ok(theseus::instance::create_shared_instance_invite_link( instance_id, max_age_seconds, max_uses, replace_invite_id, ) .await?) } #[tauri::command] pub async fn instance_share_get_invites( instance_id: &str, ) -> Result> { Ok(theseus::instance::get_shared_instance_invites(instance_id).await?) } #[tauri::command] pub async fn instance_share_revoke_invite( instance_id: &str, invite_id: String, ) -> Result<()> { Ok( theseus::instance::revoke_shared_instance_invite( instance_id, invite_id, ) .await?, ) } #[tauri::command] pub async fn instance_share_remove_users( instance_id: &str, user_ids: Vec, has_pending_recipients: bool, ) -> Result { Ok(theseus::instance::remove_shared_instance_users( instance_id, user_ids, has_pending_recipients, ) .await?) } #[tauri::command] pub async fn instance_share_get_publish_preview( instance_id: &str, ) -> Result> { Ok( theseus::instance::get_shared_instance_publish_preview(instance_id) .await?, ) } #[tauri::command] pub async fn instance_share_publish( instance_id: &str, config_paths: Vec, ) -> Result { Ok( theseus::instance::publish_shared_instance(instance_id, config_paths) .await? .into(), ) } #[tauri::command] pub async fn instance_share_unlink(instance_id: &str) -> Result<()> { theseus::instance::unlink_shared_instance(instance_id).await?; Ok(()) } #[tauri::command] pub async fn instance_share_unpublish(instance_id: &str) -> Result<()> { theseus::instance::unpublish_shared_instance(instance_id).await?; Ok(()) }