@@ -108,6 +110,7 @@
: editServerModal?.show(world)
"
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
+ @desync="() => world.type === 'server' && desyncServerModal?.show(world as ServerWorld)"
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.id, world.path)"
/>
@@ -153,6 +156,7 @@ import { useRoute } from 'vue-router'
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
import ConfirmRemoveWorldModal from '@/components/ui/world/modal/ConfirmRemoveWorldModal.vue'
+import DesyncServerModal from '@/components/ui/world/modal/DesyncServerModal.vue'
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
@@ -164,6 +168,8 @@ import { get_game_versions } from '@/helpers/tags'
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
import {
delete_world,
+ desync_server,
+ type DesyncServerMode,
get_instance_protocol_version,
getServerDomainKey,
getWorldIdentifier,
@@ -189,7 +195,11 @@ import {
import { injectServerInstall } from '@/providers/server-install'
import { injectInstancePage } from '../instance-context'
-import { instanceKeys, instanceWorldsQueryOptions } from '../query-options'
+import {
+ instanceKeys,
+ instanceListQueryOptions,
+ instanceWorldsQueryOptions,
+} from '../query-options'
const messages = defineMessages({
searchWorldsPlaceholder: {
@@ -244,6 +254,7 @@ const addServerModal = ref
>()
const editServerModal = ref>()
const editWorldModal = ref>()
const removeWorldModal = ref>()
+const desyncServerModal = ref>()
const worldToRemove = ref(null)
@@ -283,6 +294,15 @@ function toggleFilter(id: string) {
const queryClient = useQueryClient()
+const instanceListQuery = useQuery(instanceListQueryOptions())
+const otherSyncedInstanceCount = computed(
+ () =>
+ instanceListQuery.data.value?.filter(
+ (candidate) =>
+ candidate.id !== instance.value.id && candidate.synced_options.multiplayer_servers,
+ ).length ?? 0,
+)
+
const refreshingAll = ref(false)
const hadNoWorlds = ref(true)
const startingInstance = ref(false)
@@ -507,6 +527,12 @@ async function removeServer(server: ServerWorld) {
}
}
+async function confirmDesyncServer(server: ServerWorld, mode: DesyncServerMode) {
+ if (!server.server_id) return
+ await desync_server(instance.value.id, server.server_id, mode).catch(handleError)
+ await refreshAllWorlds()
+}
+
async function editWorld(path: string, name: string, removeIcon: boolean) {
const world = worlds.value.find((world) => world.type === 'singleplayer' && world.path === path)
if (world) {
diff --git a/apps/app/build.rs b/apps/app/build.rs
index e566256c11..a4d4f168fa 100644
--- a/apps/app/build.rs
+++ b/apps/app/build.rs
@@ -231,6 +231,17 @@ fn main() {
"instance_move_screenshots",
"instance_open_screenshot",
"instance_set_synced_option",
+ "instance_get_synced_options_overview",
+ "instance_get_global_synced_options",
+ "instance_synced_option_needs_base",
+ "instance_set_global_synced_option",
+ "instance_get_command_history",
+ "instance_set_command_history",
+ "instance_open_synced_options_folder",
+ "instance_list_synced_servers",
+ "instance_update_synced_server",
+ "instance_remove_synced_server",
+ "instance_rebuild_synced_options",
"instance_list",
"instance_list_groups",
"instance_create_group",
@@ -411,6 +422,7 @@ fn main() {
"add_server_to_instance",
"edit_server_in_instance",
"remove_server_from_instance",
+ "desync_server",
"get_instance_protocol_version",
"get_server_status",
"start_join_singleplayer_world",
diff --git a/apps/app/src/api/instance.rs b/apps/app/src/api/instance.rs
index 4b084a9b60..2739e7ca4a 100644
--- a/apps/app/src/api/instance.rs
+++ b/apps/app/src/api/instance.rs
@@ -62,6 +62,17 @@ pub fn init() -> tauri::plugin::TauriPlugin {
instance_move_screenshots,
instance_open_screenshot,
instance_set_synced_option,
+ instance_get_synced_options_overview,
+ instance_get_global_synced_options,
+ instance_synced_option_needs_base,
+ instance_set_global_synced_option,
+ instance_get_command_history,
+ instance_set_command_history,
+ instance_open_synced_options_folder,
+ instance_list_synced_servers,
+ instance_update_synced_server,
+ instance_remove_synced_server,
+ instance_rebuild_synced_options,
instance_check_installed,
instance_update_all,
instance_update_project,
@@ -826,6 +837,93 @@ pub async fn instance_set_synced_option(
))
}
+#[tauri::command]
+pub async fn instance_get_synced_options_overview(
+ instance_id: &str,
+) -> Result {
+ Ok(theseus::instance::get_synced_options_overview(instance_id).await?)
+}
+
+#[tauri::command]
+pub async fn instance_get_global_synced_options()
+-> Result {
+ Ok(theseus::instance::get_global_synced_options().await?)
+}
+
+#[tauri::command]
+pub async fn instance_synced_option_needs_base(
+ option: InstanceSyncedOption,
+) -> Result {
+ Ok(theseus::instance::synced_option_needs_base(option).await?)
+}
+
+#[tauri::command]
+pub async fn instance_set_global_synced_option(
+ option: InstanceSyncedOption,
+ enabled: bool,
+ base_instance_id: Option<&str>,
+) -> Result {
+ Ok(theseus::instance::set_global_synced_option(
+ option,
+ enabled,
+ base_instance_id,
+ )
+ .await?)
+}
+
+#[tauri::command]
+pub async fn instance_get_command_history() -> Result {
+ Ok(theseus::instance::get_command_history().await?)
+}
+
+#[tauri::command]
+pub async fn instance_set_command_history(contents: &str) -> Result {
+ Ok(theseus::instance::set_command_history(contents).await?)
+}
+
+#[tauri::command]
+pub async fn instance_open_synced_options_folder(
+ app_handle: AppHandle,
+) -> Result<()> {
+ let path = theseus::instance::get_synced_options_folder().await?;
+ app_handle
+ .opener()
+ .open_path(path.to_string_lossy(), None::<&str>)
+ .map_err(|error| std::io::Error::other(error.to_string()))?;
+ Ok(())
+}
+
+#[tauri::command]
+pub async fn instance_list_synced_servers()
+-> Result> {
+ Ok(theseus::instance::list_synced_servers().await?)
+}
+
+#[tauri::command]
+pub async fn instance_update_synced_server(
+ server: theseus::instance::SyncedServer,
+) -> Result<()> {
+ Ok(theseus::instance::update_synced_server(server).await?)
+}
+
+#[tauri::command]
+pub async fn instance_remove_synced_server(server_id: &str) -> Result<()> {
+ Ok(theseus::instance::remove_synced_server(server_id).await?)
+}
+
+#[tauri::command]
+pub async fn instance_rebuild_synced_options(
+ instance_id: Option<&str>,
+) -> Result<()> {
+ if let Some(instance_id) = instance_id {
+ theseus::instance::reconcile_instance_synced_options(instance_id)
+ .await?;
+ } else {
+ theseus::instance::reconcile_all_synced_options().await?;
+ }
+ Ok(())
+}
+
fn serialize_screenshots(
app_handle: &AppHandle,
screenshots: Vec,
diff --git a/apps/app/src/api/worlds.rs b/apps/app/src/api/worlds.rs
index afa743e0eb..56dcdd091b 100644
--- a/apps/app/src/api/worlds.rs
+++ b/apps/app/src/api/worlds.rs
@@ -25,6 +25,7 @@ pub fn init() -> tauri::plugin::TauriPlugin {
add_server_to_instance,
edit_server_in_instance,
remove_server_from_instance,
+ desync_server,
get_instance_protocol_version,
get_server_status,
start_join_singleplayer_world,
@@ -193,6 +194,15 @@ pub async fn remove_server_from_instance(
Ok(())
}
+#[tauri::command]
+pub async fn desync_server(
+ instance_id: &str,
+ server_id: &str,
+ mode: theseus::instance::DesyncServerMode,
+) -> Result<()> {
+ Ok(theseus::instance::desync_server(instance_id, server_id, mode).await?)
+}
+
#[tauri::command]
pub async fn get_instance_protocol_version(
instance_id: &str,
diff --git a/packages/app-lib/.sqlx/query-0365707ab9d9b9eb4f189fa9f888fa2c60ebd20a6c10d62fbae546780863d7f7.json b/packages/app-lib/.sqlx/query-0365707ab9d9b9eb4f189fa9f888fa2c60ebd20a6c10d62fbae546780863d7f7.json
new file mode 100644
index 0000000000..30ada7d0b1
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-0365707ab9d9b9eb4f189fa9f888fa2c60ebd20a6c10d62fbae546780863d7f7.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT feature\n\t\tFROM instance_sync_preferences\n\t\tWHERE instance_id = ? AND enabled = 1\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "feature",
+ "ordinal": 0,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "0365707ab9d9b9eb4f189fa9f888fa2c60ebd20a6c10d62fbae546780863d7f7"
+}
diff --git a/packages/app-lib/.sqlx/query-0c2570758978e370bc2b3240874001e6e09eaf922553d5bbfefd6d12c01eb98f.json b/packages/app-lib/.sqlx/query-0c2570758978e370bc2b3240874001e6e09eaf922553d5bbfefd6d12c01eb98f.json
new file mode 100644
index 0000000000..4df688f8f3
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-0c2570758978e370bc2b3240874001e6e09eaf922553d5bbfefd6d12c01eb98f.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\t\tINSERT INTO synced_server_state (singleton, revision)\n\t\t\tVALUES (1, 0)\n\t\t\tON CONFLICT(singleton) DO NOTHING\n\t\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": []
+ },
+ "hash": "0c2570758978e370bc2b3240874001e6e09eaf922553d5bbfefd6d12c01eb98f"
+}
diff --git a/packages/app-lib/.sqlx/query-0f66133eae064cc4b6bc079302d5b56c11ddfa4e1512cbda761e10cba5d78e68.json b/packages/app-lib/.sqlx/query-0f66133eae064cc4b6bc079302d5b56c11ddfa4e1512cbda761e10cba5d78e68.json
new file mode 100644
index 0000000000..4ee5207285
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-0f66133eae064cc4b6bc079302d5b56c11ddfa4e1512cbda761e10cba5d78e68.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT DISTINCT instance_id FROM processes",
+ "describe": {
+ "columns": [
+ {
+ "name": "instance_id",
+ "ordinal": 0,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "0f66133eae064cc4b6bc079302d5b56c11ddfa4e1512cbda761e10cba5d78e68"
+}
diff --git a/packages/app-lib/.sqlx/query-10a2857ec0152359f562650711ec7a2b421a4350290ce3dd27075567e67c7ad2.json b/packages/app-lib/.sqlx/query-10a2857ec0152359f562650711ec7a2b421a4350290ce3dd27075567e67c7ad2.json
new file mode 100644
index 0000000000..1234c366cc
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-10a2857ec0152359f562650711ec7a2b421a4350290ce3dd27075567e67c7ad2.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT instance_id, feature\n\t\tFROM instance_sync_preferences\n\t\tWHERE enabled = 1\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "instance_id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "feature",
+ "ordinal": 1,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "10a2857ec0152359f562650711ec7a2b421a4350290ce3dd27075567e67c7ad2"
+}
diff --git a/packages/app-lib/.sqlx/query-14cb855184725b3e891d2365563f262f9d961d1a0bf170c890ce61bb1dea1dc5.json b/packages/app-lib/.sqlx/query-14cb855184725b3e891d2365563f262f9d961d1a0bf170c890ce61bb1dea1dc5.json
new file mode 100644
index 0000000000..bfb139d1f9
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-14cb855184725b3e891d2365563f262f9d961d1a0bf170c890ce61bb1dea1dc5.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\t\t\tUPDATE synced_server_state\n\t\t\t\tSET revision = revision + 1\n\t\t\t\tWHERE singleton = 1\n\t\t\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": []
+ },
+ "hash": "14cb855184725b3e891d2365563f262f9d961d1a0bf170c890ce61bb1dea1dc5"
+}
diff --git a/packages/app-lib/.sqlx/query-166c4f42eab67dfe1b83d6c29374ae6a27161b7bdd38ba8b4bbbb900d43fc3c8.json b/packages/app-lib/.sqlx/query-166c4f42eab67dfe1b83d6c29374ae6a27161b7bdd38ba8b4bbbb900d43fc3c8.json
new file mode 100644
index 0000000000..4601eb1b36
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-166c4f42eab67dfe1b83d6c29374ae6a27161b7bdd38ba8b4bbbb900d43fc3c8.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT EXISTS(\n\t\t\tSELECT 1 FROM instance_server_pack_state WHERE instance_id = ?\n\t\t) AS \"exists!: bool\"\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "exists!: bool",
+ "ordinal": 0,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "166c4f42eab67dfe1b83d6c29374ae6a27161b7bdd38ba8b4bbbb900d43fc3c8"
+}
diff --git a/packages/app-lib/.sqlx/query-16ff7e3f6efa13aeee64468c0d089ea4f165a4a2405a069dd95326cc1c1bda8e.json b/packages/app-lib/.sqlx/query-16ff7e3f6efa13aeee64468c0d089ea4f165a4a2405a069dd95326cc1c1bda8e.json
new file mode 100644
index 0000000000..0b0fab9438
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-16ff7e3f6efa13aeee64468c0d089ea4f165a4a2405a069dd95326cc1c1bda8e.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "DELETE FROM instance_server_projection_entries WHERE instance_id = ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "16ff7e3f6efa13aeee64468c0d089ea4f165a4a2405a069dd95326cc1c1bda8e"
+}
diff --git a/packages/app-lib/.sqlx/query-18050b91c80f2cc7d218aa334e6bdab9e29c3e8f8401225d8351348e51cbe314.json b/packages/app-lib/.sqlx/query-18050b91c80f2cc7d218aa334e6bdab9e29c3e8f8401225d8351348e51cbe314.json
new file mode 100644
index 0000000000..075570a7bf
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-18050b91c80f2cc7d218aa334e6bdab9e29c3e8f8401225d8351348e51cbe314.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT id, nbt\n\t\tFROM synced_servers\n\t\tORDER BY position\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "nbt",
+ "ordinal": 1,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "18050b91c80f2cc7d218aa334e6bdab9e29c3e8f8401225d8351348e51cbe314"
+}
diff --git a/packages/app-lib/.sqlx/query-1fb66446c3ba0f52e682f8aae4b80a97310f308b3aa25c322d499b32d549f7b7.json b/packages/app-lib/.sqlx/query-1fb66446c3ba0f52e682f8aae4b80a97310f308b3aa25c322d499b32d549f7b7.json
new file mode 100644
index 0000000000..9d10b4b7be
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-1fb66446c3ba0f52e682f8aae4b80a97310f308b3aa25c322d499b32d549f7b7.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT instances.id, instances.name, instances.path\n\t\tFROM instances\n\t\tINNER JOIN instance_sync_preferences preferences\n\t\t\tON preferences.instance_id = instances.id\n\t\tWHERE preferences.feature = 'screenshots'\n\t\t\tAND preferences.enabled = 1\n\t\t\tAND EXISTS (\n\t\t\t\tSELECT 1\n\t\t\t\tFROM sync_feature_settings\n\t\t\t\tWHERE feature = 'screenshots' AND globally_enabled = 1\n\t\t\t)\n\t\tORDER BY instances.name, instances.id\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "path",
+ "ordinal": 2,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "1fb66446c3ba0f52e682f8aae4b80a97310f308b3aa25c322d499b32d549f7b7"
+}
diff --git a/packages/app-lib/.sqlx/query-209158894a8658b61fda164cb35bd807403238b5fd6891803a00464dbfa663fc.json b/packages/app-lib/.sqlx/query-209158894a8658b61fda164cb35bd807403238b5fd6891803a00464dbfa663fc.json
new file mode 100644
index 0000000000..a4f4a942d3
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-209158894a8658b61fda164cb35bd807403238b5fd6891803a00464dbfa663fc.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "DELETE FROM processes WHERE pid = ? AND start_time = ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "209158894a8658b61fda164cb35bd807403238b5fd6891803a00464dbfa663fc"
+}
diff --git a/packages/app-lib/.sqlx/query-3ad4711be00a29f9c84b1529f7cd8329d4bd301d4cfae600ff08564075e45989.json b/packages/app-lib/.sqlx/query-3ad4711be00a29f9c84b1529f7cd8329d4bd301d4cfae600ff08564075e45989.json
new file mode 100644
index 0000000000..4a4e9e7fc2
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-3ad4711be00a29f9c84b1529f7cd8329d4bd301d4cfae600ff08564075e45989.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tDELETE FROM instance_sync_checkpoints\n\t\tWHERE instance_id = ?\n\t\t\tAND feature = 'creative_hotbars'\n\t\t\tAND variant != ?\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "3ad4711be00a29f9c84b1529f7cd8329d4bd301d4cfae600ff08564075e45989"
+}
diff --git a/packages/app-lib/.sqlx/query-3b97f2c92fe42ade6a2981cc89750386782589c77c0517026489d8928149941b.json b/packages/app-lib/.sqlx/query-3b97f2c92fe42ade6a2981cc89750386782589c77c0517026489d8928149941b.json
new file mode 100644
index 0000000000..878aa47c1a
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-3b97f2c92fe42ade6a2981cc89750386782589c77c0517026489d8928149941b.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "DELETE FROM synced_servers WHERE id = ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "3b97f2c92fe42ade6a2981cc89750386782589c77c0517026489d8928149941b"
+}
diff --git a/packages/app-lib/.sqlx/query-58c94159c024a407b7291a16ac5cd6b28eddf7e6366de078850788d2b3128d0b.json b/packages/app-lib/.sqlx/query-58c94159c024a407b7291a16ac5cd6b28eddf7e6366de078850788d2b3128d0b.json
new file mode 100644
index 0000000000..9f99749ef6
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-58c94159c024a407b7291a16ac5cd6b28eddf7e6366de078850788d2b3128d0b.json
@@ -0,0 +1,38 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT expected_sha1, merge_base,\n\t\t\tsource_revision AS \"source_revision!: i64\", status\n\t\tFROM instance_sync_checkpoints\n\t\tWHERE instance_id = ? AND feature = ? AND variant = ?\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "expected_sha1",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "merge_base",
+ "ordinal": 1,
+ "type_info": "Blob"
+ },
+ {
+ "name": "source_revision!: i64",
+ "ordinal": 2,
+ "type_info": "Integer"
+ },
+ {
+ "name": "status",
+ "ordinal": 3,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": [
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "58c94159c024a407b7291a16ac5cd6b28eddf7e6366de078850788d2b3128d0b"
+}
diff --git a/packages/app-lib/.sqlx/query-58d7b3809798f5476eaa56f23a37668cfd117f83baab840228dbd5f6c6e9fe89.json b/packages/app-lib/.sqlx/query-58d7b3809798f5476eaa56f23a37668cfd117f83baab840228dbd5f6c6e9fe89.json
new file mode 100644
index 0000000000..3208c80cc5
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-58d7b3809798f5476eaa56f23a37668cfd117f83baab840228dbd5f6c6e9fe89.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\t\tINSERT INTO instance_servers\n\t\t\t\t(instance_id, id, source, excluded_synced_server_id,\n\t\t\t\t nbt, position)\n\t\t\tVALUES (?, ?, ?,\n\t\t\t\t(SELECT id FROM synced_servers WHERE id = ?), ?, ?)\n\t\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 6
+ },
+ "nullable": []
+ },
+ "hash": "58d7b3809798f5476eaa56f23a37668cfd117f83baab840228dbd5f6c6e9fe89"
+}
diff --git a/packages/app-lib/.sqlx/query-5d509c2cce1728990fbf9ee4921eae357c1043ac65bc021fe093da5e72b2684a.json b/packages/app-lib/.sqlx/query-5d509c2cce1728990fbf9ee4921eae357c1043ac65bc021fe093da5e72b2684a.json
new file mode 100644
index 0000000000..f2a7293908
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-5d509c2cce1728990fbf9ee4921eae357c1043ac65bc021fe093da5e72b2684a.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO instance_server_pack_state (instance_id, version_id)\n\t\tVALUES (?, ?)\n\t\tON CONFLICT(instance_id) DO UPDATE SET\n\t\t\tversion_id = excluded.version_id\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "5d509c2cce1728990fbf9ee4921eae357c1043ac65bc021fe093da5e72b2684a"
+}
diff --git a/packages/app-lib/.sqlx/query-6787d19c235ae92fc05f08782173a59d340a3ba4a0543539fd37ead0f9813a37.json b/packages/app-lib/.sqlx/query-6787d19c235ae92fc05f08782173a59d340a3ba4a0543539fd37ead0f9813a37.json
new file mode 100644
index 0000000000..b93edb98c2
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-6787d19c235ae92fc05f08782173a59d340a3ba4a0543539fd37ead0f9813a37.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT pid, start_time\n\t\tFROM processes\n\t\tWHERE instance_id = ?\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "pid",
+ "ordinal": 0,
+ "type_info": "Integer"
+ },
+ {
+ "name": "start_time",
+ "ordinal": 1,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "6787d19c235ae92fc05f08782173a59d340a3ba4a0543539fd37ead0f9813a37"
+}
diff --git a/packages/app-lib/.sqlx/query-6e8707551a6154d73d58ac54d35dc78ed283e6ce57c9a2332645df74b359b5f7.json b/packages/app-lib/.sqlx/query-6e8707551a6154d73d58ac54d35dc78ed283e6ce57c9a2332645df74b359b5f7.json
new file mode 100644
index 0000000000..14c048bfe9
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-6e8707551a6154d73d58ac54d35dc78ed283e6ce57c9a2332645df74b359b5f7.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT schema_version AS \"schema_version!: i64\",\n\t\t\trevision AS \"revision!: i64\", nbt\n\t\tFROM synced_hotbar_state\n\t\tWHERE singleton = 1\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "schema_version!: i64",
+ "ordinal": 0,
+ "type_info": "Integer"
+ },
+ {
+ "name": "revision!: i64",
+ "ordinal": 1,
+ "type_info": "Integer"
+ },
+ {
+ "name": "nbt",
+ "ordinal": 2,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "6e8707551a6154d73d58ac54d35dc78ed283e6ce57c9a2332645df74b359b5f7"
+}
diff --git a/packages/app-lib/.sqlx/query-8540ff309e2e5eaef1a3f76bdf877504cc79e5b200d5c260d86bb33e093112d9.json b/packages/app-lib/.sqlx/query-8540ff309e2e5eaef1a3f76bdf877504cc79e5b200d5c260d86bb33e093112d9.json
new file mode 100644
index 0000000000..1bdfb5581b
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-8540ff309e2e5eaef1a3f76bdf877504cc79e5b200d5c260d86bb33e093112d9.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT feature, globally_enabled AS \"globally_enabled!: bool\"\n\t\tFROM sync_feature_settings\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "feature",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "globally_enabled!: bool",
+ "ordinal": 1,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "8540ff309e2e5eaef1a3f76bdf877504cc79e5b200d5c260d86bb33e093112d9"
+}
diff --git a/packages/app-lib/.sqlx/query-8aa89bfd90dba93d7ae2274f0b1e591e1a305966ffaa84fbc2e15cccc98854aa.json b/packages/app-lib/.sqlx/query-8aa89bfd90dba93d7ae2274f0b1e591e1a305966ffaa84fbc2e15cccc98854aa.json
new file mode 100644
index 0000000000..52620432e0
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-8aa89bfd90dba93d7ae2274f0b1e591e1a305966ffaa84fbc2e15cccc98854aa.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\t\t\tINSERT INTO processes\n\t\t\t\t\t(pid, start_time, name, executable, instance_id,\n\t\t\t\t\t post_exit_command)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?)\n\t\t\t\tON CONFLICT(pid) DO UPDATE SET\n\t\t\t\t\tstart_time = excluded.start_time,\n\t\t\t\t\tname = excluded.name,\n\t\t\t\t\texecutable = excluded.executable,\n\t\t\t\t\tinstance_id = excluded.instance_id,\n\t\t\t\t\tpost_exit_command = excluded.post_exit_command\n\t\t\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 6
+ },
+ "nullable": []
+ },
+ "hash": "8aa89bfd90dba93d7ae2274f0b1e591e1a305966ffaa84fbc2e15cccc98854aa"
+}
diff --git a/packages/app-lib/.sqlx/query-8bff2deeaa56090262af4ce54c7f53b73af4d0715d7c15732416d7ecf6204019.json b/packages/app-lib/.sqlx/query-8bff2deeaa56090262af4ce54c7f53b73af4d0715d7c15732416d7ecf6204019.json
new file mode 100644
index 0000000000..98f4c3c028
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-8bff2deeaa56090262af4ce54c7f53b73af4d0715d7c15732416d7ecf6204019.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE synced_servers SET position = position + ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "8bff2deeaa56090262af4ce54c7f53b73af4d0715d7c15732416d7ecf6204019"
+}
diff --git a/packages/app-lib/.sqlx/query-8cde9f140077c5b11ff4c5b174d75c1883beaecc71a81d50e6a67d0565782ba0.json b/packages/app-lib/.sqlx/query-8cde9f140077c5b11ff4c5b174d75c1883beaecc71a81d50e6a67d0565782ba0.json
new file mode 100644
index 0000000000..a2f33faeff
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-8cde9f140077c5b11ff4c5b174d75c1883beaecc71a81d50e6a67d0565782ba0.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO instance_sync_checkpoints\n\t\t\t(instance_id, feature, variant, expected_sha1, merge_base,\n\t\t\t source_revision, status, link_mode)\n\t\tVALUES (?, 'multiplayer_servers', 'default', ?, NULL, ?,\n\t\t\t'pending', NULL)\n\t\tON CONFLICT(instance_id, feature, variant) DO UPDATE SET\n\t\t\texpected_sha1 = excluded.expected_sha1,\n\t\t\tmerge_base = NULL,\n\t\t\tsource_revision = excluded.source_revision,\n\t\t\tstatus = 'pending',\n\t\t\tlink_mode = NULL\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": []
+ },
+ "hash": "8cde9f140077c5b11ff4c5b174d75c1883beaecc71a81d50e6a67d0565782ba0"
+}
diff --git a/packages/app-lib/.sqlx/query-9182f6a8ea76fef826b442ff07ae8f44f71a1e3131d56e06aae05b26d4bbdebd.json b/packages/app-lib/.sqlx/query-9182f6a8ea76fef826b442ff07ae8f44f71a1e3131d56e06aae05b26d4bbdebd.json
new file mode 100644
index 0000000000..ab75ba11e1
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-9182f6a8ea76fef826b442ff07ae8f44f71a1e3131d56e06aae05b26d4bbdebd.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT version_id\n\t\tFROM instance_server_pack_state\n\t\tWHERE instance_id = ?\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "version_id",
+ "ordinal": 0,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ true
+ ]
+ },
+ "hash": "9182f6a8ea76fef826b442ff07ae8f44f71a1e3131d56e06aae05b26d4bbdebd"
+}
diff --git a/packages/app-lib/.sqlx/query-92fb9a65a4d741ffc64c62c65f94172cb6567c6c055399632d960548672ce343.json b/packages/app-lib/.sqlx/query-92fb9a65a4d741ffc64c62c65f94172cb6567c6c055399632d960548672ce343.json
new file mode 100644
index 0000000000..ae453fdeed
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-92fb9a65a4d741ffc64c62c65f94172cb6567c6c055399632d960548672ce343.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO instance_sync_checkpoints\n\t\t\t(instance_id, feature, variant, expected_sha1, merge_base,\n\t\t\t source_revision, status, link_mode)\n\t\tVALUES (?, ?, ?, ?, ?, ?, 'pending', NULL)\n\t\tON CONFLICT(instance_id, feature, variant) DO UPDATE SET\n\t\t\texpected_sha1 = excluded.expected_sha1,\n\t\t\tmerge_base = excluded.merge_base,\n\t\t\tsource_revision = excluded.source_revision,\n\t\t\tstatus = 'pending',\n\t\t\tlink_mode = NULL\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 6
+ },
+ "nullable": []
+ },
+ "hash": "92fb9a65a4d741ffc64c62c65f94172cb6567c6c055399632d960548672ce343"
+}
diff --git a/packages/app-lib/.sqlx/query-9dbcad56f76eca4497e928a6367f9d3a54720cbdcdf957b985ec7bef43dbe978.json b/packages/app-lib/.sqlx/query-9dbcad56f76eca4497e928a6367f9d3a54720cbdcdf957b985ec7bef43dbe978.json
new file mode 100644
index 0000000000..d3f03a0531
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-9dbcad56f76eca4497e928a6367f9d3a54720cbdcdf957b985ec7bef43dbe978.json
@@ -0,0 +1,32 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT id, position, nbt\n\t\tFROM synced_servers\n\t\tORDER BY position\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "position",
+ "ordinal": 1,
+ "type_info": "Integer"
+ },
+ {
+ "name": "nbt",
+ "ordinal": 2,
+ "type_info": "Blob"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "9dbcad56f76eca4497e928a6367f9d3a54720cbdcdf957b985ec7bef43dbe978"
+}
diff --git a/packages/app-lib/.sqlx/query-a1313b9534a5eec443721ad6ac904f808cff64f5341e2b82bc6b934fb99078b3.json b/packages/app-lib/.sqlx/query-a1313b9534a5eec443721ad6ac904f808cff64f5341e2b82bc6b934fb99078b3.json
new file mode 100644
index 0000000000..d9af0a67a6
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-a1313b9534a5eec443721ad6ac904f808cff64f5341e2b82bc6b934fb99078b3.json
@@ -0,0 +1,38 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT server_id, owner, nbt, position\n\t\tFROM instance_server_projection_entries\n\t\tWHERE instance_id = ?\n\t\tORDER BY position\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "server_id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "owner",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "nbt",
+ "ordinal": 2,
+ "type_info": "Blob"
+ },
+ {
+ "name": "position",
+ "ordinal": 3,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "a1313b9534a5eec443721ad6ac904f808cff64f5341e2b82bc6b934fb99078b3"
+}
diff --git a/packages/app-lib/.sqlx/query-a27af45c646b3bea98974f0b2f63c6f3e5719319ce8469e0304fd2d93e046006.json b/packages/app-lib/.sqlx/query-a27af45c646b3bea98974f0b2f63c6f3e5719319ce8469e0304fd2d93e046006.json
new file mode 100644
index 0000000000..ebc4089c43
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-a27af45c646b3bea98974f0b2f63c6f3e5719319ce8469e0304fd2d93e046006.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT revision AS \"revision!: i64\"\n\t\tFROM synced_server_state\n\t\tWHERE singleton = 1\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "revision!: i64",
+ "ordinal": 0,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "a27af45c646b3bea98974f0b2f63c6f3e5719319ce8469e0304fd2d93e046006"
+}
diff --git a/packages/app-lib/.sqlx/query-b012838123b5acd053412951b0044857cc3145d7936b79ab4cf6adcbeeec3095.json b/packages/app-lib/.sqlx/query-b012838123b5acd053412951b0044857cc3145d7936b79ab4cf6adcbeeec3095.json
new file mode 100644
index 0000000000..858f19fe9c
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-b012838123b5acd053412951b0044857cc3145d7936b79ab4cf6adcbeeec3095.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT EXISTS(\n\t\t\tSELECT 1 FROM synced_server_state WHERE singleton = 1\n\t\t) AS \"initialized!: bool\"\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "initialized!: bool",
+ "ordinal": 0,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "b012838123b5acd053412951b0044857cc3145d7936b79ab4cf6adcbeeec3095"
+}
diff --git a/packages/app-lib/.sqlx/query-d4879ae44bd13fb97606876e9c67e0ae6202cd391ce25a26642fcfffff05c688.json b/packages/app-lib/.sqlx/query-d4879ae44bd13fb97606876e9c67e0ae6202cd391ce25a26642fcfffff05c688.json
new file mode 100644
index 0000000000..afb1e18e66
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-d4879ae44bd13fb97606876e9c67e0ae6202cd391ce25a26642fcfffff05c688.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tUPDATE instance_sync_checkpoints\n\t\tSET status = 'ready', link_mode = ?\n\t\tWHERE instance_id = ? AND feature = ? AND variant = ?\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 4
+ },
+ "nullable": []
+ },
+ "hash": "d4879ae44bd13fb97606876e9c67e0ae6202cd391ce25a26642fcfffff05c688"
+}
diff --git a/packages/app-lib/.sqlx/query-d8f74c9d677ac6cc41503f1c88d3b360940b0419833a5679133fff5faaa81201.json b/packages/app-lib/.sqlx/query-d8f74c9d677ac6cc41503f1c88d3b360940b0419833a5679133fff5faaa81201.json
new file mode 100644
index 0000000000..e2f95e6813
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-d8f74c9d677ac6cc41503f1c88d3b360940b0419833a5679133fff5faaa81201.json
@@ -0,0 +1,44 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT id, source, excluded_synced_server_id, nbt, position\n\t\tFROM instance_servers\n\t\tWHERE instance_id = ?\n\t\tORDER BY position\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "source",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "excluded_synced_server_id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "nbt",
+ "ordinal": 3,
+ "type_info": "Blob"
+ },
+ {
+ "name": "position",
+ "ordinal": 4,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ true,
+ false,
+ false
+ ]
+ },
+ "hash": "d8f74c9d677ac6cc41503f1c88d3b360940b0419833a5679133fff5faaa81201"
+}
diff --git a/packages/app-lib/.sqlx/query-db467f33a061ee2b39c3729596a49a24fd1debeeef6495fd49cc1da50e8f56b5.json b/packages/app-lib/.sqlx/query-db467f33a061ee2b39c3729596a49a24fd1debeeef6495fd49cc1da50e8f56b5.json
new file mode 100644
index 0000000000..66bfe04e4d
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-db467f33a061ee2b39c3729596a49a24fd1debeeef6495fd49cc1da50e8f56b5.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\t\tINSERT INTO instance_server_projection_entries\n\t\t\t\t(instance_id, owner, server_id, nbt, position)\n\t\t\tVALUES (?, ?, ?, ?, ?)\n\t\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 5
+ },
+ "nullable": []
+ },
+ "hash": "db467f33a061ee2b39c3729596a49a24fd1debeeef6495fd49cc1da50e8f56b5"
+}
diff --git a/packages/app-lib/.sqlx/query-df901b84d082e28c63b41db4a0de9c86aa6b24f126c30a02b50473d4a0b78086.json b/packages/app-lib/.sqlx/query-df901b84d082e28c63b41db4a0de9c86aa6b24f126c30a02b50473d4a0b78086.json
new file mode 100644
index 0000000000..6d0c79bf9b
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-df901b84d082e28c63b41db4a0de9c86aa6b24f126c30a02b50473d4a0b78086.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO synced_hotbar_state\n\t\t\t(singleton, schema_version, revision, nbt)\n\t\tVALUES (1, ?, ?, ?)\n\t\tON CONFLICT(singleton) DO UPDATE SET\n\t\t\tschema_version = excluded.schema_version,\n\t\t\trevision = excluded.revision,\n\t\t\tnbt = excluded.nbt\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": []
+ },
+ "hash": "df901b84d082e28c63b41db4a0de9c86aa6b24f126c30a02b50473d4a0b78086"
+}
diff --git a/packages/app-lib/.sqlx/query-e6a85166a48a4350d3876e3902ad04834e26b15af36df178c16da30b5fdda090.json b/packages/app-lib/.sqlx/query-e6a85166a48a4350d3876e3902ad04834e26b15af36df178c16da30b5fdda090.json
new file mode 100644
index 0000000000..deb274d69c
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-e6a85166a48a4350d3876e3902ad04834e26b15af36df178c16da30b5fdda090.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT EXISTS(\n\t\t\tSELECT 1 FROM sync_feature_settings\n\t\t\tWHERE feature = 'multiplayer_servers' AND globally_enabled = 1\n\t\t) AS \"enabled!: bool\"\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "enabled!: bool",
+ "ordinal": 0,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "e6a85166a48a4350d3876e3902ad04834e26b15af36df178c16da30b5fdda090"
+}
diff --git a/packages/app-lib/.sqlx/query-e9ab62553f47a98c02b45655f589df2d66a0bb9f2aa766f2fe979cd82c0536a6.json b/packages/app-lib/.sqlx/query-e9ab62553f47a98c02b45655f589df2d66a0bb9f2aa766f2fe979cd82c0536a6.json
new file mode 100644
index 0000000000..dc37d06e40
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-e9ab62553f47a98c02b45655f589df2d66a0bb9f2aa766f2fe979cd82c0536a6.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO instance_sync_preferences (instance_id, feature, enabled)\n\t\tVALUES (?, ?, ?)\n\t\tON CONFLICT (instance_id, feature) DO UPDATE SET\n\t\t\tenabled = excluded.enabled\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": []
+ },
+ "hash": "e9ab62553f47a98c02b45655f589df2d66a0bb9f2aa766f2fe979cd82c0536a6"
+}
diff --git a/packages/app-lib/.sqlx/query-ea6de6d2969a82372bceee3c886e521fa08921e146ae79ea4bd0990fa1cc11ba.json b/packages/app-lib/.sqlx/query-ea6de6d2969a82372bceee3c886e521fa08921e146ae79ea4bd0990fa1cc11ba.json
new file mode 100644
index 0000000000..2b06253a1b
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-ea6de6d2969a82372bceee3c886e521fa08921e146ae79ea4bd0990fa1cc11ba.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\t\tINSERT INTO synced_servers (id, position, nbt)\n\t\t\tVALUES (?, ?, ?)\n\t\t\tON CONFLICT(id) DO UPDATE SET\n\t\t\t\tposition = excluded.position,\n\t\t\t\tnbt = excluded.nbt\n\t\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": []
+ },
+ "hash": "ea6de6d2969a82372bceee3c886e521fa08921e146ae79ea4bd0990fa1cc11ba"
+}
diff --git a/packages/app-lib/.sqlx/query-edaca667a0fcd5b429b1788e2b4693a973084f3c3ffe8d521daf8d553957f5ae.json b/packages/app-lib/.sqlx/query-edaca667a0fcd5b429b1788e2b4693a973084f3c3ffe8d521daf8d553957f5ae.json
new file mode 100644
index 0000000000..84c3b5de60
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-edaca667a0fcd5b429b1788e2b4693a973084f3c3ffe8d521daf8d553957f5ae.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "DELETE FROM instance_servers WHERE instance_id = ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "edaca667a0fcd5b429b1788e2b4693a973084f3c3ffe8d521daf8d553957f5ae"
+}
diff --git a/packages/app-lib/.sqlx/query-efd07185aa3047caee3b3323aeb5414f0ab43a9c28c4ea2327cd0022876b925f.json b/packages/app-lib/.sqlx/query-efd07185aa3047caee3b3323aeb5414f0ab43a9c28c4ea2327cd0022876b925f.json
new file mode 100644
index 0000000000..40d5de38b4
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-efd07185aa3047caee3b3323aeb5414f0ab43a9c28c4ea2327cd0022876b925f.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO instance_sync_preferences (instance_id, feature, enabled)\n\t\tSELECT ?, feature, new_instance_default\n\t\tFROM sync_feature_settings\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "efd07185aa3047caee3b3323aeb5414f0ab43a9c28c4ea2327cd0022876b925f"
+}
diff --git a/packages/app-lib/.sqlx/query-f0b9a997d247d3b53604e96c075b5ef93185aab1e2722fd13bbdae287d988c2f.json b/packages/app-lib/.sqlx/query-f0b9a997d247d3b53604e96c075b5ef93185aab1e2722fd13bbdae287d988c2f.json
new file mode 100644
index 0000000000..5196e6ac82
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-f0b9a997d247d3b53604e96c075b5ef93185aab1e2722fd13bbdae287d988c2f.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT EXISTS(\n\t\t\tSELECT 1 FROM synced_hotbar_state WHERE singleton = 1\n\t\t) AS \"exists!: bool\"\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "exists!: bool",
+ "ordinal": 0,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "f0b9a997d247d3b53604e96c075b5ef93185aab1e2722fd13bbdae287d988c2f"
+}
diff --git a/packages/app-lib/.sqlx/query-fc91a68aaf7e6762d3bddbd4fe4e461b77671619a65fb809bd9b8ed9ab71baaa.json b/packages/app-lib/.sqlx/query-fc91a68aaf7e6762d3bddbd4fe4e461b77671619a65fb809bd9b8ed9ab71baaa.json
new file mode 100644
index 0000000000..5b2dcbb69b
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-fc91a68aaf7e6762d3bddbd4fe4e461b77671619a65fb809bd9b8ed9ab71baaa.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tSELECT EXISTS(SELECT 1 FROM instances WHERE id = ?)\n\t\t\tAS \"exists!: bool\"\n\t\t",
+ "describe": {
+ "columns": [
+ {
+ "name": "exists!: bool",
+ "ordinal": 0,
+ "type_info": "Integer"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "fc91a68aaf7e6762d3bddbd4fe4e461b77671619a65fb809bd9b8ed9ab71baaa"
+}
diff --git a/packages/app-lib/.sqlx/query-ffc9e28c260ab9f2f5f57b88fc6cda439b83265cc885d720104f61d8defcba66.json b/packages/app-lib/.sqlx/query-ffc9e28c260ab9f2f5f57b88fc6cda439b83265cc885d720104f61d8defcba66.json
new file mode 100644
index 0000000000..9a39fadf9b
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-ffc9e28c260ab9f2f5f57b88fc6cda439b83265cc885d720104f61d8defcba66.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n\t\tINSERT INTO sync_feature_settings\n\t\t\t(feature, globally_enabled, new_instance_default)\n\t\tVALUES (?, ?, 1)\n\t\tON CONFLICT(feature) DO UPDATE SET\n\t\t\tglobally_enabled = excluded.globally_enabled\n\t\t",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "ffc9e28c260ab9f2f5f57b88fc6cda439b83265cc885d720104f61d8defcba66"
+}
diff --git a/packages/app-lib/migrations/20260819120000_instance-synced-options.sql b/packages/app-lib/migrations/20260819120000_instance-synced-options.sql
index 6ae1d876be..5398d14dc3 100644
--- a/packages/app-lib/migrations/20260819120000_instance-synced-options.sql
+++ b/packages/app-lib/migrations/20260819120000_instance-synced-options.sql
@@ -1,15 +1,123 @@
-CREATE TABLE instance_synced_options (
+-- Global switches and defaults for each sync feature.
+CREATE TABLE sync_feature_settings (
+ feature TEXT PRIMARY KEY NOT NULL,
+ globally_enabled INTEGER NOT NULL CHECK (globally_enabled IN (0, 1)),
+ new_instance_default INTEGER NOT NULL CHECK (new_instance_default IN (0, 1))
+);
+
+INSERT INTO sync_feature_settings
+ (feature, globally_enabled, new_instance_default)
+VALUES
+ ('command_history', 1, 1),
+ ('multiplayer_servers', 1, 1),
+ ('creative_hotbars', 1, 1),
+ ('screenshots', 1, 1);
+
+-- Each instance's switches for the sync features.
+CREATE TABLE instance_sync_preferences (
instance_id TEXT NOT NULL,
- option TEXT NOT NULL,
+ feature TEXT NOT NULL,
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
+ PRIMARY KEY (instance_id, feature),
+ FOREIGN KEY (instance_id) REFERENCES instances (id)
+ ON DELETE CASCADE,
+ FOREIGN KEY (feature) REFERENCES sync_feature_settings (feature)
+ ON DELETE CASCADE
+);
+
+CREATE INDEX instance_sync_preferences_feature_enabled ON instance_sync_preferences (
+ feature,
+ enabled
+);
+
+-- Existing instances pre-update will have screenshots on, everything else off.
+INSERT INTO instance_sync_preferences (instance_id, feature, enabled)
+SELECT instances.id, features.feature, features.enabled
+FROM
+ instances
+ CROSS JOIN (
+ SELECT 'command_history' AS feature, 0 AS enabled
+ UNION ALL
+ SELECT 'multiplayer_servers', 0
+ UNION ALL
+ SELECT 'creative_hotbars', 0
+ UNION ALL
+ SELECT 'screenshots', 1
+ ) AS features;
+
+-- The file each instance should have, and whether writing it finished.
+-- For hotbars, merge_base is the last file we generated.
+CREATE TABLE instance_sync_checkpoints (
+ instance_id TEXT NOT NULL,
+ feature TEXT NOT NULL,
+ variant TEXT NOT NULL CHECK (variant IN ('default', 'legacy', 'components')),
+ expected_sha1 TEXT NOT NULL,
+ merge_base BLOB,
+ source_revision INTEGER NOT NULL CHECK (source_revision >= 0),
+ status TEXT NOT NULL CHECK (status IN ('pending', 'ready')),
+ link_mode TEXT CHECK (link_mode IS NULL OR link_mode IN ('copy', 'hard', 'symbolic')),
+ PRIMARY KEY (instance_id, feature, variant),
+ FOREIGN KEY (instance_id, feature) REFERENCES instance_sync_preferences (instance_id, feature)
+ ON DELETE CASCADE
+);
- PRIMARY KEY (instance_id, option),
- FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE
+-- The shared hotbars. No row means they have not been set up yet.
+CREATE TABLE synced_hotbar_state (
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
+ schema_version INTEGER NOT NULL CHECK (schema_version >= 1),
+ revision INTEGER NOT NULL CHECK (revision >= 0),
+ nbt BLOB NOT NULL
);
-CREATE INDEX instance_synced_options_option_enabled
- ON instance_synced_options(option, enabled);
+-- The shared server-list revision after server sync has been set up.
+-- This row still exists when the shared server list is empty.
+CREATE TABLE synced_server_state (
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
+ revision INTEGER NOT NULL CHECK (revision >= 0)
+);
+
+-- The shared multiplayer server list.
+CREATE TABLE synced_servers (
+ id TEXT PRIMARY KEY NOT NULL,
+ position INTEGER NOT NULL UNIQUE CHECK (position >= 0),
+ nbt BLOB NOT NULL
+);
+
+-- Servers that only belong to one instance.
+-- excluded_synced_server_id is the shared server replaced by a local copy.
+CREATE TABLE instance_servers (
+ instance_id TEXT NOT NULL,
+ id TEXT NOT NULL,
+ source TEXT NOT NULL CHECK (source IN ('modpack', 'local_desynced')),
+ excluded_synced_server_id TEXT,
+ nbt BLOB NOT NULL,
+ position INTEGER NOT NULL CHECK (position >= 0),
+ PRIMARY KEY (instance_id, id),
+ UNIQUE (instance_id, position),
+ FOREIGN KEY (instance_id) REFERENCES instances (id)
+ ON DELETE CASCADE,
+ FOREIGN KEY (excluded_synced_server_id) REFERENCES synced_servers (id)
+ ON DELETE SET NULL
+);
-INSERT INTO instance_synced_options (instance_id, option, enabled)
-SELECT id, 'screenshots', 1
-FROM instances;
+-- The servers.dat we last wrote for each instance.
+-- Used to work out what changed the next time we read it.
+CREATE TABLE instance_server_projection_entries (
+ instance_id TEXT NOT NULL,
+ owner TEXT NOT NULL CHECK (owner IN ('synced', 'instance')),
+ server_id TEXT NOT NULL,
+ nbt BLOB NOT NULL,
+ position INTEGER NOT NULL CHECK (position >= 0),
+ PRIMARY KEY (instance_id, owner, server_id),
+ UNIQUE (instance_id, position),
+ FOREIGN KEY (instance_id) REFERENCES instances (id)
+ ON DELETE CASCADE
+);
+
+-- Tracks whether we have read the modpack's server list for this instance.
+CREATE TABLE instance_server_pack_state (
+ instance_id TEXT PRIMARY KEY NOT NULL,
+ version_id TEXT,
+ FOREIGN KEY (instance_id) REFERENCES instances (id)
+ ON DELETE CASCADE
+);
diff --git a/packages/app-lib/src/api/instance.rs b/packages/app-lib/src/api/instance.rs
index b771398201..43502753c0 100644
--- a/packages/app-lib/src/api/instance.rs
+++ b/packages/app-lib/src/api/instance.rs
@@ -14,6 +14,8 @@ mod run;
mod screenshot_groups;
mod screenshots;
mod shared;
+mod synced_options;
+pub(crate) mod synced_servers;
pub use self::content::{
get_content_items, get_dependencies_as_content_items,
@@ -90,3 +92,24 @@ pub use self::shared::{
remove_shared_instance_users, revoke_shared_instance_invite,
unlink_shared_instance, unpublish_shared_instance, update_shared_instance,
};
+pub use self::synced_options::{
+ GlobalSyncedOptions, SyncedOptionCapability, SyncedOptionsOverview,
+ get_capabilities as get_synced_option_capabilities, get_command_history,
+ get_global_options as get_global_synced_options,
+ get_overview as get_synced_options_overview, get_synced_options_folder,
+ set_command_history, set_global_option as set_global_synced_option,
+ synced_option_needs_base,
+};
+pub(crate) use self::synced_options::{
+ monitor_persisted_processes, prepare_instance_update,
+ reconcile_changed_file as reconcile_synced_option_file,
+ remove_generated_instance_files,
+};
+pub use self::synced_options::{
+ reconcile_all as reconcile_all_synced_options,
+ reconcile_instance as reconcile_instance_synced_options,
+};
+pub use self::synced_servers::{
+ DesyncServerMode, ServerSource, SyncedServer, desync_server,
+ list_synced_servers, remove_synced_server, update_synced_server,
+};
diff --git a/packages/app-lib/src/api/instance/lifecycle.rs b/packages/app-lib/src/api/instance/lifecycle.rs
index d5b8813631..69882efd15 100644
--- a/packages/app-lib/src/api/instance/lifecycle.rs
+++ b/packages/app-lib/src/api/instance/lifecycle.rs
@@ -77,6 +77,8 @@ pub async fn edit(
.as_error()
})?;
+ super::reconcile_instance_synced_options(instance_id).await?;
+
emit_instance(&instance.instance.id, InstancePayloadType::Edited).await?;
Ok(instance)
@@ -87,22 +89,13 @@ pub async fn set_synced_option(
option: InstanceSyncedOption,
enabled: bool,
) -> crate::Result {
- let state = State::get().await?;
- instance_rows::set_instance_synced_option(
+ let instance = super::synced_options::set_instance_option(
instance_id,
option,
enabled,
- &state.pool,
)
.await?;
- let instance = crate::state::get_instance(instance_id, &state.pool)
- .await?
- .ok_or_else(|| {
- crate::ErrorKind::InputError("Unknown instance".to_string())
- .as_error()
- })?;
-
emit_instance(&instance.instance.id, InstancePayloadType::Edited).await?;
Ok(instance)
diff --git a/packages/app-lib/src/api/instance/synced_options.rs b/packages/app-lib/src/api/instance/synced_options.rs
new file mode 100644
index 0000000000..db3879964e
--- /dev/null
+++ b/packages/app-lib/src/api/instance/synced_options.rs
@@ -0,0 +1,1691 @@
+use crate::state::instances::adapters::sqlite::instance_rows;
+use crate::state::{
+ InstanceInstallStage, InstanceLink, InstanceMetadata, SyncedOption,
+};
+use crate::util::io;
+use crate::{ErrorKind, State};
+use quartz_nbt::{NbtCompound, NbtList, NbtTag};
+use serde::{Deserialize, Serialize};
+use sha1_smol::Sha1;
+use std::io::Cursor;
+use std::path::{Path, PathBuf};
+
+const COMMAND_HISTORY_FILE: &str = "command_history.txt";
+const HOTBAR_FILE: &str = "hotbar.nbt";
+const COMMAND_HISTORY_LIMIT: usize = 50;
+const COMPONENTS_DATA_VERSION_FLOOR: i32 = 3837;
+const HOTBAR_SCHEMA_VERSION: i64 = 2;
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
+pub struct GlobalSyncedOptions {
+ pub command_history: bool,
+ pub multiplayer_servers: bool,
+ pub creative_hotbars: bool,
+ pub screenshots: bool,
+}
+
+impl GlobalSyncedOptions {
+ pub fn get(self, option: SyncedOption) -> bool {
+ match option {
+ SyncedOption::CommandHistory => self.command_history,
+ SyncedOption::MultiplayerServers => self.multiplayer_servers,
+ SyncedOption::CreativeHotbars => self.creative_hotbars,
+ SyncedOption::Screenshots => self.screenshots,
+ }
+ }
+
+ fn set(&mut self, option: SyncedOption, enabled: bool) {
+ match option {
+ SyncedOption::CommandHistory => self.command_history = enabled,
+ SyncedOption::MultiplayerServers => {
+ self.multiplayer_servers = enabled
+ }
+ SyncedOption::CreativeHotbars => self.creative_hotbars = enabled,
+ SyncedOption::Screenshots => self.screenshots = enabled,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct SyncedOptionCapability {
+ pub option: SyncedOption,
+ pub supported: bool,
+ pub disabled_reason: Option,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct SyncedOptionsOverview {
+ pub global_options: GlobalSyncedOptions,
+ pub capabilities: Vec,
+}
+
+enum CapabilityStatus {
+ Supported,
+ Unsupported(String),
+ Indeterminate(String),
+}
+
+pub(super) struct SyncCheckpoint {
+ pub expected_sha1: String,
+ pub merge_base: Option>,
+ pub source_revision: i64,
+ pub status: CheckpointStatus,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(super) enum CheckpointStatus {
+ Pending,
+ Ready,
+}
+
+impl CheckpointStatus {
+ fn from_str(value: &str) -> Option {
+ match value {
+ "pending" => Some(Self::Pending),
+ "ready" => Some(Self::Ready),
+ _ => None,
+ }
+ }
+}
+
+struct HotbarState {
+ schema_version: i64,
+ revision: i64,
+ nbt: NbtCompound,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum HotbarFamily {
+ Legacy,
+ Components,
+}
+
+impl HotbarFamily {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Legacy => "legacy",
+ Self::Components => "components",
+ }
+ }
+
+ fn other(self) -> Self {
+ match self {
+ Self::Legacy => Self::Components,
+ Self::Components => Self::Legacy,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub(super) enum LinkMode {
+ Symbolic,
+ #[cfg(windows)]
+ Hard,
+ #[cfg(windows)]
+ Copy,
+}
+
+impl LinkMode {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Symbolic => "symbolic",
+ #[cfg(windows)]
+ Self::Hard => "hard",
+ #[cfg(windows)]
+ Self::Copy => "copy",
+ }
+ }
+}
+
+pub async fn get_global_options() -> crate::Result {
+ let state = State::get().await?;
+ get_global_options_with_state(&state).await
+}
+
+pub async fn get_synced_options_folder() -> crate::Result {
+ let state = State::get().await?;
+ create_synced_directories(&state).await?;
+ Ok(synced_options_path(&state))
+}
+
+pub async fn synced_option_needs_base(
+ option: SyncedOption,
+) -> crate::Result {
+ if option == SyncedOption::Screenshots {
+ return Ok(false);
+ }
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ Ok(!canonical_exists(option, &state).await?)
+}
+
+async fn get_global_options_with_state(
+ state: &State,
+) -> crate::Result {
+ let mut options = GlobalSyncedOptions::default();
+ let rows = sqlx::query!(
+ r#"
+ SELECT feature, globally_enabled AS "globally_enabled!: bool"
+ FROM sync_feature_settings
+ "#,
+ )
+ .fetch_all(&state.pool)
+ .await?;
+
+ for row in rows {
+ if let Some(option) = option_from_str(&row.feature) {
+ options.set(option, row.globally_enabled);
+ }
+ }
+
+ Ok(options)
+}
+
+pub async fn get_overview(
+ instance_id: &str,
+) -> crate::Result {
+ let state = State::get().await?;
+ let global_options = get_global_options_with_state(&state).await?;
+ let metadata = crate::state::get_instance(instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?;
+ let mut capabilities = Vec::with_capacity(SyncedOption::ALL.len());
+
+ for option in SyncedOption::ALL {
+ capabilities.push(
+ capability(&metadata, option, global_options.get(option), &state)
+ .await,
+ );
+ }
+
+ Ok(SyncedOptionsOverview {
+ global_options,
+ capabilities,
+ })
+}
+
+pub async fn get_capabilities(
+ instance_id: &str,
+) -> crate::Result> {
+ Ok(get_overview(instance_id).await?.capabilities)
+}
+
+async fn capability(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ global_enabled: bool,
+ state: &State,
+) -> SyncedOptionCapability {
+ let status =
+ capability_status(metadata, option, global_enabled, state).await;
+ let (supported, disabled_reason) = match status {
+ CapabilityStatus::Supported => (true, None),
+ CapabilityStatus::Unsupported(reason)
+ | CapabilityStatus::Indeterminate(reason) => (false, Some(reason)),
+ };
+
+ SyncedOptionCapability {
+ option,
+ supported,
+ disabled_reason,
+ }
+}
+
+async fn capability_status(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ global_enabled: bool,
+ state: &State,
+) -> CapabilityStatus {
+ if !global_enabled {
+ return CapabilityStatus::Unsupported(
+ "This option is disabled in the app's synced options settings."
+ .to_string(),
+ );
+ }
+ if option == SyncedOption::MultiplayerServers
+ && is_linked_server_project(&metadata.link)
+ {
+ return CapabilityStatus::Unsupported(
+ "Multiplayer server syncing is unavailable for linked server-project instances."
+ .to_string(),
+ );
+ }
+ match version_capability(metadata, option, state).await {
+ CapabilityStatus::Supported => CapabilityStatus::Supported,
+ status => status,
+ }
+}
+
+async fn version_capability(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ state: &State,
+) -> CapabilityStatus {
+ if option == SyncedOption::Screenshots {
+ return CapabilityStatus::Supported;
+ }
+
+ let game_version = &metadata.applied_content_set.game_version;
+ let Ok((manifest, version_index)) =
+ crate::launcher::resolve_minecraft_manifest(game_version, state).await
+ else {
+ return CapabilityStatus::Indeterminate(
+ "This instance’s Minecraft version could not be verified, so syncing is unavailable."
+ .to_string(),
+ );
+ };
+ let cutoff_id = match option {
+ // Mojang's manifest does not include Beta 1.8 Pre-release as a
+ // separate entry, so b1.8 is the first resolvable version at that
+ // boundary.
+ SyncedOption::MultiplayerServers => "b1.8",
+ SyncedOption::CreativeHotbars => "1.12",
+ SyncedOption::CommandHistory => "1.20.2",
+ SyncedOption::Screenshots => return CapabilityStatus::Supported,
+ };
+ let Some(cutoff) =
+ manifest.versions.iter().find(|item| item.id == cutoff_id)
+ else {
+ return CapabilityStatus::Indeterminate(
+ "This instance’s Minecraft version could not be verified, so syncing is unavailable."
+ .to_string(),
+ );
+ };
+
+ let version = &manifest.versions[version_index];
+ let release_only_option = matches!(
+ option,
+ SyncedOption::CreativeHotbars | SyncedOption::CommandHistory
+ );
+ let is_supported_release =
+ matches!(&version.type_, daedalus::minecraft::VersionType::Release);
+
+ if version.release_time >= cutoff.release_time
+ && (!release_only_option || is_supported_release)
+ {
+ return CapabilityStatus::Supported;
+ }
+
+ CapabilityStatus::Unsupported(
+ match option {
+ SyncedOption::MultiplayerServers => {
+ "Multiplayer server syncing requires Minecraft Beta 1.8 Pre-release or newer."
+ }
+ SyncedOption::CreativeHotbars => {
+ "Saved creative hotbars require Minecraft 1.12 or newer."
+ }
+ SyncedOption::CommandHistory => {
+ "Command history syncing requires Minecraft 1.20.2 or newer."
+ }
+ SyncedOption::Screenshots => unreachable!(),
+ }
+ .to_string(),
+ )
+}
+
+async fn hotbar_family(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result {
+ let (manifest, version_index) =
+ crate::launcher::resolve_minecraft_manifest(
+ &metadata.applied_content_set.game_version,
+ state,
+ )
+ .await?;
+ let cutoff = manifest
+ .versions
+ .iter()
+ .find(|item| item.id == "1.20.5")
+ .ok_or_else(|| {
+ ErrorKind::LauncherError(
+ "Minecraft 1.20.5 is missing from the version manifest"
+ .to_string(),
+ )
+ })?;
+ Ok(
+ if manifest.versions[version_index].release_time >= cutoff.release_time
+ {
+ HotbarFamily::Components
+ } else {
+ HotbarFamily::Legacy
+ },
+ )
+}
+
+pub async fn set_global_option(
+ option: SyncedOption,
+ enabled: bool,
+ base_instance_id: Option<&str>,
+) -> crate::Result {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ if enabled
+ && option != SyncedOption::Screenshots
+ && !canonical_exists(option, &state).await?
+ {
+ let base_instance_id = base_instance_id.ok_or_else(|| {
+ ErrorKind::InputError(
+ "Choose a base instance before enabling a synced option."
+ .to_string(),
+ )
+ })?;
+ let metadata =
+ crate::state::get_instance(base_instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| {
+ ErrorKind::InputError("Unknown instance".to_string())
+ })?;
+ if sync_files_are_protected(&metadata)
+ || instance_is_running(&metadata, &state).await?
+ {
+ return Err(ErrorKind::InputError(
+ "The base instance must be closed and fully installed."
+ .to_string(),
+ )
+ .into());
+ }
+ let base_capability = capability(&metadata, option, true, &state).await;
+ if !base_capability.supported {
+ return Err(ErrorKind::InputError(
+ base_capability.disabled_reason.unwrap_or_default(),
+ )
+ .into());
+ }
+ seed_from_instance(&metadata, option, &state).await?;
+ }
+
+ let option_name = option.as_str();
+ sqlx::query!(
+ "
+ INSERT INTO sync_feature_settings
+ (feature, globally_enabled, new_instance_default)
+ VALUES (?, ?, 1)
+ ON CONFLICT(feature) DO UPDATE SET
+ globally_enabled = excluded.globally_enabled
+ ",
+ option_name,
+ enabled,
+ )
+ .execute(&state.pool)
+ .await?;
+
+ let instances = crate::state::list_instances(&state.pool).await?;
+ for metadata in instances {
+ if sync_files_are_protected(&metadata)
+ || instance_is_running(&metadata, &state).await?
+ {
+ continue;
+ }
+ if !instance_option_enabled(&metadata, option) {
+ detach_option(&metadata, option, &state).await?;
+ continue;
+ }
+ match capability_status(&metadata, option, enabled, &state).await {
+ CapabilityStatus::Supported => {
+ ensure_option(&metadata, option, &state).await?
+ }
+ CapabilityStatus::Unsupported(_) => {
+ detach_option(&metadata, option, &state).await?
+ }
+ CapabilityStatus::Indeterminate(_) => {}
+ }
+ }
+
+ get_global_options_with_state(&state).await
+}
+
+pub async fn set_instance_option(
+ instance_id: &str,
+ option: SyncedOption,
+ enabled: bool,
+) -> crate::Result {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ let metadata = crate::state::get_instance(instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?;
+ let global = get_global_options_with_state(&state).await?;
+ let can_reconcile = !sync_files_are_protected(&metadata)
+ && !instance_is_running(&metadata, &state).await?;
+ if enabled {
+ let eligibility =
+ capability(&metadata, option, global.get(option), &state).await;
+ if !eligibility.supported {
+ return Err(ErrorKind::InputError(
+ eligibility.disabled_reason.unwrap_or_default(),
+ )
+ .into());
+ }
+ if !canonical_exists(option, &state).await? {
+ if !can_reconcile {
+ return Err(ErrorKind::InputError(
+ "Close the instance before using it as the initial sync source."
+ .to_string(),
+ )
+ .into());
+ }
+ seed_from_instance(&metadata, option, &state).await?;
+ }
+ }
+
+ instance_rows::set_instance_sync_preference(
+ instance_id,
+ option,
+ enabled,
+ &state.pool,
+ )
+ .await?;
+ if can_reconcile {
+ if enabled {
+ ensure_option(&metadata, option, &state).await?;
+ } else {
+ detach_option(&metadata, option, &state).await?;
+ }
+ }
+
+ crate::state::get_instance(instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| {
+ ErrorKind::InputError("Unknown instance".to_string()).into()
+ })
+}
+
+pub async fn reconcile_all() -> crate::Result<()> {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ create_synced_directories(&state).await?;
+ let instances = crate::state::list_instances(&state.pool).await?;
+ for metadata in instances {
+ if let Err(error) =
+ reconcile_instance_with_state(&metadata, &state).await
+ {
+ tracing::warn!(
+ "Failed to reconcile synced options for {}: {error}",
+ metadata.instance.id
+ );
+ }
+ }
+ Ok(())
+}
+
+pub(crate) async fn monitor_persisted_processes() -> crate::Result<()> {
+ let state = State::get().await?;
+ let instance_ids =
+ sqlx::query_scalar!("SELECT DISTINCT instance_id FROM processes",)
+ .fetch_all(&state.pool)
+ .await?;
+ for instance_id in instance_ids {
+ tokio::spawn(async move {
+ loop {
+ let Ok(state) = State::get().await else {
+ break;
+ };
+ let Ok(Some(metadata)) =
+ crate::state::get_instance(&instance_id, &state.pool).await
+ else {
+ break;
+ };
+ match instance_is_running(&metadata, &state).await {
+ Ok(true) => {
+ tokio::time::sleep(std::time::Duration::from_secs(5))
+ .await;
+ }
+ Ok(false) => {
+ if let Err(error) =
+ reconcile_instance(&instance_id).await
+ {
+ tracing::warn!(
+ "Failed to reconcile synced options after a persisted process exited for {instance_id}: {error}"
+ );
+ }
+ break;
+ }
+ Err(error) => {
+ tracing::warn!(
+ "Failed to inspect the persisted process for {instance_id}: {error}"
+ );
+ tokio::time::sleep(std::time::Duration::from_secs(5))
+ .await;
+ }
+ }
+ }
+ });
+ }
+ Ok(())
+}
+
+pub async fn reconcile_instance(instance_id: &str) -> crate::Result<()> {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ let metadata = crate::state::get_instance(instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?;
+ reconcile_instance_with_state(&metadata, &state).await
+}
+
+pub(crate) async fn prepare_instance_update(
+ instance_id: &str,
+) -> crate::Result<()> {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ let metadata = crate::state::get_instance(instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?;
+ for option in [
+ SyncedOption::CommandHistory,
+ SyncedOption::CreativeHotbars,
+ SyncedOption::MultiplayerServers,
+ ] {
+ if instance_option_enabled(&metadata, option) {
+ detach_option(&metadata, option, &state).await?;
+ }
+ }
+ Ok(())
+}
+
+async fn reconcile_instance_with_state(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result<()> {
+ if sync_files_are_protected(metadata)
+ || instance_is_running(metadata, state).await?
+ {
+ return Ok(());
+ }
+ let global = get_global_options_with_state(state).await?;
+ for option in SyncedOption::ALL {
+ if !instance_option_enabled(metadata, option) {
+ detach_option(metadata, option, state).await?;
+ continue;
+ }
+ match capability_status(metadata, option, global.get(option), state)
+ .await
+ {
+ CapabilityStatus::Supported => match option {
+ SyncedOption::CommandHistory => {
+ reconcile_command_history(metadata, state).await?
+ }
+ SyncedOption::CreativeHotbars => {
+ reconcile_hotbar(metadata, state).await?
+ }
+ SyncedOption::MultiplayerServers => {
+ super::synced_servers::reconcile_servers(metadata, state)
+ .await?
+ }
+ SyncedOption::Screenshots => {}
+ },
+ CapabilityStatus::Unsupported(_) => {
+ detach_option(metadata, option, state).await?
+ }
+ CapabilityStatus::Indeterminate(_) => {}
+ }
+ }
+ Ok(())
+}
+
+pub async fn reconcile_changed_file(
+ instance_id: &str,
+ file_name: &str,
+) -> crate::Result<()> {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ let metadata = crate::state::get_instance(instance_id, &state.pool)
+ .await?
+ .ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?;
+ if sync_files_are_protected(&metadata) {
+ return Ok(());
+ }
+ match file_name {
+ COMMAND_HISTORY_FILE => {
+ reconcile_command_history(&metadata, &state).await
+ }
+ HOTBAR_FILE => reconcile_hotbar(&metadata, &state).await,
+ "servers.dat" => {
+ super::synced_servers::reconcile_servers(&metadata, &state).await
+ }
+ _ => Ok(()),
+ }
+}
+
+pub async fn get_command_history() -> crate::Result {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ let path = command_history_path(&state);
+ if !path.exists() {
+ return Ok(String::new());
+ }
+ Ok(String::from_utf8_lossy(&io::read(path).await?).into_owned())
+}
+
+pub async fn set_command_history(contents: &str) -> crate::Result {
+ let state = State::get().await?;
+ let _guard = state.lock_synced_options().await;
+ create_synced_directories(&state).await?;
+ let normalized = normalize_command_history(contents);
+ io::write(command_history_path(&state), normalized.as_bytes()).await?;
+ refresh_command_history_links(&state).await?;
+ Ok(normalized)
+}
+
+pub fn synced_options_path(state: &State) -> PathBuf {
+ state.directories.synced_options_dir()
+}
+
+pub(crate) async fn remove_generated_instance_files(
+ instance_id: &str,
+ state: &State,
+) -> crate::Result<()> {
+ let instance_id = safe_instance_id(instance_id);
+ for path in [
+ synced_options_path(state)
+ .join("hotbars/generated/legacy")
+ .join(&instance_id),
+ synced_options_path(state)
+ .join("hotbars/generated/components")
+ .join(&instance_id),
+ synced_options_path(state)
+ .join("servers/generated")
+ .join(&instance_id),
+ ] {
+ if path.exists() {
+ io::remove_dir_all(path).await?;
+ }
+ }
+ Ok(())
+}
+
+async fn create_synced_directories(state: &State) -> crate::Result<()> {
+ for path in [
+ synced_options_path(state),
+ synced_options_path(state).join("hotbars/generated/legacy"),
+ synced_options_path(state).join("hotbars/generated/components"),
+ synced_options_path(state).join("servers/generated"),
+ ] {
+ io::create_dir_all(path).await?;
+ }
+ Ok(())
+}
+
+async fn seed_from_instance(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ state: &State,
+) -> crate::Result<()> {
+ create_synced_directories(state).await?;
+ let instance_dir = instance_dir(metadata, state);
+ match option {
+ SyncedOption::CommandHistory => {
+ let path = instance_dir.join(COMMAND_HISTORY_FILE);
+ let contents = if path.exists() {
+ String::from_utf8_lossy(&io::read(path).await?).into_owned()
+ } else {
+ String::new()
+ };
+ io::write(
+ command_history_path(state),
+ normalize_command_history(&contents),
+ )
+ .await?;
+ }
+ SyncedOption::CreativeHotbars => {
+ let path = instance_dir.join(HOTBAR_FILE);
+ let family = hotbar_family(metadata, state).await?;
+ let root = if path.exists() {
+ read_nbt_file(&path).await?
+ } else {
+ empty_hotbar_root()
+ };
+ let mut sync_state = read_hotbar_state(state).await?;
+ let merge_base = empty_hotbar_root();
+ if merge_hotbar_family(
+ &mut sync_state.nbt,
+ family,
+ &merge_base,
+ &root,
+ ) {
+ increment_hotbar_revision(&mut sync_state);
+ }
+ write_hotbar_state(state, &sync_state).await?;
+ regenerate_hotbars(state).await?;
+ }
+ SyncedOption::MultiplayerServers => {
+ super::synced_servers::seed_servers(metadata, state).await?;
+ }
+ SyncedOption::Screenshots => {}
+ }
+ Ok(())
+}
+
+async fn ensure_option(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ state: &State,
+) -> crate::Result<()> {
+ match option {
+ SyncedOption::CommandHistory => {
+ ensure_command_history(metadata, state).await
+ }
+ SyncedOption::CreativeHotbars => ensure_hotbar(metadata, state).await,
+ SyncedOption::MultiplayerServers => {
+ super::synced_servers::ensure_servers(metadata, state).await
+ }
+ SyncedOption::Screenshots => Ok(()),
+ }
+}
+
+async fn detach_option(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ state: &State,
+) -> crate::Result<()> {
+ let instance_dir = instance_dir(metadata, state);
+ match option {
+ SyncedOption::CommandHistory => {
+ detach_link(
+ &command_history_path(state),
+ &instance_dir.join(COMMAND_HISTORY_FILE),
+ )
+ .await
+ }
+ SyncedOption::CreativeHotbars => {
+ let target = instance_dir.join(HOTBAR_FILE);
+ let family = hotbar_family(metadata, state).await.ok();
+ let source = family
+ .map(|family| {
+ generated_hotbar_path(state, family, &metadata.instance.id)
+ })
+ .unwrap_or_else(|| target.clone());
+ detach_link(&source, &target).await
+ }
+ SyncedOption::MultiplayerServers => {
+ super::synced_servers::detach_servers(metadata, state).await
+ }
+ SyncedOption::Screenshots => Ok(()),
+ }
+}
+
+async fn canonical_exists(
+ option: SyncedOption,
+ state: &State,
+) -> crate::Result {
+ Ok(match option {
+ SyncedOption::CommandHistory => command_history_path(state).exists(),
+ SyncedOption::CreativeHotbars => hotbar_state_exists(state).await?,
+ SyncedOption::MultiplayerServers => {
+ super::synced_servers::canonical_exists(state).await?
+ }
+ SyncedOption::Screenshots => true,
+ })
+}
+
+async fn ensure_command_history(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result<()> {
+ create_synced_directories(state).await?;
+ let canonical = command_history_path(state);
+ if !canonical.exists() {
+ let local = instance_dir(metadata, state).join(COMMAND_HISTORY_FILE);
+ let contents = if local.exists() {
+ String::from_utf8_lossy(&io::read(&local).await?).into_owned()
+ } else {
+ String::new()
+ };
+ io::write(&canonical, normalize_command_history(&contents)).await?;
+ }
+ let target = instance_dir(metadata, state).join(COMMAND_HISTORY_FILE);
+ let canonical_bytes = io::read(&canonical).await?;
+ let expected = sha1_bytes(&canonical_bytes);
+ begin_checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CommandHistory,
+ "default",
+ &expected,
+ None,
+ 0,
+ state,
+ )
+ .await?;
+ let mode = ensure_link(&canonical, &target).await?;
+ finish_checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CommandHistory,
+ "default",
+ mode,
+ state,
+ )
+ .await
+}
+
+async fn reconcile_command_history(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result<()> {
+ if !option_effective(metadata, SyncedOption::CommandHistory, state).await? {
+ return Ok(());
+ }
+ let local = instance_dir(metadata, state).join(COMMAND_HISTORY_FILE);
+ if !local.exists() {
+ return ensure_command_history(metadata, state).await;
+ }
+ let symlink = tokio::fs::symlink_metadata(&local)
+ .await
+ .map(|metadata| metadata.file_type().is_symlink())
+ .unwrap_or(false);
+ let current_checkpoint = checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CommandHistory,
+ "default",
+ state,
+ )
+ .await?;
+ if current_checkpoint
+ .as_ref()
+ .is_some_and(|value| value.status == CheckpointStatus::Pending)
+ {
+ return ensure_command_history(metadata, state).await;
+ }
+ let actual = sha1_file(&local).await?;
+ let expected = current_checkpoint.map(|value| value.expected_sha1);
+ if !symlink && expected.as_deref() != Some(actual.as_str()) {
+ let contents =
+ String::from_utf8_lossy(&io::read(&local).await?).into_owned();
+ io::write(
+ command_history_path(state),
+ normalize_command_history(&contents),
+ )
+ .await?;
+ refresh_command_history_links(state).await?;
+ } else {
+ ensure_command_history(metadata, state).await?;
+ }
+ Ok(())
+}
+
+async fn refresh_command_history_links(state: &State) -> crate::Result<()> {
+ let instances = crate::state::list_instances(&state.pool).await?;
+ for metadata in instances {
+ if option_effective(&metadata, SyncedOption::CommandHistory, state)
+ .await?
+ {
+ ensure_command_history(&metadata, state).await?;
+ }
+ }
+ Ok(())
+}
+
+fn normalize_command_history(contents: &str) -> String {
+ let lines = contents.lines().collect::>();
+ let start = lines.len().saturating_sub(COMMAND_HISTORY_LIMIT);
+ let mut normalized = lines[start..].join("\n");
+ if !normalized.is_empty() {
+ normalized.push('\n');
+ }
+ normalized
+}
+
+async fn ensure_hotbar(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result<()> {
+ create_synced_directories(state).await?;
+ if !hotbar_state_exists(state).await? {
+ seed_from_instance(metadata, SyncedOption::CreativeHotbars, state)
+ .await?;
+ }
+ write_hotbar_projection(metadata, state).await
+}
+
+async fn reconcile_hotbar(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result<()> {
+ if !option_effective(metadata, SyncedOption::CreativeHotbars, state).await?
+ {
+ return Ok(());
+ }
+ let local = instance_dir(metadata, state).join(HOTBAR_FILE);
+ if !local.exists() {
+ return ensure_hotbar(metadata, state).await;
+ }
+ let family = hotbar_family(metadata, state).await?;
+ let current_checkpoint = checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CreativeHotbars,
+ family.as_str(),
+ state,
+ )
+ .await?;
+ if current_checkpoint
+ .as_ref()
+ .is_some_and(|value| value.status == CheckpointStatus::Pending)
+ {
+ return write_hotbar_projection(metadata, state).await;
+ }
+ let actual = sha1_file(&local).await?;
+ let mut sync_state = read_hotbar_state(state).await?;
+ if current_checkpoint
+ .as_ref()
+ .map(|value| value.expected_sha1.as_str())
+ == Some(actual.as_str())
+ {
+ if current_checkpoint
+ .as_ref()
+ .is_some_and(|value| value.source_revision == sync_state.revision)
+ {
+ return Ok(());
+ }
+ return write_hotbar_projection(metadata, state).await;
+ }
+
+ let (changed_family, checkpoint) = if current_checkpoint.is_none() {
+ let previous_family = family.other();
+ let previous_checkpoint = checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CreativeHotbars,
+ previous_family.as_str(),
+ state,
+ )
+ .await?;
+ if let Some(previous_checkpoint) = previous_checkpoint {
+ if previous_checkpoint.status == CheckpointStatus::Pending
+ || previous_checkpoint.expected_sha1 == actual
+ {
+ return write_hotbar_projection(metadata, state).await;
+ }
+ (previous_family, Some(previous_checkpoint))
+ } else {
+ (family, None)
+ }
+ } else {
+ (family, current_checkpoint)
+ };
+
+ let changed = read_nbt_file(&local).await?;
+ let merge_base = checkpoint
+ .and_then(|value| value.merge_base)
+ .map(nbt_from_bytes)
+ .transpose()?
+ .unwrap_or_else(|| hotbar_family_root(&sync_state.nbt, changed_family));
+ if merge_hotbar_family(
+ &mut sync_state.nbt,
+ changed_family,
+ &merge_base,
+ &changed,
+ ) {
+ increment_hotbar_revision(&mut sync_state);
+ write_hotbar_state(state, &sync_state).await?;
+ }
+ regenerate_hotbars(state).await
+}
+
+fn merge_hotbar_family(
+ state: &mut NbtCompound,
+ family: HotbarFamily,
+ merge_base: &NbtCompound,
+ changed: &NbtCompound,
+) -> bool {
+ let family_key = family_state_key(family);
+ let mut current = state
+ .get::<_, &NbtCompound>(family_key)
+ .ok()
+ .cloned()
+ .unwrap_or_else(empty_hotbar_root);
+ let other_family = match family {
+ HotbarFamily::Legacy => HotbarFamily::Components,
+ HotbarFamily::Components => HotbarFamily::Legacy,
+ };
+ let other_key = family_state_key(other_family);
+ let other = state.get::<_, &NbtCompound>(other_key).ok().cloned();
+ let seed_components_with_legacy =
+ other.is_none() && family == HotbarFamily::Legacy;
+ let mut other_root = if seed_components_with_legacy {
+ (*changed).clone()
+ } else {
+ other.unwrap_or_else(empty_hotbar_root)
+ };
+ let mut revisions = state
+ .get::<_, &NbtCompound>("Revisions")
+ .ok()
+ .cloned()
+ .unwrap_or_default();
+ let family_versions_key = family_data_versions_key(family);
+ let other_versions_key = family_data_versions_key(other_family);
+ let mut family_versions = state
+ .get::<_, &NbtCompound>(family_versions_key)
+ .ok()
+ .cloned()
+ .unwrap_or_default();
+ let mut other_versions = state
+ .get::<_, &NbtCompound>(other_versions_key)
+ .ok()
+ .cloned()
+ .unwrap_or_default();
+ let writer_data_version = hotbar_data_version(changed).max(match family {
+ HotbarFamily::Legacy => 1,
+ HotbarFamily::Components => COMPONENTS_DATA_VERSION_FLOOR,
+ });
+ let current_data_version = hotbar_data_version(¤t);
+ let other_data_version = hotbar_data_version(&other_root);
+ let mut changed_any = false;
+
+ for slot in 0..81 {
+ let old_slot = hotbar_slot(merge_base, slot);
+ let new_slot = hotbar_slot(changed, slot);
+ if old_slot == new_slot {
+ continue;
+ }
+ let current_origin = family_versions
+ .get::<_, i32>(&slot.to_string())
+ .unwrap_or(current_data_version);
+ if writer_data_version > 0
+ && current_origin > 0
+ && writer_data_version < current_origin
+ {
+ continue;
+ }
+ set_hotbar_slot_optional(&mut current, slot, new_slot.clone());
+ family_versions.insert(slot.to_string(), writer_data_version);
+ let revision = revisions
+ .get::<_, i64>(&slot.to_string())
+ .unwrap_or(0)
+ .saturating_add(1);
+ revisions.insert(slot.to_string(), revision);
+ if seed_components_with_legacy {
+ other_versions.insert(slot.to_string(), writer_data_version);
+ } else if let Some(slot_value) = new_slot
+ .and_then(|value| convert_hotbar_slot(value, family, other_family))
+ {
+ let other_origin = other_versions
+ .get::<_, i32>(&slot.to_string())
+ .unwrap_or(other_data_version);
+ if writer_data_version <= 0
+ || other_origin <= 0
+ || writer_data_version >= other_origin
+ {
+ set_hotbar_slot(&mut other_root, slot, slot_value);
+ other_versions.insert(slot.to_string(), writer_data_version);
+ }
+ }
+ changed_any = true;
+ }
+
+ if writer_data_version > current_data_version {
+ current.insert("DataVersion", writer_data_version);
+ changed_any = true;
+ }
+ if seed_components_with_legacy
+ && writer_data_version > hotbar_data_version(&other_root)
+ {
+ other_root.insert("DataVersion", writer_data_version);
+ }
+
+ state.insert(family_key, current);
+ state.insert(other_key, other_root);
+ state.insert("Revisions", revisions);
+ state.insert(family_versions_key, family_versions);
+ state.insert(other_versions_key, other_versions);
+ changed_any
+}
+
+fn convert_hotbar_slot(
+ value: NbtTag,
+ from: HotbarFamily,
+ to: HotbarFamily,
+) -> Option {
+ if from == to {
+ return Some(value);
+ }
+ let NbtTag::Compound(item) = value else {
+ return None;
+ };
+ if item.is_empty() {
+ return Some(NbtCompound::new().into());
+ }
+ let id = item.get::<_, &str>("id").ok()?.to_string();
+
+ match (from, to) {
+ (HotbarFamily::Legacy, HotbarFamily::Components) => {
+ if item.contains_key("tag") {
+ return None;
+ }
+ let count = item
+ .get::<_, i8>("Count")
+ .map(i32::from)
+ .or_else(|_| item.get::<_, i32>("Count"))
+ .unwrap_or(1);
+ let mut converted = NbtCompound::new();
+ converted.insert("id", id);
+ converted.insert("count", count);
+ Some(converted.into())
+ }
+ (HotbarFamily::Components, HotbarFamily::Legacy) => {
+ if item
+ .get::<_, &NbtCompound>("components")
+ .is_ok_and(|components| !components.is_empty())
+ {
+ return None;
+ }
+ let count = item.get::<_, i32>("count").unwrap_or(1);
+ let count = i8::try_from(count).ok()?;
+ let mut converted = NbtCompound::new();
+ converted.insert("id", id);
+ converted.insert("Count", count);
+ Some(converted.into())
+ }
+ _ => Some(item.into()),
+ }
+}
+
+fn hotbar_slot(root: &NbtCompound, slot: usize) -> Option {
+ let toolbar = (slot / 9).to_string();
+ let position = slot % 9;
+ root.get::<_, &NbtList>(&toolbar)
+ .ok()
+ .and_then(|list| list.as_ref().get(position).cloned())
+}
+
+fn set_hotbar_slot(root: &mut NbtCompound, slot: usize, value: NbtTag) {
+ let toolbar = (slot / 9).to_string();
+ let position = slot % 9;
+ let mut list = root
+ .get::<_, &NbtList>(&toolbar)
+ .ok()
+ .cloned()
+ .unwrap_or_default();
+ while list.len() < 9 {
+ list.push(NbtCompound::new());
+ }
+ list[position] = value;
+ root.insert(toolbar, list);
+}
+
+fn set_hotbar_slot_optional(
+ root: &mut NbtCompound,
+ slot: usize,
+ value: Option,
+) {
+ set_hotbar_slot(
+ root,
+ slot,
+ value.unwrap_or_else(|| NbtCompound::new().into()),
+ );
+}
+
+fn hotbar_data_version(root: &NbtCompound) -> i32 {
+ root.get::<_, i32>("DataVersion").unwrap_or(0)
+}
+
+fn family_data_versions_key(family: HotbarFamily) -> &'static str {
+ match family {
+ HotbarFamily::Legacy => "LegacyDataVersions",
+ HotbarFamily::Components => "ComponentsDataVersions",
+ }
+}
+
+async fn regenerate_hotbars(state: &State) -> crate::Result<()> {
+ let instances = crate::state::list_instances(&state.pool).await?;
+ for metadata in instances {
+ if option_effective(&metadata, SyncedOption::CreativeHotbars, state)
+ .await?
+ {
+ write_hotbar_projection(&metadata, state).await?;
+ }
+ }
+ Ok(())
+}
+
+async fn write_hotbar_projection(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result<()> {
+ let sync_state = read_hotbar_state(state).await?;
+ let family = hotbar_family(metadata, state).await?;
+ let root = hotbar_family_root(&sync_state.nbt, family);
+ let bytes = nbt_to_bytes(&root)?;
+ let expected = sha1_bytes(&bytes);
+ let generated = generated_hotbar_path(state, family, &metadata.instance.id);
+ begin_checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CreativeHotbars,
+ family.as_str(),
+ &expected,
+ Some(&bytes),
+ sync_state.revision,
+ state,
+ )
+ .await?;
+ if let Some(parent) = generated.parent() {
+ io::create_dir_all(parent).await?;
+ }
+ io::write(&generated, &bytes).await?;
+ let local = instance_dir(metadata, state).join(HOTBAR_FILE);
+ let mode = ensure_link(&generated, &local).await?;
+ finish_checkpoint(
+ &metadata.instance.id,
+ SyncedOption::CreativeHotbars,
+ family.as_str(),
+ mode,
+ state,
+ )
+ .await?;
+ remove_other_hotbar_checkpoint(&metadata.instance.id, family, state).await
+}
+
+async fn remove_other_hotbar_checkpoint(
+ instance_id: &str,
+ family: HotbarFamily,
+ state: &State,
+) -> crate::Result<()> {
+ let variant = family.as_str();
+ sqlx::query!(
+ "
+ DELETE FROM instance_sync_checkpoints
+ WHERE instance_id = ?
+ AND feature = 'creative_hotbars'
+ AND variant != ?
+ ",
+ instance_id,
+ variant,
+ )
+ .execute(&state.pool)
+ .await?;
+ Ok(())
+}
+
+fn empty_hotbar_root() -> NbtCompound {
+ let mut root = NbtCompound::new();
+ for toolbar in 0..9 {
+ let mut items = NbtList::new();
+ for _ in 0..9 {
+ items.push(NbtCompound::new());
+ }
+ root.insert(toolbar.to_string(), items);
+ }
+ root
+}
+
+async fn read_hotbar_state(state: &State) -> crate::Result {
+ let row = sqlx::query!(
+ r#"
+ SELECT schema_version AS "schema_version!: i64",
+ revision AS "revision!: i64", nbt
+ FROM synced_hotbar_state
+ WHERE singleton = 1
+ "#,
+ )
+ .fetch_optional(&state.pool)
+ .await?;
+ if let Some(row) = row {
+ return Ok(HotbarState {
+ schema_version: row.schema_version,
+ revision: row.revision,
+ nbt: nbt_from_bytes(row.nbt)?,
+ });
+ }
+ Ok(HotbarState {
+ schema_version: HOTBAR_SCHEMA_VERSION,
+ revision: 0,
+ nbt: NbtCompound::new(),
+ })
+}
+
+async fn hotbar_state_exists(state: &State) -> crate::Result {
+ Ok(sqlx::query_scalar!(
+ r#"
+ SELECT EXISTS(
+ SELECT 1 FROM synced_hotbar_state WHERE singleton = 1
+ ) AS "exists!: bool"
+ "#,
+ )
+ .fetch_one(&state.pool)
+ .await?)
+}
+
+fn hotbar_family_root(
+ state: &NbtCompound,
+ family: HotbarFamily,
+) -> NbtCompound {
+ let legacy = state
+ .get::<_, &NbtCompound>("Legacy")
+ .ok()
+ .cloned()
+ .unwrap_or_else(empty_hotbar_root);
+ match family {
+ HotbarFamily::Legacy => legacy,
+ HotbarFamily::Components => state
+ .get::<_, &NbtCompound>("Components")
+ .ok()
+ .cloned()
+ .unwrap_or(legacy),
+ }
+}
+
+fn increment_hotbar_revision(state: &mut HotbarState) {
+ state.schema_version = HOTBAR_SCHEMA_VERSION;
+ state.revision = state.revision.saturating_add(1);
+}
+
+async fn write_hotbar_state(
+ state: &State,
+ hotbar_state: &HotbarState,
+) -> crate::Result<()> {
+ let bytes = nbt_to_bytes(&hotbar_state.nbt)?;
+ sqlx::query!(
+ "
+ INSERT INTO synced_hotbar_state
+ (singleton, schema_version, revision, nbt)
+ VALUES (1, ?, ?, ?)
+ ON CONFLICT(singleton) DO UPDATE SET
+ schema_version = excluded.schema_version,
+ revision = excluded.revision,
+ nbt = excluded.nbt
+ ",
+ hotbar_state.schema_version,
+ hotbar_state.revision,
+ bytes,
+ )
+ .execute(&state.pool)
+ .await?;
+ Ok(())
+}
+
+fn family_state_key(family: HotbarFamily) -> &'static str {
+ match family {
+ HotbarFamily::Legacy => "Legacy",
+ HotbarFamily::Components => "Components",
+ }
+}
+
+fn command_history_path(state: &State) -> PathBuf {
+ synced_options_path(state).join(COMMAND_HISTORY_FILE)
+}
+
+fn generated_hotbar_path(
+ state: &State,
+ family: HotbarFamily,
+ instance_id: &str,
+) -> PathBuf {
+ synced_options_path(state)
+ .join("hotbars/generated")
+ .join(family.as_str())
+ .join(safe_instance_id(instance_id))
+ .join(HOTBAR_FILE)
+}
+
+pub(super) fn safe_instance_id(instance_id: &str) -> String {
+ instance_id.replace([':', '/', '\\'], "_")
+}
+
+pub(super) fn instance_dir(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> PathBuf {
+ state
+ .directories
+ .instances_dir()
+ .join(&metadata.instance.path)
+}
+
+async fn option_effective(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+ state: &State,
+) -> crate::Result {
+ if sync_files_are_protected(metadata)
+ || instance_is_running(metadata, state).await?
+ {
+ return Ok(false);
+ }
+ let global = get_global_options_with_state(state).await?;
+ Ok(instance_option_enabled(metadata, option)
+ && capability(metadata, option, global.get(option), state)
+ .await
+ .supported)
+}
+
+pub(super) fn sync_files_are_protected(metadata: &InstanceMetadata) -> bool {
+ matches!(
+ metadata.instance.install_stage,
+ InstanceInstallStage::MinecraftInstalling
+ | InstanceInstallStage::PackInstalling
+ )
+}
+
+pub(super) async fn instance_is_running(
+ metadata: &InstanceMetadata,
+ state: &State,
+) -> crate::Result {
+ crate::state::instance_has_running_process(&metadata.instance.id, state)
+ .await
+}
+
+pub(super) fn instance_option_enabled(
+ metadata: &InstanceMetadata,
+ option: SyncedOption,
+) -> bool {
+ match option {
+ SyncedOption::CommandHistory => metadata.synced_options.command_history,
+ SyncedOption::MultiplayerServers => {
+ metadata.synced_options.multiplayer_servers
+ }
+ SyncedOption::CreativeHotbars => {
+ metadata.synced_options.creative_hotbars
+ }
+ SyncedOption::Screenshots => metadata.synced_options.screenshots,
+ }
+}
+
+fn is_linked_server_project(link: &InstanceLink) -> bool {
+ matches!(
+ link,
+ InstanceLink::ServerProject { .. }
+ | InstanceLink::ServerProjectModpack { .. }
+ | InstanceLink::ModrinthHosting { .. }
+ )
+}
+
+fn option_from_str(value: &str) -> Option {
+ match value {
+ "command_history" => Some(SyncedOption::CommandHistory),
+ "multiplayer_servers" => Some(SyncedOption::MultiplayerServers),
+ "creative_hotbars" => Some(SyncedOption::CreativeHotbars),
+ "screenshots" => Some(SyncedOption::Screenshots),
+ _ => None,
+ }
+}
+
+pub(super) async fn ensure_link(
+ source: &Path,
+ target: &Path,
+) -> crate::Result {
+ if let Some(parent) = target.parent() {
+ io::create_dir_all(parent).await?;
+ }
+ if tokio::fs::symlink_metadata(target)
+ .await
+ .is_ok_and(|metadata| metadata.file_type().is_symlink())
+ && tokio::fs::read_link(target)
+ .await
+ .is_ok_and(|current| current == source)
+ {
+ return Ok(LinkMode::Symbolic);
+ }
+ if tokio::fs::symlink_metadata(target).await.is_ok() {
+ io::remove_file(target).await?;
+ }
+
+ #[cfg(unix)]
+ {
+ tokio::fs::symlink(source, target).await?;
+ Ok(LinkMode::Symbolic)
+ }
+ #[cfg(windows)]
+ {
+ if tokio::fs::symlink_file(source, target).await.is_ok() {
+ return Ok(LinkMode::Symbolic);
+ }
+ if tokio::fs::hard_link(source, target).await.is_ok() {
+ return Ok(LinkMode::Hard);
+ }
+ io::copy(source, target).await?;
+ Ok(LinkMode::Copy)
+ }
+}
+
+pub(super) async fn detach_link(
+ source: &Path,
+ target: &Path,
+) -> crate::Result<()> {
+ let target_metadata = tokio::fs::symlink_metadata(target).await.ok();
+ let contents = if target_metadata.is_some() && target.exists() {
+ Some(io::read(target).await?)
+ } else if source.exists() {
+ Some(io::read(source).await?)
+ } else {
+ None
+ };
+ if target_metadata.is_some() {
+ io::remove_file(target).await?;
+ }
+ if let Some(contents) = contents {
+ io::write(target, contents).await?;
+ }
+ Ok(())
+}
+
+pub(super) async fn begin_checkpoint(
+ instance_id: &str,
+ option: SyncedOption,
+ variant: &str,
+ expected_sha1: &str,
+ merge_base: Option<&[u8]>,
+ source_revision: i64,
+ state: &State,
+) -> crate::Result<()> {
+ let option_name = option.as_str();
+ sqlx::query!(
+ "
+ INSERT INTO instance_sync_checkpoints
+ (instance_id, feature, variant, expected_sha1, merge_base,
+ source_revision, status, link_mode)
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL)
+ ON CONFLICT(instance_id, feature, variant) DO UPDATE SET
+ expected_sha1 = excluded.expected_sha1,
+ merge_base = excluded.merge_base,
+ source_revision = excluded.source_revision,
+ status = 'pending',
+ link_mode = NULL
+ ",
+ instance_id,
+ option_name,
+ variant,
+ expected_sha1,
+ merge_base,
+ source_revision,
+ )
+ .execute(&state.pool)
+ .await?;
+ Ok(())
+}
+
+pub(super) async fn finish_checkpoint(
+ instance_id: &str,
+ option: SyncedOption,
+ variant: &str,
+ mode: LinkMode,
+ state: &State,
+) -> crate::Result<()> {
+ let option_name = option.as_str();
+ let link_mode = mode.as_str();
+ sqlx::query!(
+ "
+ UPDATE instance_sync_checkpoints
+ SET status = 'ready', link_mode = ?
+ WHERE instance_id = ? AND feature = ? AND variant = ?
+ ",
+ link_mode,
+ instance_id,
+ option_name,
+ variant,
+ )
+ .execute(&state.pool)
+ .await?;
+ Ok(())
+}
+
+pub(super) async fn checkpoint(
+ instance_id: &str,
+ option: SyncedOption,
+ variant: &str,
+ state: &State,
+) -> crate::Result