use crate::state::instances::{ ContentSourceKind, Instance, InstanceLaunchOverrides, InstanceLink, LoaderComponent, adapters::sqlite::{ config_sync_rows, content_rows, instance_rows, loader_component_rows, }, config_sync, }; use crate::state::{ Hooks, InstanceInstallStage, LauncherFeatureVersion, MemorySettings, ModLoader, ReleaseChannel, WindowSize, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct EditInstance { pub install_stage: Option, pub launcher_feature_version: Option, pub name: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub icon_path: Option>, pub update_channel: Option, pub groups: Option>, pub link: Option, pub launch_overrides: Option, pub content_set_patch: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub last_played: Option>>, pub submitted_time_played: Option, pub recent_time_played: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub symlink_target: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub game_dir_override: Option>, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct InstanceLaunchOverridesPatch { pub instance_mode: 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 maximize_window: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub game_resolution: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub launch_preparation_timeout: Option>, pub hooks: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct AppliedContentSetPatch { #[serde(default, skip_serializing_if = "Option::is_none")] pub source_kind: Option, pub game_version: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub protocol_version: Option>, pub loader: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub loader_version: Option>, } pub(crate) async fn edit_instance( instance_id: &str, patch: EditInstance, pool: &SqlitePool, ) -> crate::Result { let mut instance = instance_rows::get_instance_by_id(instance_id, pool) .await? .ok_or_else(|| { crate::ErrorKind::InputError("Unknown instance".to_string()) })?; let now = Utc::now(); apply_instance_patch(&mut instance, &patch, now); let loader_projection_changed = patch.content_set_patch.as_ref().is_some_and(|patch| { patch.loader.is_some() || patch.loader_version.is_some() }); let mut content_set = match patch.content_set_patch { Some(content_set_patch) => { let applied_content_set = content_rows::get_applied_content_set(&instance.id, pool) .await? .ok_or_else(|| { crate::ErrorKind::InputError(format!( "Instance {} has no applied content set", instance.id )) })?; Some(apply_content_set_patch( applied_content_set, content_set_patch, now, )) } None => None, }; let mut launch_overrides = match patch.launch_overrides { Some(launch_patch) => { let current = instance_rows::get_instance_launch_overrides( &instance.id, pool, ) .await? .unwrap_or_else(|| { InstanceLaunchOverrides::empty(instance.id.clone()) }); Some(apply_launch_overrides_patch(current, launch_patch)) } None => None, }; let mut tx = pool.begin().await?; instance_rows::update_instance(&instance, &mut tx).await?; if let Some(content_set) = content_set.as_mut() { content_rows::update_content_set(content_set, &mut tx).await?; if loader_projection_changed { let components = LoaderComponent::from_legacy_projection( instance.id.clone(), content_set.loader, content_set.loader_version.clone(), ); loader_component_rows::replace_loader_components( &instance.id, &components, &mut tx, ) .await?; } } if let Some(link) = &patch.link { instance_rows::upsert_instance_link(&instance.id, link, &mut tx) .await?; if matches!(link, InstanceLink::Unmanaged) { let content_set_id = content_set .as_ref() .map(|content_set| content_set.id.as_str()) .or(instance.applied_content_set_id.as_deref()); if let Some(content_set_id) = content_set_id { sqlx::query( "UPDATE instance_content_sets SET source_kind = 'local', modified = ? WHERE id = ?", ) .bind(now.timestamp()) .bind(content_set_id) .execute(&mut *tx) .await?; sqlx::query( "UPDATE instance_content_entries SET source_kind = 'local', ownership_kind = 'user_added', modified_at = ? WHERE content_set_id = ? AND ownership_kind = 'pack_managed'", ) .bind(now.timestamp()) .bind(content_set_id) .execute(&mut *tx) .await?; sqlx::query("DELETE FROM instance_pack_members WHERE content_set_id = ?") .bind(content_set_id) .execute(&mut *tx) .await?; } } } if let Some(groups) = &patch.groups { instance_rows::replace_instance_groups(&instance.id, groups, &mut tx) .await?; } if let Some(overrides) = launch_overrides.as_mut() { instance_rows::upsert_instance_launch_overrides(overrides, &mut tx) .await?; } config_sync_rows::upsert_config_updated_at(&instance.id, &mut *tx).await?; tx.commit().await?; config_sync::mark_dirty(&instance.id); Ok(instance) } pub(crate) async fn restore_instance_metadata( metadata: &crate::state::InstanceMetadata, pool: &SqlitePool, ) -> crate::Result<()> { let mut tx = pool.begin().await?; let instance = metadata.instance.clone(); let mut content_set = metadata.applied_content_set.clone(); let mut launch_overrides = metadata.launch_overrides.clone(); instance_rows::update_instance(&instance, &mut tx).await?; content_rows::update_content_set(&mut content_set, &mut tx).await?; loader_component_rows::replace_loader_components( &instance.id, &metadata.loader_components, &mut tx, ) .await?; instance_rows::upsert_instance_link(&instance.id, &metadata.link, &mut tx) .await?; instance_rows::replace_instance_groups( &instance.id, &metadata.groups, &mut tx, ) .await?; instance_rows::upsert_instance_launch_overrides( &mut launch_overrides, &mut tx, ) .await?; tx.commit().await?; Ok(()) } fn apply_instance_patch( instance: &mut Instance, patch: &EditInstance, now: DateTime, ) { if let Some(install_stage) = patch.install_stage { instance.install_stage = install_stage; } if let Some(launcher_feature_version) = patch.launcher_feature_version { instance.launcher_feature_version = launcher_feature_version; } if let Some(name) = &patch.name { instance.name = name.clone(); } if let Some(icon_path) = &patch.icon_path { instance.icon_path = icon_path.clone(); } if let Some(update_channel) = patch.update_channel { instance.update_channel = update_channel; } if let Some(last_played) = &patch.last_played { instance.last_played = *last_played; } if let Some(submitted_time_played) = patch.submitted_time_played { instance.submitted_time_played = submitted_time_played; } if let Some(recent_time_played) = patch.recent_time_played { instance.recent_time_played = recent_time_played; } if let Some(symlink_target) = &patch.symlink_target { instance.symlink_target = symlink_target.clone(); } if let Some(game_dir_override) = &patch.game_dir_override { instance.game_dir_override = game_dir_override.clone(); } instance.modified = now; } fn apply_content_set_patch( mut content_set: crate::state::instances::ContentSet, patch: AppliedContentSetPatch, now: DateTime, ) -> crate::state::instances::ContentSet { if let Some(game_version) = patch.game_version { content_set.game_version = game_version; } if let Some(source_kind) = patch.source_kind { content_set.source_kind = source_kind; } if let Some(protocol_version) = patch.protocol_version { content_set.protocol_version = protocol_version; } if let Some(loader) = patch.loader { content_set.loader = loader; } if let Some(loader_version) = patch.loader_version { content_set.loader_version = loader_version; } content_set.modified = now; content_set } fn apply_launch_overrides_patch( mut overrides: InstanceLaunchOverrides, patch: InstanceLaunchOverridesPatch, ) -> InstanceLaunchOverrides { if let Some(java_path) = patch.java_path { overrides.java_path = java_path; } if let Some(extra_launch_args) = patch.extra_launch_args { overrides.extra_launch_args = extra_launch_args; } if let Some(custom_env_vars) = patch.custom_env_vars { overrides.custom_env_vars = custom_env_vars; } if let Some(memory) = patch.memory { overrides.memory = memory; } if let Some(force_fullscreen) = patch.force_fullscreen { overrides.force_fullscreen = force_fullscreen; } if let Some(maximize_window) = patch.maximize_window { overrides.maximize_window = maximize_window; } if let Some(game_resolution) = patch.game_resolution { overrides.game_resolution = game_resolution; } if let Some(mode) = patch.instance_mode { overrides.instance_mode = Some(mode); } if let Some(timeout) = patch.launch_preparation_timeout { overrides.launch_preparation_timeout = timeout; } if let Some(hooks) = patch.hooks { overrides.hooks = hooks; } overrides }