From 7a72556c757fb3c9cab79c5b5919bd34807c8b20 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 1 Sep 2026 12:29:31 +0100 Subject: [PATCH 01/33] draft: options.txt syncing --- .../instances/InstancesSyncedSettings.vue | 221 +++++- .../game-settings/GameSettingRow.vue | 260 +++++++ .../game-settings/GameSettingsModal.vue | 664 ++++++++++++++++++ .../game-settings/game-setting-editors.ts | 147 ++++ .../game-settings/game-setting-messages.ts | 468 ++++++++++++ apps/app-frontend/src/helpers/game-options.ts | 176 +++++ apps/app-frontend/src/helpers/instance.ts | 3 + apps/app-frontend/src/helpers/types.d.ts | 1 + .../synced-options-settings.vue | 19 +- apps/app/build.rs | 4 + apps/app/src/api/instance.rs | 30 + ...926ef4cd958146c16797d589f347626dbab7e.json | 12 + ...fbd69200515a3e90400d7d002acd86cf03a8c.json | 12 + ...165d10b9a34a68eb4e85b7380eee167db92b0.json | 50 ++ ...edac01d302922e9f44c4375046cc7ab5c367b.json | 12 + ...f719165ad8b7fe31ec84b8a6b31a45a00e634.json | 26 + ...5ed33f902980977f795d38f062bf7438c8334.json | 12 + ...6c0c2b1a888ea6c6c0542f4713b03bbafea7b.json | 12 + ...13d164438273c75ad4f22a4b81003e737da67.json | 32 + ...40087e771dfafc968144db5978c570e68a884.json | 12 + ...55a1a303360c04d865980ce37248b05ea3994.json | 12 + ...5dcf59f91565296e9ae97d210249cdcc29cfc.json | 26 + ...4bbbe293834ffd7b86b260a7bccb6c8028d92.json | 12 + ...f2dcb14f952cc7bcbb4661ff8abd4e168ee1c.json | 12 + ...bd64a1c6bb5fc5f3c0082500afdcfa7340efa.json | 63 +- ...40fb48d24b62a5ddddfc6de2f14b80d2bfc7c.json | 20 + ...fa810ad8ca5f246c4d81ad4bcd5866ee10f2c.json | 12 + ...f046e42ee66e31d99b8ff28917f58b4a937f6.json | 12 + ...1be42650f58edf2045fe1b85d418a537382e2.json | 12 + ...08acd794a400abeca0027cc6b21f5bd466e79.json | 12 + ...6d3d8c7b10211f275e0db81fa1eda5196c2e5.json | 12 + ...3a4767d4e9a28626c754bb4e8e7030b73ead7.json | 12 + ...65d017d21a6c6476d0f2ae09c076d75f0dda3.json | 4 +- ...2335be017bc4398a15149753bf31b2acac938.json | 12 + ...ca83c2626f097de53ec537f59b76fdb21eccb.json | 12 + ...98455c6db66d485824f27ab198c3f00de1770.json | 63 +- ...13a985f60f680c77885917f560760b5818b71.json | 12 + ...513dca33a6931b29489f59afddfc325d6258a.json | 12 + ...1f2783cbdd25a4c65031de3c0b2ed78acc0a2.json | 12 + ...b977c166ea70f3ec8f5a2aa8f067fa2f44733.json | 12 + ...ed581944cc9d9c335d1bc1c070863f93b276f.json | 12 + ...0ce434011c2e6bf3b76e0f8dd7d162ebb95ae.json | 12 + ...f5fe9c28436566c4d7b74bbcb9a82cb553959.json | 12 + ...8ee2852552b5b06a38deb6311a3bf2098cee8.json | 12 + ...d55605696d030a31be35669d8d0ee129c3f49.json | 12 + ...b1beaaca1254a4abcf2955c7af3bc5086f9cc.json | 20 + ...9103b04b9e94bbd663ec205002e3ab2ff6756.json | 12 + .../20260827120000_game-options-sync.sql | 65 ++ packages/app-lib/src/api/instance.rs | 20 +- .../api/instance/game_options/api_types.rs | 213 ++++++ .../instance/game_options/instance_support.rs | 512 ++++++++++++++ .../instance/game_options/launch_overrides.rs | 145 ++++ .../src/api/instance/game_options/mod.rs | 64 ++ .../api/instance/game_options/options_file.rs | 317 +++++++++ .../api/instance/game_options/pack_updates.rs | 236 +++++++ .../game_options/read_instance_changes.rs | 432 ++++++++++++ .../instance/game_options/settings_editor.rs | 518 ++++++++++++++ .../instance/game_options/source_selection.rs | 421 +++++++++++ .../supported_settings/accessibility.rs | 51 ++ .../game_options/supported_settings/chat.rs | 57 ++ .../supported_settings/controls.rs | 221 ++++++ .../supported_settings/known_vanilla_keys.rs | 269 +++++++ .../supported_settings/legacy_key_bindings.rs | 112 +++ .../game_options/supported_settings/mod.rs | 264 +++++++ .../game_options/supported_settings/sound.rs | 74 ++ .../supported_settings/version_changes.rs | 660 +++++++++++++++++ .../game_options/supported_settings/video.rs | 162 +++++ .../game_options/write_shared_settings.rs | 353 ++++++++++ .../src/api/instance/synced_options/files.rs | 23 + .../src/api/instance/synced_options/mod.rs | 9 +- .../instance/synced_options/orchestration.rs | 242 ++++++- .../app-lib/src/api/pack/install_mrpack.rs | 48 +- packages/app-lib/src/install/runner.rs | 1 + .../app-lib/src/install/shared_instance.rs | 26 + packages/app-lib/src/launcher/mod.rs | 57 +- .../adapters/sqlite/instance_rows.rs | 2 + .../state/instances/commands/game_options.rs | 136 ++++ .../src/state/instances/commands/mod.rs | 3 + packages/app-lib/src/state/instances/mod.rs | 9 + .../src/state/instances/model/game_options.rs | 93 +++ .../src/state/instances/model/instance.rs | 7 +- .../app-lib/src/state/instances/model/mod.rs | 7 + .../app-lib/src/state/instances/watcher.rs | 70 +- packages/app-lib/src/state/mod.rs | 9 + 84 files changed, 8366 insertions(+), 163 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/settings/instances/game-settings/GameSettingRow.vue create mode 100644 apps/app-frontend/src/components/ui/settings/instances/game-settings/GameSettingsModal.vue create mode 100644 apps/app-frontend/src/components/ui/settings/instances/game-settings/game-setting-editors.ts create mode 100644 apps/app-frontend/src/components/ui/settings/instances/game-settings/game-setting-messages.ts create mode 100644 apps/app-frontend/src/helpers/game-options.ts create mode 100644 packages/app-lib/.sqlx/query-006f7c73e672da9d9d607e58ce0926ef4cd958146c16797d589f347626dbab7e.json create mode 100644 packages/app-lib/.sqlx/query-06e155e02c8d097e32f18b161b3fbd69200515a3e90400d7d002acd86cf03a8c.json create mode 100644 packages/app-lib/.sqlx/query-083e4766729cc70fd43bcc2b920165d10b9a34a68eb4e85b7380eee167db92b0.json create mode 100644 packages/app-lib/.sqlx/query-117563d32f10e1e0f678d606aa5edac01d302922e9f44c4375046cc7ab5c367b.json create mode 100644 packages/app-lib/.sqlx/query-1499ce88ed936711e88354f6123f719165ad8b7fe31ec84b8a6b31a45a00e634.json create mode 100644 packages/app-lib/.sqlx/query-1d66f3fb56c2e74146f8bc3b2985ed33f902980977f795d38f062bf7438c8334.json create mode 100644 packages/app-lib/.sqlx/query-253ab2ee7d3504fbc1b098739d06c0c2b1a888ea6c6c0542f4713b03bbafea7b.json create mode 100644 packages/app-lib/.sqlx/query-27958cce6c5ed137310edc76fd113d164438273c75ad4f22a4b81003e737da67.json create mode 100644 packages/app-lib/.sqlx/query-39459b06ceb9ca531c476c6a77e40087e771dfafc968144db5978c570e68a884.json create mode 100644 packages/app-lib/.sqlx/query-465bc85ac3abc47c9179cf2f09e55a1a303360c04d865980ce37248b05ea3994.json create mode 100644 packages/app-lib/.sqlx/query-51adcaf25852937edaf46a304e85dcf59f91565296e9ae97d210249cdcc29cfc.json create mode 100644 packages/app-lib/.sqlx/query-58dbcc46287aa386bb87bebc0a94bbbe293834ffd7b86b260a7bccb6c8028d92.json create mode 100644 packages/app-lib/.sqlx/query-6b6645cbfb273b6c37f4c472d5df2dcb14f952cc7bcbb4661ff8abd4e168ee1c.json create mode 100644 packages/app-lib/.sqlx/query-7062aaed9cde80d865cf4c845cb40fb48d24b62a5ddddfc6de2f14b80d2bfc7c.json create mode 100644 packages/app-lib/.sqlx/query-71952a8ac14cf790a5b93eb722efa810ad8ca5f246c4d81ad4bcd5866ee10f2c.json create mode 100644 packages/app-lib/.sqlx/query-738203b62c0c38211bb535a3595f046e42ee66e31d99b8ff28917f58b4a937f6.json create mode 100644 packages/app-lib/.sqlx/query-76896bb2061e4c839a53bed4a221be42650f58edf2045fe1b85d418a537382e2.json create mode 100644 packages/app-lib/.sqlx/query-7874719d05692b28b1c2cdbd6f208acd794a400abeca0027cc6b21f5bd466e79.json create mode 100644 packages/app-lib/.sqlx/query-7a91c72a32d0d8cf712ddcbe9746d3d8c7b10211f275e0db81fa1eda5196c2e5.json create mode 100644 packages/app-lib/.sqlx/query-838b438f2dfa21885848b2eaf013a4767d4e9a28626c754bb4e8e7030b73ead7.json create mode 100644 packages/app-lib/.sqlx/query-8e5cb4f707e7adae2d7fce146402335be017bc4398a15149753bf31b2acac938.json create mode 100644 packages/app-lib/.sqlx/query-91c09c88e89aadbf34fd823f119ca83c2626f097de53ec537f59b76fdb21eccb.json create mode 100644 packages/app-lib/.sqlx/query-aaaf087deec439148bc1aa7365913a985f60f680c77885917f560760b5818b71.json create mode 100644 packages/app-lib/.sqlx/query-aabd92c612d1c2b66c50385cb7a513dca33a6931b29489f59afddfc325d6258a.json create mode 100644 packages/app-lib/.sqlx/query-b6583d5f711af94f3f21c0fbd6d1f2783cbdd25a4c65031de3c0b2ed78acc0a2.json create mode 100644 packages/app-lib/.sqlx/query-c4c56a28a27c15ef48df89f5145b977c166ea70f3ec8f5a2aa8f067fa2f44733.json create mode 100644 packages/app-lib/.sqlx/query-c640d4a60e6eb04fb484e3c99bfed581944cc9d9c335d1bc1c070863f93b276f.json create mode 100644 packages/app-lib/.sqlx/query-c6eb6fd3bd76b37eacf442bdc1a0ce434011c2e6bf3b76e0f8dd7d162ebb95ae.json create mode 100644 packages/app-lib/.sqlx/query-d125cb88ef6a465b49ae207f5abf5fe9c28436566c4d7b74bbcb9a82cb553959.json create mode 100644 packages/app-lib/.sqlx/query-d642bafe7282bde2908f9add0ad8ee2852552b5b06a38deb6311a3bf2098cee8.json create mode 100644 packages/app-lib/.sqlx/query-d808d8eded7c18861d5e865bda7d55605696d030a31be35669d8d0ee129c3f49.json create mode 100644 packages/app-lib/.sqlx/query-ec00a599037bcf5fb080eb0f7cab1beaaca1254a4abcf2955c7af3bc5086f9cc.json create mode 100644 packages/app-lib/.sqlx/query-fc51a02b2fa872f4b105b0eb25f9103b04b9e94bbd663ec205002e3ab2ff6756.json create mode 100644 packages/app-lib/migrations/20260827120000_game-options-sync.sql create mode 100644 packages/app-lib/src/api/instance/game_options/api_types.rs create mode 100644 packages/app-lib/src/api/instance/game_options/instance_support.rs create mode 100644 packages/app-lib/src/api/instance/game_options/launch_overrides.rs create mode 100644 packages/app-lib/src/api/instance/game_options/mod.rs create mode 100644 packages/app-lib/src/api/instance/game_options/options_file.rs create mode 100644 packages/app-lib/src/api/instance/game_options/pack_updates.rs create mode 100644 packages/app-lib/src/api/instance/game_options/read_instance_changes.rs create mode 100644 packages/app-lib/src/api/instance/game_options/settings_editor.rs create mode 100644 packages/app-lib/src/api/instance/game_options/source_selection.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/accessibility.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/chat.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/controls.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/known_vanilla_keys.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/legacy_key_bindings.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/mod.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/sound.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/version_changes.rs create mode 100644 packages/app-lib/src/api/instance/game_options/supported_settings/video.rs create mode 100644 packages/app-lib/src/api/instance/game_options/write_shared_settings.rs create mode 100644 packages/app-lib/src/state/instances/commands/game_options.rs create mode 100644 packages/app-lib/src/state/instances/model/game_options.rs diff --git a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue index 548abba7af..eba48797c7 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue @@ -26,7 +26,16 @@ import type { Component } from 'vue' import { computed, ref, shallowRef, watch } from 'vue' import WorldItem from '@/components/ui/world/WorldItem.vue' +import GameSettingsModal from '@/components/ui/settings/instances/game-settings/GameSettingsModal.vue' +import { + formatSourceDisabledReason, + formatSourceSettingsSummary, +} from '@/components/ui/settings/instances/game-settings/game-setting-messages' import useMemorySlider from '@/composables/useMemorySlider' +import { + type GameOptionsSourceCandidate, + list_game_options_sync_sources, +} from '@/helpers/game-options' import { get_command_history, get_global_synced_options, @@ -65,6 +74,23 @@ const messages = defineMessages({ // id: 'app.settings.synced-options.folder', // defaultMessage: 'Synced folder', // }, + gameSettings: { + id: 'app.settings.synced-options.game-settings', + defaultMessage: 'Game settings', + }, + gameSettingsDescription: { + id: 'app.settings.synced-options.game-settings.description', + defaultMessage: + 'You can choose which options to sync across instances so they apply on top of a modpack’s options.txt.', + }, + gameSettingsButton: { + id: 'app.settings.synced-options.game-settings.button', + defaultMessage: 'Settings', + }, + gameSettingsDisabledTooltip: { + id: 'app.settings.synced-options.game-settings.disabled-tooltip', + defaultMessage: 'Enable game settings sync before choosing individual settings.', + }, multiplayerServers: { id: 'app.settings.synced-options.multiplayer-servers', defaultMessage: 'Sync multiplayer servers', @@ -105,6 +131,11 @@ const messages = defineMessages({ id: 'app.settings.synced-options.choose-sync-source.creative-hotbars-description', defaultMessage: 'Pick the instance whose saved creative hotbars become the shared copy.', }, + gameSettingsSyncSourceDescription: { + id: 'app.settings.synced-options.choose-sync-source.game-settings-description', + defaultMessage: + 'Pick the instance whose options.txt values become the shared game settings baseline.', + }, searchInstance: { id: 'app.settings.synced-options.choose-sync-source.search-placeholder', defaultMessage: 'Search instance', @@ -292,8 +323,14 @@ const globalRows: Array<{ option: SyncedOption title: keyof typeof messages description?: keyof typeof messages - editable?: 'servers' | 'commands' + editable?: 'game-settings' | 'servers' | 'commands' }> = [ + { + option: 'game_options', + title: 'gameSettings', + description: 'gameSettingsDescription', + editable: 'game-settings', + }, { option: 'multiplayer_servers', title: 'multiplayerServers', @@ -316,6 +353,7 @@ const globalRows: Array<{ const globalSyncedOptionsQueryKey = ['global-synced-options'] as const const globalSyncedOptionsMutationKey = ['global-synced-options', 'set'] as const const defaultGlobalOptions: GlobalSyncedOptions = { + game_options: false, command_history: false, multiplayer_servers: false, creative_hotbars: false, @@ -332,6 +370,7 @@ const baseOption = ref(null) const baseInstanceId = ref(instances.value[0]?.id ?? '') const baseInstanceSearch = ref('') const baseModal = ref | null>(null) +const gameSettingsModal = ref | null>(null) const commandHistoryModal = ref | null>(null) const serverEditorModal = ref | null>(null) const editServerModal = ref | null>(null) @@ -345,6 +384,20 @@ const syncedServers = ref( const editedServer = ref(null) const serverData = ref>({}) const editorComponent = shallowRef(null) +const gameOptionSources = ref([]) +const baseSourcesLoading = ref(false) +let baseSourceGeneration = 0 + +type BaseSourceCandidate = { + id: string + name: string + icon_path?: string | null + game_version?: string | null + eligible: boolean + disabled_reason?: GameOptionsSourceCandidate['disabled_reason'] + recognized_setting_count?: number + custom_setting_count?: number +} const syncedServerCards = computed(() => syncedServers.value.map((server, index) => ({ @@ -368,6 +421,8 @@ const syncedServerCards = computed(() => const baseInstanceDescription = computed(() => { switch (baseOption.value) { + case 'game_options': + return formatMessage(messages.gameSettingsSyncSourceDescription) case 'multiplayer_servers': return formatMessage(messages.multiplayerServersSyncSourceDescription) case 'command_history': @@ -381,10 +436,44 @@ const baseInstanceDescription = computed(() => { const filteredBaseInstances = computed(() => { const search = baseInstanceSearch.value.trim().toLowerCase() - if (!search) return instances.value - return instances.value.filter((instance) => instance.name.toLowerCase().includes(search)) + const candidates: BaseSourceCandidate[] = + baseOption.value === 'game_options' + ? gameOptionSources.value.map((source) => ({ + id: source.source_id, + name: source.name, + icon_path: source.icon_path, + game_version: source.game_version, + eligible: source.eligible, + disabled_reason: source.disabled_reason, + recognized_setting_count: source.recognized_setting_count, + custom_setting_count: source.custom_setting_count, + })) + : instances.value.map((instance) => ({ + id: instance.id, + name: instance.name, + icon_path: instance.icon_path, + game_version: instance.game_version, + eligible: true, + })) + if (!search) return candidates + return candidates.filter((instance) => instance.name.toLowerCase().includes(search)) }) +function baseSourceDescription(source: BaseSourceCandidate): string | null { + if (source.disabled_reason) { + return formatSourceDisabledReason(formatMessage, source.disabled_reason) + } + if ( + source.recognized_setting_count === undefined || + source.custom_setting_count === undefined + ) + return null + return formatSourceSettingsSummary(formatMessage, { + recognized_setting_count: source.recognized_setting_count, + custom_setting_count: source.custom_setting_count, + }) +} + async function invalidateSyncedOptions() { await Promise.all([ queryClient.invalidateQueries({ queryKey: instanceKeys.all }), @@ -407,10 +496,12 @@ const globalOptionMutation = useMutation({ await queryClient.cancelQueries({ queryKey: globalSyncedOptionsQueryKey }) const previous = globalOptions.value[option] - queryClient.setQueryData(globalSyncedOptionsQueryKey, (current) => ({ - ...(current ?? defaultGlobalOptions), - [option]: enabled, - })) + if (option !== 'game_options' || !enabled) { + queryClient.setQueryData(globalSyncedOptionsQueryKey, (current) => ({ + ...(current ?? defaultGlobalOptions), + [option]: enabled, + })) + } return { previous } }, @@ -421,7 +512,8 @@ const globalOptionMutation = useMutation({ })) handleError(error) }, - onSuccess: async (_options, { option, enabled }) => { + onSuccess: async (options, { option, enabled }) => { + queryClient.setQueryData(globalSyncedOptionsQueryKey, options) if (enabled) baseModal.value?.hide() if (enabled && option === 'multiplayer_servers') { syncedServers.value = await list_synced_servers().catch((error) => { @@ -441,21 +533,59 @@ function applyGlobalOption(option: SyncedOption, enabled: boolean, baseInstanceI globalOptionMutation.mutate({ option, enabled, baseInstanceId }) } -function toggleGlobalOption(option: SyncedOption, enabled: boolean) { - if (!enabled) { - applyGlobalOption(option, false) +async function chooseBaseInstance(option: SyncedOption) { + const generation = ++baseSourceGeneration + baseOption.value = option + baseInstanceSearch.value = '' + baseSourcesLoading.value = option === 'game_options' + baseModal.value?.show() + + if (option === 'game_options') { + try { + const sources = await list_game_options_sync_sources() + if (generation !== baseSourceGeneration || baseOption.value !== option) return + gameOptionSources.value = sources + baseInstanceId.value = + gameOptionSources.value.find((source) => source.eligible)?.source_id ?? '' + } catch (error) { + if (generation !== baseSourceGeneration || baseOption.value !== option) return + gameOptionSources.value = [] + baseInstanceId.value = '' + handleError(error) + } finally { + if (generation === baseSourceGeneration && baseOption.value === option) { + baseSourcesLoading.value = false + } + } return } - baseOption.value = option baseInstanceId.value = instances.value[0]?.id ?? '' - baseInstanceSearch.value = '' - baseModal.value?.show() } -function confirmBaseInstance() { +function toggleGlobalOption(option: SyncedOption, enabled: boolean) { + if (enabled && option !== 'screenshots') { + void chooseBaseInstance(option) + return + } + applyGlobalOption(option, enabled) +} + +async function confirmBaseInstance() { if (!baseOption.value || !baseInstanceId.value) return - applyGlobalOption(baseOption.value, true, baseInstanceId.value) + try { + await globalOptionMutation.mutateAsync({ + option: baseOption.value, + enabled: true, + baseInstanceId: baseInstanceId.value, + }) + } catch { + return + } +} + +function openGameSettings() { + gameSettingsModal.value?.show() } async function openCommandHistoryEditor() { @@ -569,6 +699,8 @@ watch(