diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 52f7a10e15..985e7e2e5a 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -2216,7 +2216,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) > -
:deep(*) { + flex-shrink: 0; + } } .app-grid-statusbar { diff --git a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue index 7387eaca6a..b0cdbe9b72 100644 --- a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue +++ b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue @@ -27,7 +27,6 @@ import { showInstanceInFolder } from '@/helpers/utils' import { instanceListQueryOptions } from '@/pages/instance/query-options' const ITEM_SIZE = 52 -const APPROX_USED_VERTICAL_SPACE = 475 // doesn't need to be exact lol just close enough so there's a little gap and no overflow const { handleError } = injectNotificationManager() const instancesQuery = useQuery(instanceListQueryOptions()) const router = useRouter() @@ -36,6 +35,8 @@ const runningInstances = ref([]) const { formatMessage } = useVIntl() +const container = ref() +let resizeObserver const maxAuto = ref(0) const allInstances = computed(() => (instancesQuery.data.value ?? []).slice().sort((a, b) => { @@ -69,9 +70,13 @@ const canDrag = computed(() => maxVisible.value > 0) const showOverdrag = ref(false) const updateMaxAuto = () => { + if (!container.value) return + const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) + const dividerHeight = rem + 1 + const gap = rem / 4 maxAuto.value = Math.max( 0, - Math.floor((window.innerHeight - APPROX_USED_VERTICAL_SPACE) / ITEM_SIZE), + Math.floor((container.value.clientHeight - 2 * dividerHeight - gap) / (3 * rem + gap)), ) } @@ -154,17 +159,18 @@ const onDividerPointerUp = (event) => { } await instancesQuery.suspense().catch(handleError) -updateMaxAuto() useAppEvent('process', checkProcesses) onMounted(() => { - window.addEventListener('resize', updateMaxAuto) + resizeObserver = new ResizeObserver(updateMaxAuto) + resizeObserver.observe(container.value) + updateMaxAuto() checkProcesses() }) onUnmounted(() => { - window.removeEventListener('resize', updateMaxAuto) + resizeObserver?.disconnect() document.body.classList.remove('quick-instance-dragging') clearOverdragFlash() }) @@ -264,55 +270,61 @@ function openContextMenu(event, instance) { diff --git a/apps/app-frontend/src/components/ui/library/instance-group/index.vue b/apps/app-frontend/src/components/ui/library/instance-group/index.vue index 8662927fd1..644246aee4 100644 --- a/apps/app-frontend/src/components/ui/library/instance-group/index.vue +++ b/apps/app-frontend/src/components/ui/library/instance-group/index.vue @@ -128,7 +128,9 @@ const cardWidth = computed( () => (gridWidth.value - gap.value * (columnCount.value - 1)) / columnCount.value, ) const cardHeight = computed(() => - compactMode.value ? remSize.value * 3.875 : Math.max(0, cardWidth.value) + remSize.value * 3.375, + compactMode.value + ? remSize.value * 3.875 + 2 + : Math.max(0, cardWidth.value) + remSize.value * 3.375, ) const gridHeight = computed(() => Math.max( diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts index 0ccd80d64d..16908ab858 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts +++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts @@ -64,6 +64,11 @@ export function gameSettingChanges( }) } +export function gameSettingNumberScale(setting: EditableGameSetting): number { + if (setting.editor.unit !== 'percent') return 1 + return setting.option_id === 'sensitivity' ? 200 : 100 +} + export function canonicalValueText(setting: EditableGameSetting): string { const value = setting.canonical_value if (!value) return '' @@ -74,7 +79,7 @@ export function canonicalValueText(setting: EditableGameSetting): string { case 'integer': case 'decimal': return setting.editor.unit === 'percent' - ? String(Number((Number(value.value) * 100).toFixed(8))) + ? String(Number((Number(value.value) * gameSettingNumberScale(setting)).toFixed(8))) : String(value.value) case 'string_list': return value.value.join(', ') @@ -90,7 +95,7 @@ export function canonicalBooleanValue(setting: EditableGameSetting): boolean | u export function isKeybindSetting(setting: EditableGameSetting): boolean { return ( setting.editor.type === 'key_binding' || - (setting.editor.type === 'external_raw' && !!setting.raw_key?.startsWith('key_key')) + (setting.editor.type === 'external_raw' && !!setting.raw_key?.startsWith('key_')) ) } @@ -114,7 +119,7 @@ export function canonicalValueFromInput( type: 'decimal', value: setting.editor.unit === 'percent' - ? String(Number((parsed / 100).toFixed(8))) + ? String(Number((parsed / gameSettingNumberScale(setting)).toFixed(8))) : String(value), } } diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue index f1a826bb1d..6550842a1a 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue @@ -40,13 +40,10 @@ import { settingSearchText, } from './editors' import { minecraftKeybindConflictKey } from './keybinds' -import { - formatGameSettingDescription, - formatGameSettingLabel, - gameSettingCategoryMessage, -} from './messages' +import { formatGameSettingDescription, gameSettingCategoryMessage } from './messages' import GameSettingRow from './row.vue' import { useGameSettingsEditor } from './use-editor' +import { useGameSettingLabels } from './use-labels' const props = defineProps<{ instanceId?: string @@ -119,6 +116,7 @@ const modal = ref | null>(null) const confirmLeaveModal = ref | null>(null) const activeCategoryId = ref('') const search = ref('') +const opened = ref(false) let allowClose = false const { @@ -154,6 +152,16 @@ const categoryIcons: Record = { custom_settings: WrenchIcon, } +const localeLabels = useGameSettingLabels( + opened, + () => props.instanceId, + () => draftState.value?.settings ?? [], +) + +function settingLabel(setting: EditableGameSetting) { + return localeLabels.value[setting.option_id]?.label ?? setting.raw_key ?? setting.option_id +} + const categories = computed(() => { if (!draftState.value) return [] @@ -192,7 +200,7 @@ const categorySettings = computed(() => { query && !settingSearchText( setting, - formatGameSettingLabel(formatMessage, setting), + settingLabel(setting), formatGameSettingDescription(formatMessage, setting), ).includes(query) ) @@ -220,7 +228,7 @@ const keybindConflicts = computed(() => { setting.option_id, settings .filter((candidate) => candidate.option_id !== setting.option_id) - .map((candidate) => formatGameSettingLabel(formatMessage, candidate)), + .map((candidate) => settingLabel(candidate)), ) } } @@ -267,6 +275,7 @@ async function load() { } function show() { + opened.value = true allowClose = false search.value = '' modal.value?.show() @@ -278,6 +287,7 @@ function hide() { } function reset() { + opened.value = false resetEditor() allowClose = false } @@ -394,6 +404,7 @@ defineExpose({ show, hide }) v-for="setting in categorySettings" :key="setting.option_id" :setting="setting" + :locale-label="localeLabels[setting.option_id]" :keybind-conflicts="keybindConflicts.get(setting.option_id)" :show-sync-toggle="!isLocalEditor" @update:sync-enabled="setSyncEnabled([setting.option_id], $event)" diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/languages.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/languages.ts new file mode 100644 index 0000000000..752e3aa928 --- /dev/null +++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/languages.ts @@ -0,0 +1,152 @@ +import type { ComboboxOption } from '@modrinth/ui' + +/** Java Edition language names and regions from https://github.com/misode/mcmeta/blob/assets/pack.mcmeta. */ +const languageNames: Record = { + en_us: 'English (US)', + af_za: 'Afrikaans (Suid-Afrika)', + esan: 'Andalûh (Andaluçía)', + enp: 'Anglish (Oned Riches)', + ast_es: 'Asturianu (Asturies)', + az_az: 'Azərbaycanca (Azərbaycan)', + id_id: 'Bahasa Indonesia (Indonesia)', + ms_my: 'Bahasa Melayu (Malaysia)', + tzo_mx: "Bats'i k'op (Jobel)", + qid: 'Bhs. Indonesia edjaän lama (Indonesia tempo doeloe)', + be_latn: 'Biełaruskaja (Biełaruś)', + bar: 'Boarisch (Bayern)', + bs_ba: 'Bosanski (Bosna i Hercegovina)', + brb: 'Braobans (Braobant)', + br_fr: 'Brezhoneg (Breizh)', + ca_es: 'Català (Catalunya)', + val_es: 'Català (Valencià) (País Valencià)', + cy_gb: 'Cymraeg (Cymru)', + qcb_es: 'Cántabru/Montañés (Cantabria)', + da_dk: 'Dansk (Danmark)', + se_no: 'Davvisámegiella (Sápmi)', + de_at: 'Deitsch (Österreich)', + de_de: 'Deutsch (Deutschland)', + et_ee: 'Eesti keel (Eesti)', + en_au: 'English (Australia)', + en_ca: 'English (Canada)', + en_nz: 'English (New Zealand)', + en_gb: 'English (United Kingdom)', + es_ar: 'Español (Argentina)', + es_cl: 'Español (Chile)', + es_ec: 'Español (Ecuador)', + es_es: 'Español (España)', + es_mx: 'Español (México)', + es_uy: 'Español (Uruguay)', + es_ve: 'Español (Venezuela)', + eo_uy: 'Esperanto (Esperantujo)', + eu_es: 'Euskara (Euskal Herria)', + fil_ph: 'Filipino (Pilipinas)', + fr_ca: 'Français (Canada)', + fr_fr: 'Français (France)', + fr_ch: 'Français (Suisse)', + fy_nl: 'Frysk (Fryslân)', + fra_de: 'Fränggisch (Franggn)', + fur_it: 'Furlan (Friûl)', + fo_fo: 'Føroyskt (Føroyar)', + ga_ie: 'Gaeilge (Éire)', + gl_es: 'Galego (Galicia / Galiza)', + go_fr: 'Galo (Bertègn)', + gd_gb: 'Gàidhlig (Alba)', + hr_hr: 'Hrvatski (Hrvatska)', + hn_no: 'Høgnorsk (Norig)', + io_en: 'Ido (Idia)', + ig_ng: 'Igbo (Naigeria)', + it_it: 'Italiano (Italia)', + kw_gb: 'Kernewek (Kernow)', + ksh: 'Kölsch/Ripoarisch (Rhingland)', + lol_us: 'LOLCAT (Kingdom of Cats)', + la_la: 'Latina (Latium)', + lv_lv: 'Latviešu (Latvija)', + lt_lt: 'Lietuvių (Lietuva)', + li_li: 'Limburgs (Limburg)', + lmo: 'Lombard (Lombardia)', + lb_lu: 'Lëtzebuergesch (Lëtzebuerg)', + hu_hu: 'Magyar (Magyarország)', + mt_mt: 'Malti (Malta)', + isv: 'Medžuslovjansky (Slovjanščina)', + nah: 'Mēxikatlahtōlli (Mēxiko)', + nl_nl: 'Nederlands (Nederland)', + pls: 'Ngiiwà (Ndanìꞌngà)', + no_no: 'Norsk bokmål (Norge)', + nn_no: 'Norsk nynorsk (Noreg)', + uz_uz: "O'zbekcha (O'zbekiston)", + oc_fr: 'Occitan (Occitània)', + en_pt: 'Pirate Speak (The Seven Seas)', + nds_de: 'Plattdüütsch (Düütschland)', + pl_pl: 'Polski (Polska)', + pt_br: 'Português (Brasil)', + pt_pt: 'Português (Portugal)', + qya_aa: 'Quenya (Arda)', + ro_ro: 'Română (România)', + de_ch: 'Schwiizerdütsch (Schwiiz)', + enws: 'Shakespearean English (Kingdom of England)', + sq_al: 'Shqip (Shqipëri)', + sk_sk: 'Slovenčina (Slovensko)', + sl_si: 'Slovenščina (Slovenija)', + so_so: 'Soomaali (Soomaaliya)', + sr_cs: 'Srpski (Srbija)', + fi_fi: 'Suomi (Suomi)', + sv_se: 'Svenska (Sverige)', + sxu: 'Säggs’sch (Saggsn)', + tl_ph: 'Tagalog (Pilipinas)', + vi_vn: 'Tiếng Việt (Việt Nam)', + tr_tr: 'Türkçe (Türkiye)', + vp_vl: 'Viossa (Vilant)', + nl_be: 'Vlaams (België)', + vec_it: 'Vèneto (Veneto)', + vro: 'Võro (Eesti)', + yo_ng: 'Yorùbá (Nàìjíríà)', + jbo_en: "la .lojban. (la jbogu'e)", + tlh_aa: "tlhIngan Hol (tlhIngan wo')", + tok: 'toki pona (kulupu pona)', + is_is: 'Íslenska (Ísland)', + ovd: 'Övdalska (Swerre)', + cs_cz: 'Čeština (Česko)', + szl: 'Ślōnski (Gōrny Ślōnsk)', + en_ud: 'ɥsᴉꞁᵷuƎ (uʍoᗡ ǝpᴉsd∩)', + haw_us: 'ʻŌlelo Hawaiʻi (Hawaiʻi)', + el_gr: 'Ελληνικά (Ελλάδα)', + ba_ru: 'Башҡортса (Башҡортостан, Рәсәй)', + be_by: 'Беларуская (Беларусь)', + bg_bg: 'Български (България)', + hal_ua: 'Галицка (Галичина, Вкраїна)', + ky_kg: 'Кыргызча (Кыргызстан)', + mk_mk: 'Македонски (Северна Македонија)', + mn_mn: 'Монгол (Монгол Улс)', + ry_ua: 'Руснацькый (Пудкарпатя, Украина)', + ru_ru: 'Русский (Россия)', + rpr: 'Русскій дореформенный (Россійская имперія)', + sah_sah: 'Сахалыы (Cаха Сирэ)', + sr_sp: 'Српски (Србија)', + tt_ru: 'Татарча (Татарстан, Рәсәй)', + uk_ua: 'Українська (Україна)', + cv_cu: 'Чӑвашла (Чӑваш Ен, Раҫҫей)', + kk_kz: 'Қазақша (Қазақстан)', + hy_am: 'Հայերեն (Հայաստան)', + yi_de: 'ייִדיש (אשכנזיש יידן)', + he_il: 'עברית (ישראל)', + ar_sa: 'العربية (العالم العربي)', + zlm_arab: 'بهاس ملايو (مليسيا)', + fa_ir: 'فارسی (ايران)', + hi_in: 'हिंदी (भारत)', + ta_in: 'தமிழ் (இந்தியா)', + kn_in: 'ಕನ್ನಡ (ಭಾರತ)', + th_th: 'ไทย (ประเทศไทย)', + lo_la: 'ລາວ (ປະເທດລາວ)', + ka_ge: 'ქართული (საქართველო)', + lzh: '文言 (華夏)', + ja_jp: '日本語 (日本)', + zh_cn: '简体中文 (中国大陆)', + zh_tw: '繁體中文 (台灣)', + zh_hk: '繁體中文 (香港特別行政區)', + ko_kr: '한국어 (대한민국)', + got_de: '𐌲𐌿𐍄𐍂𐌰𐌶𐌳𐌰 (𐌲𐌿𐍄𐌸𐌹𐌿𐌳𐌰)', +} + +export const minecraftLanguageOptions: ComboboxOption[] = Object.entries(languageNames) + .map(([value, label]) => ({ value, label, searchTerms: [value] })) + .sort((a, b) => a.label.localeCompare(b.label)) diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts index a5e741b63a..0cd17a2663 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts +++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts @@ -9,855 +9,110 @@ import type { type FormatMessage = VIntlFormatters['formatMessage'] const settingMessages = defineMessages({ - fovLabel: { id: 'app.settings.game-options.setting.fov.label', defaultMessage: 'Field of view' }, - graphicsLabel: { - id: 'app.settings.game-options.setting.graphics.label', - defaultMessage: 'Graphics', - }, graphicsDescription: { id: 'app.settings.game-options.setting.graphics.description', defaultMessage: 'Controls visual quality and performance.', }, - ambientOcclusionLabel: { - id: 'app.settings.game-options.setting.ambient-occlusion.label', - defaultMessage: 'Smooth lighting', - }, - renderDistanceLabel: { - id: 'app.settings.game-options.setting.render-distance.label', - defaultMessage: 'Render distance', - }, - simulationDistanceLabel: { - id: 'app.settings.game-options.setting.simulation-distance.label', - defaultMessage: 'Simulation distance', - }, simulationDistanceDescription: { id: 'app.settings.game-options.setting.simulation-distance.description', defaultMessage: 'How far away entities update and blocks and fluids tick.', }, - guiScaleLabel: { - id: 'app.settings.game-options.setting.gui-scale.label', - defaultMessage: 'GUI scale', - }, guiScaleDescription: { id: 'app.settings.game-options.setting.gui-scale.description', defaultMessage: 'The size of the game interface and HUD.', }, - particlesLabel: { - id: 'app.settings.game-options.setting.particles.label', - defaultMessage: 'Particles', - }, - cloudsLabel: { id: 'app.settings.game-options.setting.clouds.label', defaultMessage: 'Clouds' }, - entityShadowsLabel: { - id: 'app.settings.game-options.setting.entity-shadows.label', - defaultMessage: 'Entity shadows', - }, - viewBobbingLabel: { - id: 'app.settings.game-options.setting.view-bobbing.label', - defaultMessage: 'View bobbing', - }, viewBobbingDescription: { id: 'app.settings.game-options.setting.view-bobbing.description', defaultMessage: 'Add a bobbing motion to the camera while walking.', }, - vsyncLabel: { id: 'app.settings.game-options.setting.vsync.label', defaultMessage: 'VSync' }, vsyncDescription: { id: 'app.settings.game-options.setting.vsync.description', defaultMessage: 'Limit the frame rate to the display refresh rate to prevent screen tearing.', }, - fullscreenLabel: { - id: 'app.settings.game-options.setting.fullscreen.label', - defaultMessage: 'Fullscreen', - }, - maxFramerateLabel: { - id: 'app.settings.game-options.setting.max-framerate.label', - defaultMessage: 'Maximum framerate', - }, - mipmapLevelsLabel: { - id: 'app.settings.game-options.setting.mipmap-levels.label', - defaultMessage: 'Mipmap levels', - }, mipmapLevelsDescription: { id: 'app.settings.game-options.setting.mipmap-levels.description', defaultMessage: 'Texture smoothing at a distance.', }, - biomeBlendRadiusLabel: { - id: 'app.settings.game-options.setting.biome-blend-radius.label', - defaultMessage: 'Biome blend', - }, biomeBlendRadiusDescription: { id: 'app.settings.game-options.setting.biome-blend-radius.description', defaultMessage: 'The distance over which biome colors transition.', }, - languageLabel: { - id: 'app.settings.game-options.setting.language.label', - defaultMessage: 'Language', - }, - masterVolumeLabel: { - id: 'app.settings.game-options.setting.master-volume.label', - defaultMessage: 'Master volume', - }, - musicVolumeLabel: { - id: 'app.settings.game-options.setting.music-volume.label', - defaultMessage: 'Music', - }, - musicToastLabel: { - id: 'app.settings.game-options.setting.music-toast.label', - defaultMessage: 'Music notification', - }, musicToastDescription: { id: 'app.settings.game-options.setting.music-toast.description', defaultMessage: 'Choose whether music titles appear in the pause menu and as toasts.', }, - recordVolumeLabel: { - id: 'app.settings.game-options.setting.record-volume.label', - defaultMessage: 'Jukebox/Note Blocks', - }, - weatherVolumeLabel: { - id: 'app.settings.game-options.setting.weather-volume.label', - defaultMessage: 'Weather', - }, - blocksVolumeLabel: { - id: 'app.settings.game-options.setting.blocks-volume.label', - defaultMessage: 'Blocks', - }, - hostileVolumeLabel: { - id: 'app.settings.game-options.setting.hostile-volume.label', - defaultMessage: 'Hostile creatures', - }, - neutralVolumeLabel: { - id: 'app.settings.game-options.setting.neutral-volume.label', - defaultMessage: 'Friendly creatures', - }, - playersVolumeLabel: { - id: 'app.settings.game-options.setting.players-volume.label', - defaultMessage: 'Players', - }, - ambientVolumeLabel: { - id: 'app.settings.game-options.setting.ambient-volume.label', - defaultMessage: 'Ambient/Environment', - }, - voiceVolumeLabel: { - id: 'app.settings.game-options.setting.voice-volume.label', - defaultMessage: 'Voice and speech', - }, - uiVolumeLabel: { - id: 'app.settings.game-options.setting.ui-volume.label', - defaultMessage: 'UI', - }, - sensitivityLabel: { - id: 'app.settings.game-options.setting.sensitivity.label', - defaultMessage: 'Mouse sensitivity', - }, - invertMouseLabel: { - id: 'app.settings.game-options.setting.invert-mouse.label', - defaultMessage: 'Invert mouse', - }, invertMouseDescription: { id: 'app.settings.game-options.setting.invert-mouse.description', defaultMessage: 'Invert vertical mouse movement.', }, - autoJumpLabel: { - id: 'app.settings.game-options.setting.auto-jump.label', - defaultMessage: 'Auto-jump', - }, autoJumpDescription: { id: 'app.settings.game-options.setting.auto-jump.description', defaultMessage: 'Automatically jump up one-block-high obstacles.', }, - toggleCrouchLabel: { - id: 'app.settings.game-options.setting.toggle-crouch.label', - defaultMessage: 'Toggle crouch', - }, toggleCrouchDescription: { id: 'app.settings.game-options.setting.toggle-crouch.description', defaultMessage: 'Press once to remain crouched.', }, - toggleSprintLabel: { - id: 'app.settings.game-options.setting.toggle-sprint.label', - defaultMessage: 'Toggle sprint', - }, toggleSprintDescription: { id: 'app.settings.game-options.setting.toggle-sprint.description', defaultMessage: 'Press once to remain sprinting.', }, - discreteMouseScrollLabel: { - id: 'app.settings.game-options.setting.discrete-mouse-scroll.label', - defaultMessage: 'Discrete scrolling', - }, discreteMouseScrollDescription: { id: 'app.settings.game-options.setting.discrete-mouse-scroll.description', defaultMessage: 'Treat each mouse-wheel input as a single scroll step.', }, - keyForwardLabel: { - id: 'app.settings.game-options.setting.key-forward.label', - defaultMessage: 'Move forward', - }, - keyLeftLabel: { - id: 'app.settings.game-options.setting.key-left.label', - defaultMessage: 'Strafe left', - }, - keyBackLabel: { - id: 'app.settings.game-options.setting.key-back.label', - defaultMessage: 'Move backward', - }, - keyRightLabel: { - id: 'app.settings.game-options.setting.key-right.label', - defaultMessage: 'Strafe right', - }, - keyJumpLabel: { id: 'app.settings.game-options.setting.key-jump.label', defaultMessage: 'Jump' }, - keySneakLabel: { - id: 'app.settings.game-options.setting.key-sneak.label', - defaultMessage: 'Sneak', - }, - keySprintLabel: { - id: 'app.settings.game-options.setting.key-sprint.label', - defaultMessage: 'Sprint', - }, - keyInventoryLabel: { - id: 'app.settings.game-options.setting.key-inventory.label', - defaultMessage: 'Inventory', - }, - keySwapOffhandLabel: { - id: 'app.settings.game-options.setting.key-swap-offhand.label', - defaultMessage: 'Swap offhand', - }, - keyDropLabel: { - id: 'app.settings.game-options.setting.key-drop.label', - defaultMessage: 'Drop item', - }, - keyUseLabel: { - id: 'app.settings.game-options.setting.key-use.label', - defaultMessage: 'Use item', - }, - keyAttackLabel: { - id: 'app.settings.game-options.setting.key-attack.label', - defaultMessage: 'Attack', - }, - keyPickItemLabel: { - id: 'app.settings.game-options.setting.key-pick-item.label', - defaultMessage: 'Pick block', - }, - keyChatLabel: { - id: 'app.settings.game-options.setting.key-chat.label', - defaultMessage: 'Open chat', - }, - keyPlayerListLabel: { - id: 'app.settings.game-options.setting.key-player-list.label', - defaultMessage: 'Player list', - }, - keyCommandLabel: { - id: 'app.settings.game-options.setting.key-command.label', - defaultMessage: 'Command', - }, - keyScreenshotLabel: { - id: 'app.settings.game-options.setting.key-screenshot.label', - defaultMessage: 'Screenshot', - }, - keyPerspectiveLabel: { - id: 'app.settings.game-options.setting.key-perspective.label', - defaultMessage: 'Change perspective', - }, - keyFullscreenLabel: { - id: 'app.settings.game-options.setting.key-fullscreen.label', - defaultMessage: 'Toggle fullscreen', - }, - keyAdvancementsLabel: { - id: 'app.settings.game-options.setting.key-advancements.label', - defaultMessage: 'Advancements', - }, - chatVisibilityLabel: { - id: 'app.settings.game-options.setting.chat-visibility.label', - defaultMessage: 'Chat visibility', - }, - chatColorsLabel: { - id: 'app.settings.game-options.setting.chat-colors.label', - defaultMessage: 'Chat colors', - }, - chatLinksLabel: { - id: 'app.settings.game-options.setting.chat-links.label', - defaultMessage: 'Web links', - }, chatLinksDescription: { id: 'app.settings.game-options.setting.chat-links.description', defaultMessage: 'Allow web links in chat to be opened.', }, - chatLinksPromptLabel: { - id: 'app.settings.game-options.setting.chat-links-prompt.label', - defaultMessage: 'Prompt on links', - }, chatLinksPromptDescription: { id: 'app.settings.game-options.setting.chat-links-prompt.description', defaultMessage: 'Ask before opening links from chat.', }, - chatOpacityLabel: { - id: 'app.settings.game-options.setting.chat-opacity.label', - defaultMessage: 'Chat opacity', - }, chatOpacityDescription: { id: 'app.settings.game-options.setting.chat-opacity.description', defaultMessage: 'The opacity of chat text.', }, - chatScaleLabel: { - id: 'app.settings.game-options.setting.chat-scale.label', - defaultMessage: 'Chat scale', - }, - narratorLabel: { - id: 'app.settings.game-options.setting.narrator.label', - defaultMessage: 'Narrator', - }, narratorDescription: { id: 'app.settings.game-options.setting.narrator.description', defaultMessage: 'Choose what the narrator reads.', }, - subtitlesLabel: { - id: 'app.settings.game-options.setting.subtitles.label', - defaultMessage: 'Subtitles', - }, subtitlesDescription: { id: 'app.settings.game-options.setting.subtitles.description', defaultMessage: 'Show captions for sounds played in the game.', }, - highContrastLabel: { - id: 'app.settings.game-options.setting.high-contrast.label', - defaultMessage: 'High contrast', - }, highContrastDescription: { id: 'app.settings.game-options.setting.high-contrast.description', defaultMessage: 'Enhance the contrast of interface elements.', }, - darkSplashLabel: { - id: 'app.settings.game-options.setting.dark-splash.label', - defaultMessage: 'Monochrome logo', - }, darkSplashDescription: { id: 'app.settings.game-options.setting.dark-splash.description', defaultMessage: 'Change the Mojang Studios loading screen from red to black.', }, - notificationTimeLabel: { - id: 'app.settings.game-options.setting.notification-time.label', - defaultMessage: 'Notification time', - }, notificationTimeDescription: { id: 'app.settings.game-options.setting.notification-time.description', defaultMessage: 'How long toast notifications remain visible.', }, - mainHandLabel: { - id: 'app.settings.game-options.setting.main-hand.label', - defaultMessage: 'Main hand', - }, mainHandDescription: { id: 'app.settings.game-options.setting.main-hand.description', defaultMessage: 'Choose whether the main hand is left or right.', }, - capeLabel: { id: 'app.settings.game-options.setting.cape.label', defaultMessage: 'Cape' }, capeDescription: { id: 'app.settings.game-options.setting.cape.description', defaultMessage: "Show the player's cape, including its elytra texture.", }, - hatLabel: { id: 'app.settings.game-options.setting.hat.label', defaultMessage: 'Hat' }, hatDescription: { id: 'app.settings.game-options.setting.hat.description', defaultMessage: 'Show the hat skin layer.', }, - jacketLabel: { id: 'app.settings.game-options.setting.jacket.label', defaultMessage: 'Jacket' }, jacketDescription: { id: 'app.settings.game-options.setting.jacket.description', defaultMessage: 'Show the jacket skin layer.', }, - allowServerListingLabel: { - id: 'app.settings.game-options.setting.allow-server-listing.label', - defaultMessage: 'Server listings', - }, allowServerListingDescription: { id: 'app.settings.game-options.setting.allow-server-listing.description', defaultMessage: "Allow the player's name to appear in server listings.", }, - realmsNotificationsLabel: { - id: 'app.settings.game-options.setting.realms-notifications.label', - defaultMessage: 'Realms notifications', - }, -}) - -const catalogSettingMessages = defineMessages({ - brightnessLabel: { - id: 'app.settings.game-options.setting.brightness.label', - defaultMessage: 'Brightness', - }, - legacyViewDistanceLabel: { - id: 'app.settings.game-options.setting.legacy-view-distance.label', - defaultMessage: 'View distance', - }, - entityDistanceLabel: { - id: 'app.settings.game-options.setting.entity-distance.label', - defaultMessage: 'Entity distance', - }, - debugGuiScaleLabel: { - id: 'app.settings.game-options.setting.debug-gui-scale.label', - defaultMessage: 'Debug GUI scale', - }, - graphicsBackendLabel: { - id: 'app.settings.game-options.setting.graphics-backend.label', - defaultMessage: 'Graphics backend', - }, - cloudRangeLabel: { - id: 'app.settings.game-options.setting.cloud-range.label', - defaultMessage: 'Cloud distance', - }, - exclusiveFullscreenLabel: { - id: 'app.settings.game-options.setting.exclusive-fullscreen.label', - defaultMessage: 'Exclusive fullscreen', - }, - macFullscreenMenuLabel: { - id: 'app.settings.game-options.setting.mac-fullscreen-menu.label', - defaultMessage: 'Show macOS menu in fullscreen', - }, - legacyFramerateLimitLabel: { - id: 'app.settings.game-options.setting.legacy-framerate-limit.label', - defaultMessage: 'Framerate limit', - }, - inactivityFramerateLimitLabel: { - id: 'app.settings.game-options.setting.inactivity-framerate-limit.label', - defaultMessage: 'Reduced framerate', - }, - prioritizeChunkUpdatesLabel: { - id: 'app.settings.game-options.setting.prioritize-chunk-updates.label', - defaultMessage: 'Prioritize chunk updates', - }, - attackIndicatorLabel: { - id: 'app.settings.game-options.setting.attack-indicator.label', - defaultMessage: 'Attack indicator', - }, - reducedDebugInfoLabel: { - id: 'app.settings.game-options.setting.reduced-debug-info.label', - defaultMessage: 'Reduced debug information', - }, - chunkFadeTimeLabel: { - id: 'app.settings.game-options.setting.chunk-fade-time.label', - defaultMessage: 'Chunk fade time', - }, - cutoutLeavesLabel: { - id: 'app.settings.game-options.setting.cutout-leaves.label', - defaultMessage: 'Cutout leaves', - }, - improvedTransparencyLabel: { - id: 'app.settings.game-options.setting.improved-transparency.label', - defaultMessage: 'Improved transparency', - }, - textureFilteringLabel: { - id: 'app.settings.game-options.setting.texture-filtering.label', - defaultMessage: 'Texture filtering', - }, - anisotropyLabel: { - id: 'app.settings.game-options.setting.anisotropy.label', - defaultMessage: 'Anisotropy', - }, - vignetteLabel: { - id: 'app.settings.game-options.setting.vignette.label', - defaultMessage: 'Vignette', - }, - weatherRadiusLabel: { - id: 'app.settings.game-options.setting.weather-radius.label', - defaultMessage: 'Weather radius', - }, - advancedOpenGlLabel: { - id: 'app.settings.game-options.setting.advanced-opengl.label', - defaultMessage: 'Advanced OpenGL', - }, - anaglyph3dLabel: { - id: 'app.settings.game-options.setting.anaglyph-3d.label', - defaultMessage: '3D anaglyph', - }, - anisotropicFilteringLabel: { - id: 'app.settings.game-options.setting.anisotropic-filtering.label', - defaultMessage: 'Anisotropic filtering', - }, - alternateBlocksLabel: { - id: 'app.settings.game-options.setting.alternate-blocks.label', - defaultMessage: 'Alternate blocks', - }, - heldItemTooltipsLabel: { - id: 'app.settings.game-options.setting.held-item-tooltips.label', - defaultMessage: 'Held item tooltips', - }, - useVboLabel: { - id: 'app.settings.game-options.setting.use-vbo.label', - defaultMessage: 'Use VBOs', - }, - forceUnicodeFontLabel: { - id: 'app.settings.game-options.setting.force-unicode-font.label', - defaultMessage: 'Force Unicode font', - }, - japaneseGlyphVariantsLabel: { - id: 'app.settings.game-options.setting.japanese-glyph-variants.label', - defaultMessage: 'Japanese glyph variants', - }, - musicFrequencyLabel: { - id: 'app.settings.game-options.setting.music-frequency.label', - defaultMessage: 'Music frequency', - }, - directionalAudioLabel: { - id: 'app.settings.game-options.setting.directional-audio.label', - defaultMessage: 'Directional audio', - }, - invertHorizontalMouseLabel: { - id: 'app.settings.game-options.setting.invert-horizontal-mouse.label', - defaultMessage: 'Invert horizontal mouse', - }, - toggleAttackLabel: { - id: 'app.settings.game-options.setting.toggle-attack.label', - defaultMessage: 'Toggle attack', - }, - toggleUseLabel: { - id: 'app.settings.game-options.setting.toggle-use.label', - defaultMessage: 'Toggle use', - }, - mouseWheelSensitivityLabel: { - id: 'app.settings.game-options.setting.mouse-wheel-sensitivity.label', - defaultMessage: 'Mouse wheel sensitivity', - }, - rawMouseInputLabel: { - id: 'app.settings.game-options.setting.raw-mouse-input.label', - defaultMessage: 'Raw mouse input', - }, - touchscreenLabel: { - id: 'app.settings.game-options.setting.touchscreen.label', - defaultMessage: 'Touchscreen mode', - }, - allowCursorChangesLabel: { - id: 'app.settings.game-options.setting.allow-cursor-changes.label', - defaultMessage: 'Allow cursor changes', - }, - sprintWindowLabel: { - id: 'app.settings.game-options.setting.sprint-window.label', - defaultMessage: 'Sprint window', - }, - operatorItemsTabLabel: { - id: 'app.settings.game-options.setting.operator-items-tab.label', - defaultMessage: 'Operator items tab', - }, - ctrlClickRightClickLabel: { - id: 'app.settings.game-options.setting.ctrl-click-right-click.label', - defaultMessage: 'Control-click as right-click', - }, - quitShortcutsLabel: { - id: 'app.settings.game-options.setting.quit-shortcuts.label', - defaultMessage: 'Quit shortcuts', - }, - chatWidthLabel: { - id: 'app.settings.game-options.setting.chat-width.label', - defaultMessage: 'Chat width', - }, - focusedChatHeightLabel: { - id: 'app.settings.game-options.setting.focused-chat-height.label', - defaultMessage: 'Focused chat height', - }, - unfocusedChatHeightLabel: { - id: 'app.settings.game-options.setting.unfocused-chat-height.label', - defaultMessage: 'Unfocused chat height', - }, - chatLineSpacingLabel: { - id: 'app.settings.game-options.setting.chat-line-spacing.label', - defaultMessage: 'Chat line spacing', - }, - chatDelayLabel: { - id: 'app.settings.game-options.setting.chat-delay.label', - defaultMessage: 'Chat delay', - }, - textBackgroundOpacityLabel: { - id: 'app.settings.game-options.setting.text-background-opacity.label', - defaultMessage: 'Text background opacity', - }, - chatBackgroundOnlyLabel: { - id: 'app.settings.game-options.setting.chat-background-only.label', - defaultMessage: 'Chat background only', - }, - autoSuggestionsLabel: { - id: 'app.settings.game-options.setting.auto-suggestions.label', - defaultMessage: 'Command suggestions', - }, - secureChatOnlyLabel: { - id: 'app.settings.game-options.setting.secure-chat-only.label', - defaultMessage: 'Only show secure chat', - }, - saveChatDraftsLabel: { - id: 'app.settings.game-options.setting.save-chat-drafts.label', - defaultMessage: 'Save chat drafts', - }, - hideMatchedNamesLabel: { - id: 'app.settings.game-options.setting.hide-matched-names.label', - defaultMessage: 'Hide matched names', - }, - chatPreviewLabel: { - id: 'app.settings.game-options.setting.chat-preview.label', - defaultMessage: 'Chat preview', - }, - fovEffectsLabel: { - id: 'app.settings.game-options.setting.fov-effects.label', - defaultMessage: 'FOV effects', - }, - screenEffectsLabel: { - id: 'app.settings.game-options.setting.screen-effects.label', - defaultMessage: 'Screen effects', - }, - darknessPulsingLabel: { - id: 'app.settings.game-options.setting.darkness-pulsing.label', - defaultMessage: 'Darkness pulsing', - }, - damageTiltLabel: { - id: 'app.settings.game-options.setting.damage-tilt.label', - defaultMessage: 'Damage tilt', - }, - glintSpeedLabel: { - id: 'app.settings.game-options.setting.glint-speed.label', - defaultMessage: 'Glint speed', - }, - glintStrengthLabel: { - id: 'app.settings.game-options.setting.glint-strength.label', - defaultMessage: 'Glint strength', - }, - hideLightningFlashesLabel: { - id: 'app.settings.game-options.setting.hide-lightning-flashes.label', - defaultMessage: 'Hide lightning flashes', - }, - hideSplashTextsLabel: { - id: 'app.settings.game-options.setting.hide-splash-texts.label', - defaultMessage: 'Hide splash texts', - }, - highContrastOutlineLabel: { - id: 'app.settings.game-options.setting.high-contrast-outline.label', - defaultMessage: 'High contrast block outline', - }, - narratorHotkeyLabel: { - id: 'app.settings.game-options.setting.narrator-hotkey.label', - defaultMessage: 'Narrator hotkey', - }, - autosaveIndicatorLabel: { - id: 'app.settings.game-options.setting.autosave-indicator.label', - defaultMessage: 'Autosave indicator', - }, - panoramaSpeedLabel: { - id: 'app.settings.game-options.setting.panorama-speed.label', - defaultMessage: 'Panorama speed', - }, - menuBackgroundBlurLabel: { - id: 'app.settings.game-options.setting.menu-background-blur.label', - defaultMessage: 'Menu background blur', - }, - rotateWithMinecartLabel: { - id: 'app.settings.game-options.setting.rotate-with-minecart.label', - defaultMessage: 'Rotate with minecart', - }, - leftSleeveLabel: { - id: 'app.settings.game-options.setting.left-sleeve.label', - defaultMessage: 'Left sleeve', - }, - rightSleeveLabel: { - id: 'app.settings.game-options.setting.right-sleeve.label', - defaultMessage: 'Right sleeve', - }, - leftPantsLegLabel: { - id: 'app.settings.game-options.setting.left-pants-leg.label', - defaultMessage: 'Left pants leg', - }, - rightPantsLegLabel: { - id: 'app.settings.game-options.setting.right-pants-leg.label', - defaultMessage: 'Right pants leg', - }, - hideServerAddressLabel: { - id: 'app.settings.game-options.setting.hide-server-address.label', - defaultMessage: 'Hide server address', - }, - serverTexturesLabel: { - id: 'app.settings.game-options.setting.server-textures.label', - defaultMessage: 'Server textures', - }, - snooperLabel: { - id: 'app.settings.game-options.setting.snooper.label', - defaultMessage: 'Snooper', - }, - extraTelemetryLabel: { - id: 'app.settings.game-options.setting.extra-telemetry.label', - defaultMessage: 'Optional telemetry', - }, - inGameNotificationsLabel: { - id: 'app.settings.game-options.setting.in-game-notifications.label', - defaultMessage: 'In-game notifications', - }, - sharePresenceLabel: { - id: 'app.settings.game-options.setting.share-presence.label', - defaultMessage: 'Share presence', - }, -}) - -const catalogKeyMessages = defineMessages({ - smoothCameraLabel: { - id: 'app.settings.game-options.setting.key-smooth-camera.label', - defaultMessage: 'Toggle cinematic camera', - }, - spectatorOutlinesLabel: { - id: 'app.settings.game-options.setting.key-spectator-outlines.label', - defaultMessage: 'Highlight spectators', - }, - saveToolbarLabel: { - id: 'app.settings.game-options.setting.key-save-toolbar.label', - defaultMessage: 'Save toolbar', - }, - loadToolbarLabel: { - id: 'app.settings.game-options.setting.key-load-toolbar.label', - defaultMessage: 'Load toolbar', - }, - socialInteractionsLabel: { - id: 'app.settings.game-options.setting.key-social-interactions.label', - defaultMessage: 'Social interactions', - }, - quickActionsLabel: { - id: 'app.settings.game-options.setting.key-quick-actions.label', - defaultMessage: 'Quick actions', - }, - spectatorHotbarLabel: { - id: 'app.settings.game-options.setting.key-spectator-hotbar.label', - defaultMessage: 'Spectator hotbar', - }, - friendsLabel: { - id: 'app.settings.game-options.setting.key-friends.label', - defaultMessage: 'Friends', - }, - toggleGuiLabel: { - id: 'app.settings.game-options.setting.key-toggle-gui.label', - defaultMessage: 'Toggle HUD', - }, - toggleSpectatorShaderLabel: { - id: 'app.settings.game-options.setting.key-toggle-spectator-shader.label', - defaultMessage: 'Toggle spectator shader', - }, - hotbar1Label: { - id: 'app.settings.game-options.setting.key-hotbar-1.label', - defaultMessage: 'Hotbar 1', - }, - hotbar2Label: { - id: 'app.settings.game-options.setting.key-hotbar-2.label', - defaultMessage: 'Hotbar 2', - }, - hotbar3Label: { - id: 'app.settings.game-options.setting.key-hotbar-3.label', - defaultMessage: 'Hotbar 3', - }, - hotbar4Label: { - id: 'app.settings.game-options.setting.key-hotbar-4.label', - defaultMessage: 'Hotbar 4', - }, - hotbar5Label: { - id: 'app.settings.game-options.setting.key-hotbar-5.label', - defaultMessage: 'Hotbar 5', - }, - hotbar6Label: { - id: 'app.settings.game-options.setting.key-hotbar-6.label', - defaultMessage: 'Hotbar 6', - }, - hotbar7Label: { - id: 'app.settings.game-options.setting.key-hotbar-7.label', - defaultMessage: 'Hotbar 7', - }, - hotbar8Label: { - id: 'app.settings.game-options.setting.key-hotbar-8.label', - defaultMessage: 'Hotbar 8', - }, - hotbar9Label: { - id: 'app.settings.game-options.setting.key-hotbar-9.label', - defaultMessage: 'Hotbar 9', - }, - debugOverlayLabel: { - id: 'app.settings.game-options.setting.key-debug-overlay.label', - defaultMessage: 'Debug overlay', - }, - debugModifierLabel: { - id: 'app.settings.game-options.setting.key-debug-modifier.label', - defaultMessage: 'Debug modifier', - }, - debugReloadChunksLabel: { - id: 'app.settings.game-options.setting.key-debug-reload-chunks.label', - defaultMessage: 'Reload chunks', - }, - debugHitboxesLabel: { - id: 'app.settings.game-options.setting.key-debug-hitboxes.label', - defaultMessage: 'Show hitboxes', - }, - debugClearChatLabel: { - id: 'app.settings.game-options.setting.key-debug-clear-chat.label', - defaultMessage: 'Clear chat', - }, - debugCrashLabel: { - id: 'app.settings.game-options.setting.key-debug-crash.label', - defaultMessage: 'Trigger debug crash', - }, - debugChunkBordersLabel: { - id: 'app.settings.game-options.setting.key-debug-chunk-borders.label', - defaultMessage: 'Show chunk borders', - }, - debugAdvancedTooltipsLabel: { - id: 'app.settings.game-options.setting.key-debug-advanced-tooltips.label', - defaultMessage: 'Show advanced tooltips', - }, - debugCopyRecreateCommandLabel: { - id: 'app.settings.game-options.setting.key-debug-copy-recreate-command.label', - defaultMessage: 'Copy recreate command', - }, - debugSpectateLabel: { - id: 'app.settings.game-options.setting.key-debug-spectate.label', - defaultMessage: 'Spectate entity', - }, - debugSwitchGameModeLabel: { - id: 'app.settings.game-options.setting.key-debug-switch-game-mode.label', - defaultMessage: 'Switch game mode', - }, - debugOptionsLabel: { - id: 'app.settings.game-options.setting.key-debug-options.label', - defaultMessage: 'Debug options', - }, - debugFocusPauseLabel: { - id: 'app.settings.game-options.setting.key-debug-focus-pause.label', - defaultMessage: 'Pause on lost focus', - }, - debugDumpDynamicTexturesLabel: { - id: 'app.settings.game-options.setting.key-debug-dump-dynamic-textures.label', - defaultMessage: 'Dump dynamic textures', - }, - debugReloadResourcePacksLabel: { - id: 'app.settings.game-options.setting.key-debug-reload-resource-packs.label', - defaultMessage: 'Reload resource packs', - }, - debugProfilingLabel: { - id: 'app.settings.game-options.setting.key-debug-profiling.label', - defaultMessage: 'Start profiling', - }, - debugCopyLocationLabel: { - id: 'app.settings.game-options.setting.key-debug-copy-location.label', - defaultMessage: 'Copy location', - }, - debugDumpVersionLabel: { - id: 'app.settings.game-options.setting.key-debug-dump-version.label', - defaultMessage: 'Dump version', - }, - debugProfilingChartLabel: { - id: 'app.settings.game-options.setting.key-debug-profiling-chart.label', - defaultMessage: 'Profiling chart', - }, - debugFpsChartsLabel: { - id: 'app.settings.game-options.setting.key-debug-fps-charts.label', - defaultMessage: 'FPS charts', - }, - debugNetworkChartsLabel: { - id: 'app.settings.game-options.setting.key-debug-network-charts.label', - defaultMessage: 'Network charts', - }, - debugLightmapTextureLabel: { - id: 'app.settings.game-options.setting.key-debug-lightmap-texture.label', - defaultMessage: 'Lightmap texture', - }, - debugImprovedTransparencyLabel: { - id: 'app.settings.game-options.setting.key-debug-improved-transparency.label', - defaultMessage: 'Improved transparency debug view', - }, }) const categoryMessages = defineMessages({ @@ -926,59 +181,6 @@ const categoryMessages = defineMessages({ }, }) -const choiceMessages = defineMessages({ - fast: { id: 'app.settings.game-options.choice.fast', defaultMessage: 'Fast' }, - fancy: { id: 'app.settings.game-options.choice.fancy', defaultMessage: 'Fancy' }, - fabulous: { id: 'app.settings.game-options.choice.fabulous', defaultMessage: 'Fabulous' }, - custom: { id: 'app.settings.game-options.choice.custom', defaultMessage: 'Custom' }, - left: { id: 'app.settings.game-options.choice.left', defaultMessage: 'Left' }, - right: { id: 'app.settings.game-options.choice.right', defaultMessage: 'Right' }, - shown: { id: 'app.settings.game-options.choice.shown', defaultMessage: 'Shown' }, - commandsOnly: { - id: 'app.settings.game-options.choice.commands-only', - defaultMessage: 'Commands only', - }, - hidden: { id: 'app.settings.game-options.choice.hidden', defaultMessage: 'Hidden' }, - all: { id: 'app.settings.game-options.choice.all', defaultMessage: 'All' }, - decreased: { id: 'app.settings.game-options.choice.decreased', defaultMessage: 'Decreased' }, - minimal: { id: 'app.settings.game-options.choice.minimal', defaultMessage: 'Minimal' }, - off: { id: 'app.settings.game-options.choice.off', defaultMessage: 'Off' }, - chat: { id: 'app.settings.game-options.choice.chat', defaultMessage: 'Chat' }, - system: { id: 'app.settings.game-options.choice.system', defaultMessage: 'System' }, - on: { id: 'app.settings.game-options.choice.on', defaultMessage: 'On' }, - minimum: { id: 'app.settings.game-options.choice.minimum', defaultMessage: 'Minimum' }, - maximum: { id: 'app.settings.game-options.choice.maximum', defaultMessage: 'Maximum' }, - never: { id: 'app.settings.game-options.choice.never', defaultMessage: 'Never' }, - pause: { id: 'app.settings.game-options.choice.pause', defaultMessage: 'Pause menu' }, - pauseAndToast: { - id: 'app.settings.game-options.choice.pause-and-toast', - defaultMessage: 'Pause menu and toast', - }, - far: { id: 'app.settings.game-options.choice.far', defaultMessage: 'Far' }, - normal: { id: 'app.settings.game-options.choice.normal', defaultMessage: 'Normal' }, - short: { id: 'app.settings.game-options.choice.short', defaultMessage: 'Short' }, - tiny: { id: 'app.settings.game-options.choice.tiny', defaultMessage: 'Tiny' }, - maxFps: { id: 'app.settings.game-options.choice.max-fps', defaultMessage: 'Max FPS' }, - balanced: { id: 'app.settings.game-options.choice.balanced', defaultMessage: 'Balanced' }, - powerSaver: { id: 'app.settings.game-options.choice.power-saver', defaultMessage: 'Power saver' }, - whileAfk: { id: 'app.settings.game-options.choice.while-afk', defaultMessage: 'While AFK' }, - whenMinimized: { - id: 'app.settings.game-options.choice.when-minimized', - defaultMessage: 'When minimized', - }, - none: { id: 'app.settings.game-options.choice.none', defaultMessage: 'None' }, - byPlayer: { id: 'app.settings.game-options.choice.by-player', defaultMessage: 'By player' }, - nearby: { id: 'app.settings.game-options.choice.nearby', defaultMessage: 'Nearby' }, - crosshair: { id: 'app.settings.game-options.choice.crosshair', defaultMessage: 'Crosshair' }, - hotbar: { id: 'app.settings.game-options.choice.hotbar', defaultMessage: 'Hotbar' }, - constant: { id: 'app.settings.game-options.choice.constant', defaultMessage: 'Constant' }, - default: { id: 'app.settings.game-options.choice.default', defaultMessage: 'Default' }, - frequent: { id: 'app.settings.game-options.choice.frequent', defaultMessage: 'Frequent' }, - limited: { id: 'app.settings.game-options.choice.limited', defaultMessage: 'Limited' }, - openGl: { id: 'app.settings.game-options.choice.opengl', defaultMessage: 'OpenGL' }, - vulkan: { id: 'app.settings.game-options.choice.vulkan', defaultMessage: 'Vulkan' }, -}) - export const presentationMessages = defineMessages({ customValuePlaceholder: { id: 'app.settings.game-options.custom-value.placeholder', @@ -1010,277 +212,34 @@ export const presentationMessages = defineMessages({ }, }) -const knownSettings: Record = - { - fov: { label: settingMessages.fovLabel }, - graphics: { - label: settingMessages.graphicsLabel, - description: settingMessages.graphicsDescription, - }, - ambient_occlusion: { label: settingMessages.ambientOcclusionLabel }, - render_distance: { label: settingMessages.renderDistanceLabel }, - simulation_distance: { - label: settingMessages.simulationDistanceLabel, - description: settingMessages.simulationDistanceDescription, - }, - gui_scale: { - label: settingMessages.guiScaleLabel, - description: settingMessages.guiScaleDescription, - }, - particles: { label: settingMessages.particlesLabel }, - clouds: { label: settingMessages.cloudsLabel }, - entity_shadows: { label: settingMessages.entityShadowsLabel }, - view_bobbing: { - label: settingMessages.viewBobbingLabel, - description: settingMessages.viewBobbingDescription, - }, - vsync: { label: settingMessages.vsyncLabel, description: settingMessages.vsyncDescription }, - fullscreen: { label: settingMessages.fullscreenLabel }, - max_framerate: { label: settingMessages.maxFramerateLabel }, - mipmap_levels: { - label: settingMessages.mipmapLevelsLabel, - description: settingMessages.mipmapLevelsDescription, - }, - biome_blend_radius: { - label: settingMessages.biomeBlendRadiusLabel, - description: settingMessages.biomeBlendRadiusDescription, - }, - language: { label: settingMessages.languageLabel }, - master_volume: { label: settingMessages.masterVolumeLabel }, - music_volume: { label: settingMessages.musicVolumeLabel }, - music_toast: { - label: settingMessages.musicToastLabel, - description: settingMessages.musicToastDescription, - }, - record_volume: { label: settingMessages.recordVolumeLabel }, - weather_volume: { label: settingMessages.weatherVolumeLabel }, - blocks_volume: { label: settingMessages.blocksVolumeLabel }, - hostile_volume: { label: settingMessages.hostileVolumeLabel }, - neutral_volume: { label: settingMessages.neutralVolumeLabel }, - players_volume: { label: settingMessages.playersVolumeLabel }, - ambient_volume: { label: settingMessages.ambientVolumeLabel }, - voice_volume: { label: settingMessages.voiceVolumeLabel }, - ui_volume: { label: settingMessages.uiVolumeLabel }, - sensitivity: { label: settingMessages.sensitivityLabel }, - invert_mouse: { - label: settingMessages.invertMouseLabel, - description: settingMessages.invertMouseDescription, - }, - auto_jump: { - label: settingMessages.autoJumpLabel, - description: settingMessages.autoJumpDescription, - }, - toggle_crouch: { - label: settingMessages.toggleCrouchLabel, - description: settingMessages.toggleCrouchDescription, - }, - toggle_sprint: { - label: settingMessages.toggleSprintLabel, - description: settingMessages.toggleSprintDescription, - }, - discrete_mouse_scroll: { - label: settingMessages.discreteMouseScrollLabel, - description: settingMessages.discreteMouseScrollDescription, - }, - 'key.forward': { label: settingMessages.keyForwardLabel }, - 'key.left': { label: settingMessages.keyLeftLabel }, - 'key.back': { label: settingMessages.keyBackLabel }, - 'key.right': { label: settingMessages.keyRightLabel }, - 'key.jump': { label: settingMessages.keyJumpLabel }, - 'key.sneak': { label: settingMessages.keySneakLabel }, - 'key.sprint': { label: settingMessages.keySprintLabel }, - 'key.inventory': { label: settingMessages.keyInventoryLabel }, - 'key.swap_offhand': { label: settingMessages.keySwapOffhandLabel }, - 'key.drop': { label: settingMessages.keyDropLabel }, - 'key.use': { label: settingMessages.keyUseLabel }, - 'key.attack': { label: settingMessages.keyAttackLabel }, - 'key.pick_item': { label: settingMessages.keyPickItemLabel }, - 'key.chat': { label: settingMessages.keyChatLabel }, - 'key.player_list': { label: settingMessages.keyPlayerListLabel }, - 'key.command': { label: settingMessages.keyCommandLabel }, - 'key.screenshot': { label: settingMessages.keyScreenshotLabel }, - 'key.perspective': { label: settingMessages.keyPerspectiveLabel }, - 'key.fullscreen': { label: settingMessages.keyFullscreenLabel }, - 'key.advancements': { label: settingMessages.keyAdvancementsLabel }, - chat_visibility: { label: settingMessages.chatVisibilityLabel }, - chat_colors: { label: settingMessages.chatColorsLabel }, - chat_links: { - label: settingMessages.chatLinksLabel, - description: settingMessages.chatLinksDescription, - }, - chat_links_prompt: { - label: settingMessages.chatLinksPromptLabel, - description: settingMessages.chatLinksPromptDescription, - }, - chat_opacity: { - label: settingMessages.chatOpacityLabel, - description: settingMessages.chatOpacityDescription, - }, - chat_scale: { label: settingMessages.chatScaleLabel }, - narrator: { - label: settingMessages.narratorLabel, - description: settingMessages.narratorDescription, - }, - subtitles: { - label: settingMessages.subtitlesLabel, - description: settingMessages.subtitlesDescription, - }, - high_contrast: { - label: settingMessages.highContrastLabel, - description: settingMessages.highContrastDescription, - }, - dark_splash: { - label: settingMessages.darkSplashLabel, - description: settingMessages.darkSplashDescription, - }, - notification_time: { - label: settingMessages.notificationTimeLabel, - description: settingMessages.notificationTimeDescription, - }, - main_hand: { - label: settingMessages.mainHandLabel, - description: settingMessages.mainHandDescription, - }, - cape: { label: settingMessages.capeLabel, description: settingMessages.capeDescription }, - hat: { label: settingMessages.hatLabel, description: settingMessages.hatDescription }, - jacket: { label: settingMessages.jacketLabel, description: settingMessages.jacketDescription }, - allow_server_listing: { - label: settingMessages.allowServerListingLabel, - description: settingMessages.allowServerListingDescription, - }, - realms_notifications: { label: settingMessages.realmsNotificationsLabel }, - brightness: { label: catalogSettingMessages.brightnessLabel }, - legacy_view_distance: { label: catalogSettingMessages.legacyViewDistanceLabel }, - entity_distance: { label: catalogSettingMessages.entityDistanceLabel }, - debug_gui_scale: { label: catalogSettingMessages.debugGuiScaleLabel }, - graphics_backend: { label: catalogSettingMessages.graphicsBackendLabel }, - cloud_range: { label: catalogSettingMessages.cloudRangeLabel }, - exclusive_fullscreen: { label: catalogSettingMessages.exclusiveFullscreenLabel }, - mac_fullscreen_menu: { label: catalogSettingMessages.macFullscreenMenuLabel }, - legacy_framerate_limit: { label: catalogSettingMessages.legacyFramerateLimitLabel }, - inactivity_framerate_limit: { - label: catalogSettingMessages.inactivityFramerateLimitLabel, - }, - prioritize_chunk_updates: { label: catalogSettingMessages.prioritizeChunkUpdatesLabel }, - attack_indicator: { label: catalogSettingMessages.attackIndicatorLabel }, - reduced_debug_info: { label: catalogSettingMessages.reducedDebugInfoLabel }, - chunk_fade_time: { label: catalogSettingMessages.chunkFadeTimeLabel }, - cutout_leaves: { label: catalogSettingMessages.cutoutLeavesLabel }, - improved_transparency: { label: catalogSettingMessages.improvedTransparencyLabel }, - texture_filtering: { label: catalogSettingMessages.textureFilteringLabel }, - anisotropy: { label: catalogSettingMessages.anisotropyLabel }, - vignette: { label: catalogSettingMessages.vignetteLabel }, - weather_radius: { label: catalogSettingMessages.weatherRadiusLabel }, - advanced_opengl: { label: catalogSettingMessages.advancedOpenGlLabel }, - anaglyph_3d: { label: catalogSettingMessages.anaglyph3dLabel }, - anisotropic_filtering: { label: catalogSettingMessages.anisotropicFilteringLabel }, - alternate_blocks: { label: catalogSettingMessages.alternateBlocksLabel }, - held_item_tooltips: { label: catalogSettingMessages.heldItemTooltipsLabel }, - use_vbo: { label: catalogSettingMessages.useVboLabel }, - force_unicode_font: { label: catalogSettingMessages.forceUnicodeFontLabel }, - japanese_glyph_variants: { label: catalogSettingMessages.japaneseGlyphVariantsLabel }, - music_frequency: { label: catalogSettingMessages.musicFrequencyLabel }, - directional_audio: { label: catalogSettingMessages.directionalAudioLabel }, - invert_horizontal_mouse: { label: catalogSettingMessages.invertHorizontalMouseLabel }, - toggle_attack: { label: catalogSettingMessages.toggleAttackLabel }, - toggle_use: { label: catalogSettingMessages.toggleUseLabel }, - mouse_wheel_sensitivity: { label: catalogSettingMessages.mouseWheelSensitivityLabel }, - raw_mouse_input: { label: catalogSettingMessages.rawMouseInputLabel }, - touchscreen: { label: catalogSettingMessages.touchscreenLabel }, - allow_cursor_changes: { label: catalogSettingMessages.allowCursorChangesLabel }, - sprint_window: { label: catalogSettingMessages.sprintWindowLabel }, - operator_items_tab: { label: catalogSettingMessages.operatorItemsTabLabel }, - ctrl_click_right_click: { label: catalogSettingMessages.ctrlClickRightClickLabel }, - quit_shortcuts: { label: catalogSettingMessages.quitShortcutsLabel }, - chat_width: { label: catalogSettingMessages.chatWidthLabel }, - focused_chat_height: { label: catalogSettingMessages.focusedChatHeightLabel }, - unfocused_chat_height: { label: catalogSettingMessages.unfocusedChatHeightLabel }, - chat_line_spacing: { label: catalogSettingMessages.chatLineSpacingLabel }, - chat_delay: { label: catalogSettingMessages.chatDelayLabel }, - text_background_opacity: { label: catalogSettingMessages.textBackgroundOpacityLabel }, - chat_background_only: { label: catalogSettingMessages.chatBackgroundOnlyLabel }, - auto_suggestions: { label: catalogSettingMessages.autoSuggestionsLabel }, - secure_chat_only: { label: catalogSettingMessages.secureChatOnlyLabel }, - save_chat_drafts: { label: catalogSettingMessages.saveChatDraftsLabel }, - hide_matched_names: { label: catalogSettingMessages.hideMatchedNamesLabel }, - chat_preview: { label: catalogSettingMessages.chatPreviewLabel }, - fov_effects: { label: catalogSettingMessages.fovEffectsLabel }, - screen_effects: { label: catalogSettingMessages.screenEffectsLabel }, - darkness_pulsing: { label: catalogSettingMessages.darknessPulsingLabel }, - damage_tilt: { label: catalogSettingMessages.damageTiltLabel }, - glint_speed: { label: catalogSettingMessages.glintSpeedLabel }, - glint_strength: { label: catalogSettingMessages.glintStrengthLabel }, - hide_lightning_flashes: { label: catalogSettingMessages.hideLightningFlashesLabel }, - hide_splash_texts: { label: catalogSettingMessages.hideSplashTextsLabel }, - high_contrast_outline: { label: catalogSettingMessages.highContrastOutlineLabel }, - narrator_hotkey: { label: catalogSettingMessages.narratorHotkeyLabel }, - autosave_indicator: { label: catalogSettingMessages.autosaveIndicatorLabel }, - panorama_speed: { label: catalogSettingMessages.panoramaSpeedLabel }, - menu_background_blur: { label: catalogSettingMessages.menuBackgroundBlurLabel }, - rotate_with_minecart: { label: catalogSettingMessages.rotateWithMinecartLabel }, - left_sleeve: { label: catalogSettingMessages.leftSleeveLabel }, - right_sleeve: { label: catalogSettingMessages.rightSleeveLabel }, - left_pants_leg: { label: catalogSettingMessages.leftPantsLegLabel }, - right_pants_leg: { label: catalogSettingMessages.rightPantsLegLabel }, - hide_server_address: { label: catalogSettingMessages.hideServerAddressLabel }, - server_textures: { label: catalogSettingMessages.serverTexturesLabel }, - snooper: { label: catalogSettingMessages.snooperLabel }, - extra_telemetry: { label: catalogSettingMessages.extraTelemetryLabel }, - in_game_notifications: { label: catalogSettingMessages.inGameNotificationsLabel }, - share_presence: { label: catalogSettingMessages.sharePresenceLabel }, - 'key.smooth_camera': { label: catalogKeyMessages.smoothCameraLabel }, - 'key.spectator_outlines': { label: catalogKeyMessages.spectatorOutlinesLabel }, - 'key.save_toolbar': { label: catalogKeyMessages.saveToolbarLabel }, - 'key.load_toolbar': { label: catalogKeyMessages.loadToolbarLabel }, - 'key.social_interactions': { label: catalogKeyMessages.socialInteractionsLabel }, - 'key.quick_actions': { label: catalogKeyMessages.quickActionsLabel }, - 'key.spectator_hotbar': { label: catalogKeyMessages.spectatorHotbarLabel }, - 'key.friends': { label: catalogKeyMessages.friendsLabel }, - 'key.toggle_gui': { label: catalogKeyMessages.toggleGuiLabel }, - 'key.toggle_spectator_shader': { label: catalogKeyMessages.toggleSpectatorShaderLabel }, - 'key.hotbar.1': { label: catalogKeyMessages.hotbar1Label }, - 'key.hotbar.2': { label: catalogKeyMessages.hotbar2Label }, - 'key.hotbar.3': { label: catalogKeyMessages.hotbar3Label }, - 'key.hotbar.4': { label: catalogKeyMessages.hotbar4Label }, - 'key.hotbar.5': { label: catalogKeyMessages.hotbar5Label }, - 'key.hotbar.6': { label: catalogKeyMessages.hotbar6Label }, - 'key.hotbar.7': { label: catalogKeyMessages.hotbar7Label }, - 'key.hotbar.8': { label: catalogKeyMessages.hotbar8Label }, - 'key.hotbar.9': { label: catalogKeyMessages.hotbar9Label }, - 'key.debug.overlay': { label: catalogKeyMessages.debugOverlayLabel }, - 'key.debug.modifier': { label: catalogKeyMessages.debugModifierLabel }, - 'key.debug.reload_chunks': { label: catalogKeyMessages.debugReloadChunksLabel }, - 'key.debug.hitboxes': { label: catalogKeyMessages.debugHitboxesLabel }, - 'key.debug.clear_chat': { label: catalogKeyMessages.debugClearChatLabel }, - 'key.debug.crash': { label: catalogKeyMessages.debugCrashLabel }, - 'key.debug.chunk_borders': { label: catalogKeyMessages.debugChunkBordersLabel }, - 'key.debug.advanced_tooltips': { - label: catalogKeyMessages.debugAdvancedTooltipsLabel, - }, - 'key.debug.copy_recreate_command': { - label: catalogKeyMessages.debugCopyRecreateCommandLabel, - }, - 'key.debug.spectate': { label: catalogKeyMessages.debugSpectateLabel }, - 'key.debug.switch_game_mode': { label: catalogKeyMessages.debugSwitchGameModeLabel }, - 'key.debug.options': { label: catalogKeyMessages.debugOptionsLabel }, - 'key.debug.focus_pause': { label: catalogKeyMessages.debugFocusPauseLabel }, - 'key.debug.dump_dynamic_textures': { - label: catalogKeyMessages.debugDumpDynamicTexturesLabel, - }, - 'key.debug.reload_resource_packs': { - label: catalogKeyMessages.debugReloadResourcePacksLabel, - }, - 'key.debug.profiling': { label: catalogKeyMessages.debugProfilingLabel }, - 'key.debug.copy_location': { label: catalogKeyMessages.debugCopyLocationLabel }, - 'key.debug.dump_version': { label: catalogKeyMessages.debugDumpVersionLabel }, - 'key.debug.profiling_chart': { label: catalogKeyMessages.debugProfilingChartLabel }, - 'key.debug.fps_charts': { label: catalogKeyMessages.debugFpsChartsLabel }, - 'key.debug.network_charts': { label: catalogKeyMessages.debugNetworkChartsLabel }, - 'key.debug.lightmap_texture': { label: catalogKeyMessages.debugLightmapTextureLabel }, - 'key.debug.improved_transparency': { - label: catalogKeyMessages.debugImprovedTransparencyLabel, - }, - } +const settingDescriptions: Record = { + graphics: settingMessages.graphicsDescription, + simulation_distance: settingMessages.simulationDistanceDescription, + gui_scale: settingMessages.guiScaleDescription, + view_bobbing: settingMessages.viewBobbingDescription, + vsync: settingMessages.vsyncDescription, + mipmap_levels: settingMessages.mipmapLevelsDescription, + biome_blend_radius: settingMessages.biomeBlendRadiusDescription, + music_toast: settingMessages.musicToastDescription, + invert_mouse: settingMessages.invertMouseDescription, + auto_jump: settingMessages.autoJumpDescription, + toggle_crouch: settingMessages.toggleCrouchDescription, + toggle_sprint: settingMessages.toggleSprintDescription, + discrete_mouse_scroll: settingMessages.discreteMouseScrollDescription, + chat_links: settingMessages.chatLinksDescription, + chat_links_prompt: settingMessages.chatLinksPromptDescription, + chat_opacity: settingMessages.chatOpacityDescription, + narrator: settingMessages.narratorDescription, + subtitles: settingMessages.subtitlesDescription, + high_contrast: settingMessages.highContrastDescription, + dark_splash: settingMessages.darkSplashDescription, + notification_time: settingMessages.notificationTimeDescription, + main_hand: settingMessages.mainHandDescription, + cape: settingMessages.capeDescription, + hat: settingMessages.hatDescription, + jacket: settingMessages.jacketDescription, + allow_server_listing: settingMessages.allowServerListingDescription, +} const categories: Record = { skin_customization: { @@ -1321,62 +280,6 @@ const categories: Record = { - 'graphics:fast': choiceMessages.fast, - 'graphics:fancy': choiceMessages.fancy, - 'graphics:fabulous': choiceMessages.fabulous, - 'graphics:custom': choiceMessages.custom, - 'main_hand:left': choiceMessages.left, - 'main_hand:right': choiceMessages.right, - 'chat_visibility:0': choiceMessages.shown, - 'chat_visibility:1': choiceMessages.commandsOnly, - 'chat_visibility:2': choiceMessages.hidden, - 'particles:0': choiceMessages.all, - 'particles:1': choiceMessages.decreased, - 'particles:2': choiceMessages.minimal, - 'narrator:0': choiceMessages.off, - 'narrator:1': choiceMessages.all, - 'narrator:2': choiceMessages.chat, - 'narrator:3': choiceMessages.system, - 'clouds:false': choiceMessages.off, - 'clouds:fast': choiceMessages.fast, - 'clouds:true': choiceMessages.fancy, - 'ambient_occlusion:off': choiceMessages.off, - 'ambient_occlusion:on': choiceMessages.on, - 'ambient_occlusion:minimum': choiceMessages.minimum, - 'ambient_occlusion:maximum': choiceMessages.maximum, - 'music_toast:never': choiceMessages.never, - 'music_toast:pause': choiceMessages.pause, - 'music_toast:pause_and_toast': choiceMessages.pauseAndToast, - 'legacy_view_distance:0': choiceMessages.far, - 'legacy_view_distance:1': choiceMessages.normal, - 'legacy_view_distance:2': choiceMessages.short, - 'legacy_view_distance:3': choiceMessages.tiny, - 'legacy_framerate_limit:0': choiceMessages.maxFps, - 'legacy_framerate_limit:1': choiceMessages.balanced, - 'legacy_framerate_limit:2': choiceMessages.powerSaver, - 'inactivity_framerate_limit:afk': choiceMessages.whileAfk, - 'inactivity_framerate_limit:minimized': choiceMessages.whenMinimized, - 'prioritize_chunk_updates:0': choiceMessages.none, - 'prioritize_chunk_updates:1': choiceMessages.byPlayer, - 'prioritize_chunk_updates:2': choiceMessages.nearby, - 'attack_indicator:0': choiceMessages.off, - 'attack_indicator:1': choiceMessages.crosshair, - 'attack_indicator:2': choiceMessages.hotbar, - 'chat_preview:0': choiceMessages.off, - 'chat_preview:1': choiceMessages.commandsOnly, - 'chat_preview:2': choiceMessages.on, - 'music_frequency:CONSTANT': choiceMessages.constant, - 'music_frequency:DEFAULT': choiceMessages.default, - 'music_frequency:FREQUENT': choiceMessages.frequent, - 'share_presence:all': choiceMessages.all, - 'share_presence:limited': choiceMessages.limited, - 'share_presence:none': choiceMessages.none, - 'graphics_backend:default': choiceMessages.default, - 'graphics_backend:opengl': choiceMessages.openGl, - 'graphics_backend:vulkan': choiceMessages.vulkan, -} - const validationMessages: Record = { missing_value: presentationMessages.validationMissingValue, no_compatible_instances: presentationMessages.validationNoCompatibleInstances, @@ -1384,22 +287,13 @@ const validationMessages: Record = changed_since_opened: presentationMessages.validationChangedSinceOpened, } -export function formatGameSettingLabel( - formatMessage: FormatMessage, - setting: EditableGameSetting, -): string { - if (setting.kind === 'external') return setting.raw_key ?? setting.option_id - const definition = knownSettings[setting.option_id] - return definition ? formatMessage(definition.label) : setting.option_id -} - export function formatGameSettingDescription( formatMessage: FormatMessage, setting: EditableGameSetting, ): string { if (setting.kind === 'external') return '' - const definition = knownSettings[setting.option_id] - return definition?.description ? formatMessage(definition.description) : '' + const description = settingDescriptions[setting.option_id] + return description ? formatMessage(description) : '' } export function gameSettingCategoryMessage(category: GameSettingCategory): MessageDescriptor { @@ -1411,15 +305,6 @@ export function gameSettingCategoryMessage(category: GameSettingCategory): Messa ) } -export function formatGameSettingChoice( - formatMessage: FormatMessage, - optionId: string, - value: string, -): string { - const message = choices[`${optionId}:${value}`] - return message ? formatMessage(message) : value -} - export function formatGameSettingValidation( formatMessage: FormatMessage, error: GameOptionValidationError | null | undefined, diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue index 049afab5c1..ef0d768572 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue @@ -12,21 +12,25 @@ import { } from '@modrinth/ui' import { computed, ref } from 'vue' -import type { EditableGameSetting, GameOptionCanonicalValue } from '@/helpers/game-options' +import type { + EditableGameSetting, + GameOptionCanonicalValue, + GameSettingLocaleLabel, +} from '@/helpers/game-options' import GameSettingBooleanControl from './boolean-control.vue' import { canonicalBooleanValue, canonicalValueFromInput, canonicalValueText, + gameSettingNumberScale, isKeybindSetting, settingCanBeEnabled, } from './editors' import GameKeybindInput from './keybind-input.vue' +import { minecraftLanguageOptions } from './languages' import { - formatGameSettingChoice, formatGameSettingDescription, - formatGameSettingLabel, formatGameSettingValidation, presentationMessages, } from './messages' @@ -34,6 +38,7 @@ import { const props = withDefaults( defineProps<{ setting: EditableGameSetting + localeLabel?: GameSettingLocaleLabel keybindConflicts?: string[] disabled?: boolean showSyncToggle?: boolean @@ -88,15 +93,24 @@ const messages = defineMessages({ }, }) -const settingLabel = computed(() => formatGameSettingLabel(formatMessage, props.setting)) +const settingLabel = computed( + () => props.localeLabel?.label ?? props.setting.raw_key ?? props.setting.option_id, +) const settingDescription = computed(() => formatGameSettingDescription(formatMessage, props.setting), ) const valueText = computed(() => canonicalValueText(props.setting)) +const languageOptions = computed[]>(() => { + const value = valueText.value + if (value && !minecraftLanguageOptions.some((option) => option.value === value)) { + return [{ value, label: value }, ...minecraftLanguageOptions] + } + return minecraftLanguageOptions +}) const enumOptions = computed[]>(() => (props.setting.editor.choices ?? []).map((choice) => ({ value: choice.value, - label: formatGameSettingChoice(formatMessage, props.setting.option_id, choice.value), + label: props.localeLabel?.choices[choice.value] ?? choice.value, })), ) const isNumber = computed( @@ -106,7 +120,7 @@ const isSlider = computed( () => isNumber.value && props.setting.editor.min != null && props.setting.editor.max != null, ) const booleanValue = computed(() => canonicalBooleanValue(props.setting)) -const numberScale = computed(() => (props.setting.editor.unit === 'percent' ? 100 : 1)) +const numberScale = computed(() => gameSettingNumberScale(props.setting)) const inputMin = computed(() => props.setting.editor.min === null || props.setting.editor.min === undefined ? undefined @@ -254,6 +268,20 @@ function updateValue(value: string | number | boolean | undefined) { @update:model-value="updateValue" /> + + , + instanceId: MaybeRefOrGetter, + settings: MaybeRefOrGetter, +) { + const { locale } = useVIntl() + const labels = shallowRef>({}) + const optionIds = computed(() => + toValue(settings) + .map((setting) => setting.option_id) + .sort(), + ) + let generation = 0 + let stopListening: UnlistenFn | undefined + let refreshSources = false + + async function refresh() { + if (!toValue(opened) || !optionIds.value.length) return + const request = ++generation + const reindex = refreshSources + refreshSources = false + try { + const result = await get_game_setting_locale_labels( + toValue(instanceId), + locale.value, + optionIds.value, + reindex, + ) + if (request === generation && toValue(opened)) labels.value = result.settings + } catch (error) { + console.debug('Could not load Minecraft setting labels', error) + } + } + + watch( + () => toValue(opened), + (active, _, onCleanup) => { + let cancelled = false + onCleanup(() => { + cancelled = true + stopListening?.() + stopListening = undefined + generation++ + labels.value = {} + }) + if (!active) return + refreshSources = true + void listen('game-option-locales-updated', () => void refresh()) + .then((unlisten) => { + if (cancelled) unlisten() + else { + stopListening = unlisten + void refresh() + } + }) + .catch((error) => { + console.debug('Could not listen for Minecraft setting labels', error) + if (!cancelled) void refresh() + }) + }, + { flush: 'sync' }, + ) + + watch([locale, () => toValue(instanceId), () => optionIds.value.join('\n')], () => { + generation++ + labels.value = {} + void refresh() + }) + + onScopeDispose(() => { + generation++ + stopListening?.() + labels.value = {} + }) + + return labels +} diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue index 5482bbf067..085988ab39 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue @@ -50,7 +50,7 @@ const messages = defineMessages({ }, resourcePacksDescription: { id: 'app.settings.synced-options.resource-packs.description', - defaultMessage: 'Use the same resource packs across your instances', + defaultMessage: 'Use the same resource packs across your instances.', }, dataPacks: { id: 'app.settings.synced-options.data-packs', defaultMessage: 'Sync data packs' }, dataPacksDescription: { @@ -272,13 +272,13 @@ const baseSourcesLoading = computed(() => baseOption.value === null ? false : baseOption.value === 'game_options' - ? gameOptionSourcesQuery.isFetching.value - : instancesQuery.isFetching.value, + ? gameOptionSourcesQuery.isPending.value + : instancesQuery.isPending.value, ) const baseSourcesError = computed(() => baseOption.value === 'game_options' - ? gameOptionSourcesQuery.isError.value - : instancesQuery.isError.value, + ? gameOptionSourcesQuery.isError.value && !gameOptionSourcesQuery.data.value + : instancesQuery.isError.value && !instancesQuery.data.value, ) let baseSourceGeneration = 0 diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue index e5ac9f5698..4f4802b9b7 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue @@ -335,6 +335,8 @@ const messages = defineMessages({ :step="64" :snap-points="snapPoints" :snap-range="512" + min-label="512 MB" + :max-label="`${Number((maxMemory / 1024).toFixed(1))} GB`" unit="MB" />

diff --git a/apps/app-frontend/src/components/ui/world/WorldItem.vue b/apps/app-frontend/src/components/ui/world/WorldItem.vue index c7b1282ba2..1cb1b19655 100644 --- a/apps/app-frontend/src/components/ui/world/WorldItem.vue +++ b/apps/app-frontend/src/components/ui/world/WorldItem.vue @@ -6,6 +6,7 @@ import { EyeIcon, FolderOpenIcon, IssuesIcon, + Link2Icon, MoreVerticalIcon, NoSignalIcon, PlayIcon, @@ -252,6 +253,10 @@ const messages = defineMessages({ id: 'instance.worlds.create_shortcut', defaultMessage: 'Create shortcut', }, + syncedServer: { + id: 'instance.worlds.synced_server', + defaultMessage: 'Synced across instances', + }, linkedServer: { id: 'instance.worlds.linked_server', defaultMessage: 'Managed by server project', @@ -498,6 +503,16 @@ function openContextMenu(event: MouseEvent) { >

+} + +export async function get_game_setting_locale_labels( + instanceId: string | undefined, + locale: string, + optionIds: string[], + refreshSources = false, +): Promise<{ settings: Record }> { + return await invoke('plugin:instance|instance_get_game_setting_locale_labels', { + instanceId, + locale, + optionIds, + refreshSources, + }) +} + export async function list_game_options_sync_sources(): Promise { return await invoke('plugin:instance|instance_list_game_options_sync_sources') } diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index ebeec35d0c..3163ae194e 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -1694,129 +1694,6 @@ "app.settings.game-options.category.video.label": { "message": "Video" }, - "app.settings.game-options.choice.all": { - "message": "All" - }, - "app.settings.game-options.choice.balanced": { - "message": "Balanced" - }, - "app.settings.game-options.choice.by-player": { - "message": "By player" - }, - "app.settings.game-options.choice.chat": { - "message": "Chat" - }, - "app.settings.game-options.choice.commands-only": { - "message": "Commands only" - }, - "app.settings.game-options.choice.constant": { - "message": "Constant" - }, - "app.settings.game-options.choice.crosshair": { - "message": "Crosshair" - }, - "app.settings.game-options.choice.custom": { - "message": "Custom" - }, - "app.settings.game-options.choice.decreased": { - "message": "Decreased" - }, - "app.settings.game-options.choice.default": { - "message": "Default" - }, - "app.settings.game-options.choice.fabulous": { - "message": "Fabulous" - }, - "app.settings.game-options.choice.fancy": { - "message": "Fancy" - }, - "app.settings.game-options.choice.far": { - "message": "Far" - }, - "app.settings.game-options.choice.fast": { - "message": "Fast" - }, - "app.settings.game-options.choice.frequent": { - "message": "Frequent" - }, - "app.settings.game-options.choice.hidden": { - "message": "Hidden" - }, - "app.settings.game-options.choice.hotbar": { - "message": "Hotbar" - }, - "app.settings.game-options.choice.left": { - "message": "Left" - }, - "app.settings.game-options.choice.limited": { - "message": "Limited" - }, - "app.settings.game-options.choice.max-fps": { - "message": "Max FPS" - }, - "app.settings.game-options.choice.maximum": { - "message": "Maximum" - }, - "app.settings.game-options.choice.minimal": { - "message": "Minimal" - }, - "app.settings.game-options.choice.minimum": { - "message": "Minimum" - }, - "app.settings.game-options.choice.nearby": { - "message": "Nearby" - }, - "app.settings.game-options.choice.never": { - "message": "Never" - }, - "app.settings.game-options.choice.none": { - "message": "None" - }, - "app.settings.game-options.choice.normal": { - "message": "Normal" - }, - "app.settings.game-options.choice.off": { - "message": "Off" - }, - "app.settings.game-options.choice.on": { - "message": "On" - }, - "app.settings.game-options.choice.opengl": { - "message": "OpenGL" - }, - "app.settings.game-options.choice.pause": { - "message": "Pause menu" - }, - "app.settings.game-options.choice.pause-and-toast": { - "message": "Pause menu and toast" - }, - "app.settings.game-options.choice.power-saver": { - "message": "Power saver" - }, - "app.settings.game-options.choice.right": { - "message": "Right" - }, - "app.settings.game-options.choice.short": { - "message": "Short" - }, - "app.settings.game-options.choice.shown": { - "message": "Shown" - }, - "app.settings.game-options.choice.system": { - "message": "System" - }, - "app.settings.game-options.choice.tiny": { - "message": "Tiny" - }, - "app.settings.game-options.choice.vulkan": { - "message": "Vulkan" - }, - "app.settings.game-options.choice.when-minimized": { - "message": "When minimized" - }, - "app.settings.game-options.choice.while-afk": { - "message": "While AFK" - }, "app.settings.game-options.compatibility.none": { "message": "Some of your instances cannot use this setting" }, @@ -1988,654 +1865,84 @@ "app.settings.game-options.keybind.unsupported-status": { "message": "That input cannot be used by Minecraft. Press another key." }, - "app.settings.game-options.setting.advanced-opengl.label": { - "message": "Advanced OpenGL" - }, - "app.settings.game-options.setting.allow-cursor-changes.label": { - "message": "Allow cursor changes" - }, "app.settings.game-options.setting.allow-server-listing.description": { "message": "Allow the player's name to appear in server listings." }, - "app.settings.game-options.setting.allow-server-listing.label": { - "message": "Server listings" - }, - "app.settings.game-options.setting.alternate-blocks.label": { - "message": "Alternate blocks" - }, - "app.settings.game-options.setting.ambient-occlusion.label": { - "message": "Smooth lighting" - }, - "app.settings.game-options.setting.ambient-volume.label": { - "message": "Ambient/Environment" - }, - "app.settings.game-options.setting.anaglyph-3d.label": { - "message": "3D anaglyph" - }, - "app.settings.game-options.setting.anisotropic-filtering.label": { - "message": "Anisotropic filtering" - }, - "app.settings.game-options.setting.anisotropy.label": { - "message": "Anisotropy" - }, - "app.settings.game-options.setting.attack-indicator.label": { - "message": "Attack indicator" - }, "app.settings.game-options.setting.auto-jump.description": { "message": "Automatically jump up one-block-high obstacles." }, - "app.settings.game-options.setting.auto-jump.label": { - "message": "Auto-jump" - }, - "app.settings.game-options.setting.auto-suggestions.label": { - "message": "Command suggestions" - }, - "app.settings.game-options.setting.autosave-indicator.label": { - "message": "Autosave indicator" - }, "app.settings.game-options.setting.biome-blend-radius.description": { "message": "The distance over which biome colors transition." }, - "app.settings.game-options.setting.biome-blend-radius.label": { - "message": "Biome blend" - }, - "app.settings.game-options.setting.blocks-volume.label": { - "message": "Blocks" - }, - "app.settings.game-options.setting.brightness.label": { - "message": "Brightness" - }, "app.settings.game-options.setting.cape.description": { "message": "Show the player's cape, including its elytra texture." }, - "app.settings.game-options.setting.cape.label": { - "message": "Cape" - }, - "app.settings.game-options.setting.chat-background-only.label": { - "message": "Chat background only" - }, - "app.settings.game-options.setting.chat-colors.label": { - "message": "Chat colors" - }, - "app.settings.game-options.setting.chat-delay.label": { - "message": "Chat delay" - }, - "app.settings.game-options.setting.chat-line-spacing.label": { - "message": "Chat line spacing" - }, "app.settings.game-options.setting.chat-links-prompt.description": { "message": "Ask before opening links from chat." }, - "app.settings.game-options.setting.chat-links-prompt.label": { - "message": "Prompt on links" - }, "app.settings.game-options.setting.chat-links.description": { "message": "Allow web links in chat to be opened." }, - "app.settings.game-options.setting.chat-links.label": { - "message": "Web links" - }, "app.settings.game-options.setting.chat-opacity.description": { "message": "The opacity of chat text." }, - "app.settings.game-options.setting.chat-opacity.label": { - "message": "Chat opacity" - }, - "app.settings.game-options.setting.chat-preview.label": { - "message": "Chat preview" - }, - "app.settings.game-options.setting.chat-scale.label": { - "message": "Chat scale" - }, - "app.settings.game-options.setting.chat-visibility.label": { - "message": "Chat visibility" - }, - "app.settings.game-options.setting.chat-width.label": { - "message": "Chat width" - }, - "app.settings.game-options.setting.chunk-fade-time.label": { - "message": "Chunk fade time" - }, - "app.settings.game-options.setting.cloud-range.label": { - "message": "Cloud distance" - }, - "app.settings.game-options.setting.clouds.label": { - "message": "Clouds" - }, - "app.settings.game-options.setting.ctrl-click-right-click.label": { - "message": "Control-click as right-click" - }, - "app.settings.game-options.setting.cutout-leaves.label": { - "message": "Cutout leaves" - }, - "app.settings.game-options.setting.damage-tilt.label": { - "message": "Damage tilt" - }, "app.settings.game-options.setting.dark-splash.description": { "message": "Change the Mojang Studios loading screen from red to black." }, - "app.settings.game-options.setting.dark-splash.label": { - "message": "Monochrome logo" - }, - "app.settings.game-options.setting.darkness-pulsing.label": { - "message": "Darkness pulsing" - }, - "app.settings.game-options.setting.debug-gui-scale.label": { - "message": "Debug GUI scale" - }, - "app.settings.game-options.setting.directional-audio.label": { - "message": "Directional audio" - }, "app.settings.game-options.setting.discrete-mouse-scroll.description": { "message": "Treat each mouse-wheel input as a single scroll step." }, - "app.settings.game-options.setting.discrete-mouse-scroll.label": { - "message": "Discrete scrolling" - }, - "app.settings.game-options.setting.entity-distance.label": { - "message": "Entity distance" - }, - "app.settings.game-options.setting.entity-shadows.label": { - "message": "Entity shadows" - }, - "app.settings.game-options.setting.exclusive-fullscreen.label": { - "message": "Exclusive fullscreen" - }, - "app.settings.game-options.setting.extra-telemetry.label": { - "message": "Optional telemetry" - }, - "app.settings.game-options.setting.focused-chat-height.label": { - "message": "Focused chat height" - }, - "app.settings.game-options.setting.force-unicode-font.label": { - "message": "Force Unicode font" - }, - "app.settings.game-options.setting.fov-effects.label": { - "message": "FOV effects" - }, - "app.settings.game-options.setting.fov.label": { - "message": "Field of view" - }, - "app.settings.game-options.setting.fullscreen.label": { - "message": "Fullscreen" - }, - "app.settings.game-options.setting.glint-speed.label": { - "message": "Glint speed" - }, - "app.settings.game-options.setting.glint-strength.label": { - "message": "Glint strength" - }, - "app.settings.game-options.setting.graphics-backend.label": { - "message": "Graphics backend" - }, "app.settings.game-options.setting.graphics.description": { "message": "Controls visual quality and performance." }, - "app.settings.game-options.setting.graphics.label": { - "message": "Graphics" - }, "app.settings.game-options.setting.gui-scale.description": { "message": "The size of the game interface and HUD." }, - "app.settings.game-options.setting.gui-scale.label": { - "message": "GUI scale" - }, "app.settings.game-options.setting.hat.description": { "message": "Show the hat skin layer." }, - "app.settings.game-options.setting.hat.label": { - "message": "Hat" - }, - "app.settings.game-options.setting.held-item-tooltips.label": { - "message": "Held item tooltips" - }, - "app.settings.game-options.setting.hide-lightning-flashes.label": { - "message": "Hide lightning flashes" - }, - "app.settings.game-options.setting.hide-matched-names.label": { - "message": "Hide matched names" - }, - "app.settings.game-options.setting.hide-server-address.label": { - "message": "Hide server address" - }, - "app.settings.game-options.setting.hide-splash-texts.label": { - "message": "Hide splash texts" - }, - "app.settings.game-options.setting.high-contrast-outline.label": { - "message": "High contrast block outline" - }, "app.settings.game-options.setting.high-contrast.description": { "message": "Enhance the contrast of interface elements." }, - "app.settings.game-options.setting.high-contrast.label": { - "message": "High contrast" - }, - "app.settings.game-options.setting.hostile-volume.label": { - "message": "Hostile creatures" - }, - "app.settings.game-options.setting.improved-transparency.label": { - "message": "Improved transparency" - }, - "app.settings.game-options.setting.in-game-notifications.label": { - "message": "In-game notifications" - }, - "app.settings.game-options.setting.inactivity-framerate-limit.label": { - "message": "Reduced framerate" - }, - "app.settings.game-options.setting.invert-horizontal-mouse.label": { - "message": "Invert horizontal mouse" - }, "app.settings.game-options.setting.invert-mouse.description": { "message": "Invert vertical mouse movement." }, - "app.settings.game-options.setting.invert-mouse.label": { - "message": "Invert mouse" - }, "app.settings.game-options.setting.jacket.description": { "message": "Show the jacket skin layer." }, - "app.settings.game-options.setting.jacket.label": { - "message": "Jacket" - }, - "app.settings.game-options.setting.japanese-glyph-variants.label": { - "message": "Japanese glyph variants" - }, - "app.settings.game-options.setting.key-advancements.label": { - "message": "Advancements" - }, - "app.settings.game-options.setting.key-attack.label": { - "message": "Attack" - }, - "app.settings.game-options.setting.key-back.label": { - "message": "Move backward" - }, - "app.settings.game-options.setting.key-chat.label": { - "message": "Open chat" - }, - "app.settings.game-options.setting.key-command.label": { - "message": "Command" - }, - "app.settings.game-options.setting.key-debug-advanced-tooltips.label": { - "message": "Show advanced tooltips" - }, - "app.settings.game-options.setting.key-debug-chunk-borders.label": { - "message": "Show chunk borders" - }, - "app.settings.game-options.setting.key-debug-clear-chat.label": { - "message": "Clear chat" - }, - "app.settings.game-options.setting.key-debug-copy-location.label": { - "message": "Copy location" - }, - "app.settings.game-options.setting.key-debug-copy-recreate-command.label": { - "message": "Copy recreate command" - }, - "app.settings.game-options.setting.key-debug-crash.label": { - "message": "Trigger debug crash" - }, - "app.settings.game-options.setting.key-debug-dump-dynamic-textures.label": { - "message": "Dump dynamic textures" - }, - "app.settings.game-options.setting.key-debug-dump-version.label": { - "message": "Dump version" - }, - "app.settings.game-options.setting.key-debug-focus-pause.label": { - "message": "Pause on lost focus" - }, - "app.settings.game-options.setting.key-debug-fps-charts.label": { - "message": "FPS charts" - }, - "app.settings.game-options.setting.key-debug-hitboxes.label": { - "message": "Show hitboxes" - }, - "app.settings.game-options.setting.key-debug-improved-transparency.label": { - "message": "Improved transparency debug view" - }, - "app.settings.game-options.setting.key-debug-lightmap-texture.label": { - "message": "Lightmap texture" - }, - "app.settings.game-options.setting.key-debug-modifier.label": { - "message": "Debug modifier" - }, - "app.settings.game-options.setting.key-debug-network-charts.label": { - "message": "Network charts" - }, - "app.settings.game-options.setting.key-debug-options.label": { - "message": "Debug options" - }, - "app.settings.game-options.setting.key-debug-overlay.label": { - "message": "Debug overlay" - }, - "app.settings.game-options.setting.key-debug-profiling-chart.label": { - "message": "Profiling chart" - }, - "app.settings.game-options.setting.key-debug-profiling.label": { - "message": "Start profiling" - }, - "app.settings.game-options.setting.key-debug-reload-chunks.label": { - "message": "Reload chunks" - }, - "app.settings.game-options.setting.key-debug-reload-resource-packs.label": { - "message": "Reload resource packs" - }, - "app.settings.game-options.setting.key-debug-spectate.label": { - "message": "Spectate entity" - }, - "app.settings.game-options.setting.key-debug-switch-game-mode.label": { - "message": "Switch game mode" - }, - "app.settings.game-options.setting.key-drop.label": { - "message": "Drop item" - }, - "app.settings.game-options.setting.key-forward.label": { - "message": "Move forward" - }, - "app.settings.game-options.setting.key-friends.label": { - "message": "Friends" - }, - "app.settings.game-options.setting.key-fullscreen.label": { - "message": "Toggle fullscreen" - }, - "app.settings.game-options.setting.key-hotbar-1.label": { - "message": "Hotbar 1" - }, - "app.settings.game-options.setting.key-hotbar-2.label": { - "message": "Hotbar 2" - }, - "app.settings.game-options.setting.key-hotbar-3.label": { - "message": "Hotbar 3" - }, - "app.settings.game-options.setting.key-hotbar-4.label": { - "message": "Hotbar 4" - }, - "app.settings.game-options.setting.key-hotbar-5.label": { - "message": "Hotbar 5" - }, - "app.settings.game-options.setting.key-hotbar-6.label": { - "message": "Hotbar 6" - }, - "app.settings.game-options.setting.key-hotbar-7.label": { - "message": "Hotbar 7" - }, - "app.settings.game-options.setting.key-hotbar-8.label": { - "message": "Hotbar 8" - }, - "app.settings.game-options.setting.key-hotbar-9.label": { - "message": "Hotbar 9" - }, - "app.settings.game-options.setting.key-inventory.label": { - "message": "Inventory" - }, - "app.settings.game-options.setting.key-jump.label": { - "message": "Jump" - }, - "app.settings.game-options.setting.key-left.label": { - "message": "Strafe left" - }, - "app.settings.game-options.setting.key-load-toolbar.label": { - "message": "Load toolbar" - }, - "app.settings.game-options.setting.key-perspective.label": { - "message": "Change perspective" - }, - "app.settings.game-options.setting.key-pick-item.label": { - "message": "Pick block" - }, - "app.settings.game-options.setting.key-player-list.label": { - "message": "Player list" - }, - "app.settings.game-options.setting.key-quick-actions.label": { - "message": "Quick actions" - }, - "app.settings.game-options.setting.key-right.label": { - "message": "Strafe right" - }, - "app.settings.game-options.setting.key-save-toolbar.label": { - "message": "Save toolbar" - }, - "app.settings.game-options.setting.key-screenshot.label": { - "message": "Screenshot" - }, - "app.settings.game-options.setting.key-smooth-camera.label": { - "message": "Toggle cinematic camera" - }, - "app.settings.game-options.setting.key-sneak.label": { - "message": "Sneak" - }, - "app.settings.game-options.setting.key-social-interactions.label": { - "message": "Social interactions" - }, - "app.settings.game-options.setting.key-spectator-hotbar.label": { - "message": "Spectator hotbar" - }, - "app.settings.game-options.setting.key-spectator-outlines.label": { - "message": "Highlight spectators" - }, - "app.settings.game-options.setting.key-sprint.label": { - "message": "Sprint" - }, - "app.settings.game-options.setting.key-swap-offhand.label": { - "message": "Swap offhand" - }, - "app.settings.game-options.setting.key-toggle-gui.label": { - "message": "Toggle HUD" - }, - "app.settings.game-options.setting.key-toggle-spectator-shader.label": { - "message": "Toggle spectator shader" - }, - "app.settings.game-options.setting.key-use.label": { - "message": "Use item" - }, - "app.settings.game-options.setting.language.label": { - "message": "Language" - }, - "app.settings.game-options.setting.left-pants-leg.label": { - "message": "Left pants leg" - }, - "app.settings.game-options.setting.left-sleeve.label": { - "message": "Left sleeve" - }, - "app.settings.game-options.setting.legacy-framerate-limit.label": { - "message": "Framerate limit" - }, - "app.settings.game-options.setting.legacy-view-distance.label": { - "message": "View distance" - }, - "app.settings.game-options.setting.mac-fullscreen-menu.label": { - "message": "Show macOS menu in fullscreen" - }, "app.settings.game-options.setting.main-hand.description": { "message": "Choose whether the main hand is left or right." }, - "app.settings.game-options.setting.main-hand.label": { - "message": "Main hand" - }, - "app.settings.game-options.setting.master-volume.label": { - "message": "Master volume" - }, - "app.settings.game-options.setting.max-framerate.label": { - "message": "Maximum framerate" - }, - "app.settings.game-options.setting.menu-background-blur.label": { - "message": "Menu background blur" - }, "app.settings.game-options.setting.mipmap-levels.description": { "message": "Texture smoothing at a distance." }, - "app.settings.game-options.setting.mipmap-levels.label": { - "message": "Mipmap levels" - }, - "app.settings.game-options.setting.mouse-wheel-sensitivity.label": { - "message": "Mouse wheel sensitivity" - }, - "app.settings.game-options.setting.music-frequency.label": { - "message": "Music frequency" - }, "app.settings.game-options.setting.music-toast.description": { "message": "Choose whether music titles appear in the pause menu and as toasts." }, - "app.settings.game-options.setting.music-toast.label": { - "message": "Music notification" - }, - "app.settings.game-options.setting.music-volume.label": { - "message": "Music" - }, - "app.settings.game-options.setting.narrator-hotkey.label": { - "message": "Narrator hotkey" - }, "app.settings.game-options.setting.narrator.description": { "message": "Choose what the narrator reads." }, - "app.settings.game-options.setting.narrator.label": { - "message": "Narrator" - }, - "app.settings.game-options.setting.neutral-volume.label": { - "message": "Friendly creatures" - }, "app.settings.game-options.setting.notification-time.description": { "message": "How long toast notifications remain visible." }, - "app.settings.game-options.setting.notification-time.label": { - "message": "Notification time" - }, - "app.settings.game-options.setting.operator-items-tab.label": { - "message": "Operator items tab" - }, - "app.settings.game-options.setting.panorama-speed.label": { - "message": "Panorama speed" - }, - "app.settings.game-options.setting.particles.label": { - "message": "Particles" - }, - "app.settings.game-options.setting.players-volume.label": { - "message": "Players" - }, - "app.settings.game-options.setting.prioritize-chunk-updates.label": { - "message": "Prioritize chunk updates" - }, - "app.settings.game-options.setting.quit-shortcuts.label": { - "message": "Quit shortcuts" - }, - "app.settings.game-options.setting.raw-mouse-input.label": { - "message": "Raw mouse input" - }, - "app.settings.game-options.setting.realms-notifications.label": { - "message": "Realms notifications" - }, - "app.settings.game-options.setting.record-volume.label": { - "message": "Jukebox/Note Blocks" - }, - "app.settings.game-options.setting.reduced-debug-info.label": { - "message": "Reduced debug information" - }, - "app.settings.game-options.setting.render-distance.label": { - "message": "Render distance" - }, - "app.settings.game-options.setting.right-pants-leg.label": { - "message": "Right pants leg" - }, - "app.settings.game-options.setting.right-sleeve.label": { - "message": "Right sleeve" - }, - "app.settings.game-options.setting.rotate-with-minecart.label": { - "message": "Rotate with minecart" - }, - "app.settings.game-options.setting.save-chat-drafts.label": { - "message": "Save chat drafts" - }, - "app.settings.game-options.setting.screen-effects.label": { - "message": "Screen effects" - }, - "app.settings.game-options.setting.secure-chat-only.label": { - "message": "Only show secure chat" - }, - "app.settings.game-options.setting.sensitivity.label": { - "message": "Mouse sensitivity" - }, - "app.settings.game-options.setting.server-textures.label": { - "message": "Server textures" - }, - "app.settings.game-options.setting.share-presence.label": { - "message": "Share presence" - }, "app.settings.game-options.setting.simulation-distance.description": { "message": "How far away entities update and blocks and fluids tick." }, - "app.settings.game-options.setting.simulation-distance.label": { - "message": "Simulation distance" - }, - "app.settings.game-options.setting.snooper.label": { - "message": "Snooper" - }, - "app.settings.game-options.setting.sprint-window.label": { - "message": "Sprint window" - }, "app.settings.game-options.setting.subtitles.description": { "message": "Show captions for sounds played in the game." }, - "app.settings.game-options.setting.subtitles.label": { - "message": "Subtitles" - }, - "app.settings.game-options.setting.text-background-opacity.label": { - "message": "Text background opacity" - }, - "app.settings.game-options.setting.texture-filtering.label": { - "message": "Texture filtering" - }, - "app.settings.game-options.setting.toggle-attack.label": { - "message": "Toggle attack" - }, "app.settings.game-options.setting.toggle-crouch.description": { "message": "Press once to remain crouched." }, - "app.settings.game-options.setting.toggle-crouch.label": { - "message": "Toggle crouch" - }, "app.settings.game-options.setting.toggle-sprint.description": { "message": "Press once to remain sprinting." }, - "app.settings.game-options.setting.toggle-sprint.label": { - "message": "Toggle sprint" - }, - "app.settings.game-options.setting.toggle-use.label": { - "message": "Toggle use" - }, - "app.settings.game-options.setting.touchscreen.label": { - "message": "Touchscreen mode" - }, - "app.settings.game-options.setting.ui-volume.label": { - "message": "UI" - }, - "app.settings.game-options.setting.unfocused-chat-height.label": { - "message": "Unfocused chat height" - }, - "app.settings.game-options.setting.use-vbo.label": { - "message": "Use VBOs" - }, "app.settings.game-options.setting.view-bobbing.description": { "message": "Add a bobbing motion to the camera while walking." }, - "app.settings.game-options.setting.view-bobbing.label": { - "message": "View bobbing" - }, - "app.settings.game-options.setting.vignette.label": { - "message": "Vignette" - }, - "app.settings.game-options.setting.voice-volume.label": { - "message": "Voice and speech" - }, "app.settings.game-options.setting.vsync.description": { "message": "Limit the frame rate to the display refresh rate to prevent screen tearing." }, - "app.settings.game-options.setting.vsync.label": { - "message": "VSync" - }, - "app.settings.game-options.setting.weather-radius.label": { - "message": "Weather radius" - }, - "app.settings.game-options.setting.weather-volume.label": { - "message": "Weather" - }, "app.settings.game-options.validation.changed-since-opened": { "message": "This setting changed elsewhere. Check it and try again." }, @@ -2943,7 +2250,7 @@ "message": "Sync resource packs" }, "app.settings.synced-options.resource-packs.description": { - "message": "Use the same resource packs across your instances" + "message": "Use the same resource packs across your instances." }, "app.settings.synced-options.resource-packs.none-synced-yet": { "message": "You haven't synced any resource packs yet" @@ -4277,6 +3584,9 @@ "instance.worlds.shortcut-creation-failed": { "message": "Failed to create shortcut" }, + "instance.worlds.synced_server": { + "message": "Synced across instances" + }, "instance.worlds.view_instance": { "message": "View instance" }, diff --git a/apps/app-frontend/src/pages/Screenshots.vue b/apps/app-frontend/src/pages/Screenshots.vue index 650acb3faa..00835b8f65 100644 --- a/apps/app-frontend/src/pages/Screenshots.vue +++ b/apps/app-frontend/src/pages/Screenshots.vue @@ -1,5 +1,5 @@ diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue index a660b09eaf..c94f3b01a3 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue @@ -228,6 +228,8 @@ const messages = defineMessages({ :step="64" :snap-points="snapPoints" :snap-range="512" + min-label="512 MB" + :max-label="`${Number((maxMemory / 1024).toFixed(1))} GB`" unit="MB" />
diff --git a/apps/app/src/api/instance.rs b/apps/app/src/api/instance.rs index ef1e4165b9..f49a365a8d 100644 --- a/apps/app/src/api/instance.rs +++ b/apps/app/src/api/instance.rs @@ -69,6 +69,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { instance_set_global_synced_option, instance_list_game_options_sync_sources, instance_get_synced_game_options_config, + instance_get_game_setting_locale_labels, instance_preview_synced_game_option_changes, instance_save_synced_game_option_changes, instance_get_local_game_options_config, @@ -887,6 +888,13 @@ pub async fn instance_get_synced_game_options_config() Ok(theseus::instance::get_synced_game_options_config().await?) } +#[tauri::command] +pub async fn instance_get_game_setting_locale_labels( + instance_id: Option, locale: String, option_ids: Vec, refresh_sources: bool, +) -> Result { + Ok(theseus::instance::get_game_setting_locale_labels(instance_id.as_deref(), &locale, option_ids, refresh_sources).await?) +} + #[tauri::command] pub async fn instance_preview_synced_game_option_changes( request: theseus::instance::UpdateGameSettingsRequest, diff --git a/packages/app-lib/.sqlx/query-5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b.json b/packages/app-lib/.sqlx/query-5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b.json new file mode 100644 index 0000000000..8a7b1c4bd1 --- /dev/null +++ b/packages/app-lib/.sqlx/query-5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE game_option_locale_origins SET origin_json = ?\n\t\tWHERE scope = ? AND option_id = ? AND origin_json IS NULL", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b" +} diff --git a/packages/app-lib/.sqlx/query-c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961.json b/packages/app-lib/.sqlx/query-c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961.json new file mode 100644 index 0000000000..df0d2dd0bb --- /dev/null +++ b/packages/app-lib/.sqlx/query-c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961.json @@ -0,0 +1,56 @@ +{ + "db_name": "SQLite", + "query": "SELECT scope, option_id, source_instance_id, source_game_version,\n\t\tbackfilled AS \"backfilled!: bool\", observation_json, origin_json\n\t\tFROM game_option_locale_origins\n\t\tWHERE (? IS NULL OR scope = ?)\n\t\tORDER BY scope, option_id", + "describe": { + "columns": [ + { + "name": "scope", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "option_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "source_instance_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "source_game_version", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "backfilled!: bool", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "observation_json", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "origin_json", + "ordinal": 6, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + true, + false, + true, + true + ] + }, + "hash": "c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961" +} diff --git a/packages/app-lib/.sqlx/query-d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827.json b/packages/app-lib/.sqlx/query-d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827.json new file mode 100644 index 0000000000..659da5a9fa --- /dev/null +++ b/packages/app-lib/.sqlx/query-d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO game_option_locale_origins\n\t\t(scope, option_id, source_instance_id, source_game_version, observation_json)\n\t\tSELECT ?, ?, ?, ?, ?\n\t\tWHERE ? <> '' OR EXISTS (SELECT 1 FROM synced_game_option_values WHERE option_id = ?)\n\t\tON CONFLICT(scope, option_id) DO UPDATE SET observation_json = excluded.observation_json\n\t\tWHERE game_option_locale_origins.observation_json IS NULL\n\t\tAND game_option_locale_origins.origin_json IS NULL\n\t\tAND game_option_locale_origins.backfilled = 0\n\t\tAND game_option_locale_origins.source_instance_id = excluded.source_instance_id\n\t\tAND game_option_locale_origins.source_game_version = excluded.source_game_version", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827" +} diff --git a/packages/app-lib/migrations/20260908120000_game-option-locales.sql b/packages/app-lib/migrations/20260908120000_game-option-locales.sql new file mode 100644 index 0000000000..8601b9e8ae --- /dev/null +++ b/packages/app-lib/migrations/20260908120000_game-option-locales.sql @@ -0,0 +1,24 @@ +CREATE TABLE game_option_locale_origins ( + scope TEXT NOT NULL, + option_id TEXT NOT NULL, + source_instance_id TEXT, + source_game_version TEXT, + backfilled INTEGER NOT NULL DEFAULT 0, + observation_json TEXT, + origin_json TEXT, + PRIMARY KEY (scope, option_id) +); + +INSERT INTO game_option_locale_origins + (scope, option_id, source_instance_id, source_game_version, backfilled) +SELECT '', option_id, source_instance_id, source_game_version, 1 +FROM synced_game_option_values; + +CREATE TRIGGER record_game_option_locale_origin +AFTER INSERT ON synced_game_option_values +BEGIN + INSERT INTO game_option_locale_origins + (scope, option_id, source_instance_id, source_game_version) + VALUES ('', NEW.option_id, NEW.source_instance_id, NEW.source_game_version) + ON CONFLICT(scope, option_id) DO NOTHING; +END; diff --git a/packages/app-lib/src/api/instance.rs b/packages/app-lib/src/api/instance.rs index e9ae3da69d..3ae711b885 100644 --- a/packages/app-lib/src/api/instance.rs +++ b/packages/app-lib/src/api/instance.rs @@ -93,6 +93,7 @@ pub use self::shared::{ unlink_shared_instance, unpublish_shared_instance, update_shared_instance, }; pub use self::synced_options::game_options::{ + GameSettingLocaleLabels, get_game_setting_locale_labels, CanonicalValue as GameOptionCanonicalValue, EditableGameSetting, GameOptionCompatibility, GameOptionCompatibilityBucket, GameOptionCompatibilityReason, GameOptionCompatibilityStatus, @@ -113,6 +114,7 @@ pub use self::synced_options::game_options::{ sync_before_launch as sync_game_options_before_launch, }; pub(crate) use self::synced_options::game_options::{ + GameLocaleIndexer, queue_game_locale_index, start_game_locale_indexer, shared_fullscreen_value, sync_all_participating_instances, update_shared_fullscreen_from_app, }; diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs b/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs index 0bbedd5464..e73415be40 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs @@ -69,20 +69,12 @@ pub(in crate::api::instance) const AMBIENT_OCCLUSION_KEYS: &[VersionedKey] = &[ }, ]; -pub(in crate::api::instance) const FOV_KEYS: &[VersionedKey] = &[ - VersionedKey { - key: "fov", - since: "1.0", - until: "1.18.2", - mapping: GameOptionMappingKind::Legacy, - }, - VersionedKey { - key: "fov", - since: "1.19", - until: "26.3", - mapping: GameOptionMappingKind::Direct, - }, -]; +pub(in crate::api::instance) const FOV_KEYS: &[VersionedKey] = &[VersionedKey { + key: "fov", + since: "1.0", + until: "26.3", + mapping: GameOptionMappingKind::Direct, +}]; pub(in crate::api::instance) const CLOUD_KEYS: &[VersionedKey] = &[ VersionedKey { @@ -551,14 +543,8 @@ pub(in crate::api::instance) fn encode_value( (ValueEncoding::Fov, CanonicalValue::Integer(value)) if (30..=110).contains(value) => { - if release_version(game_version) - .is_some_and(|version| version >= (1, 19, 0)) - { - Some(value.to_string()) - } else { - let normalized = (*value as f64 - 70.0) / 40.0; - Some(format_decimal(normalized)) - } + let normalized = (*value as f64 - 70.0) / 40.0; + Some(format_decimal(normalized)) } (ValueEncoding::GuiScale, CanonicalValue::Integer(value)) if (0..=8).contains(value) @@ -914,14 +900,9 @@ pub(in crate::api::instance) fn physical_representation_supported_for_target( }; } if matches!(definition.encoding, ValueEncoding::Fov) { - return if target_version >= (1, 19, 0) { - raw.parse::() - .is_ok_and(|value| (30..=110).contains(&value)) - } else { - raw.parse::().is_ok_and(|value| { - value.is_finite() && (-1.0..=1.0).contains(&value) - }) - }; + return raw.parse::().is_ok_and(|value| { + value.is_finite() && (-1.0..=1.0).contains(&value) + }); } if matches!(definition.encoding, ValueEncoding::ChatPreview) { return if target_version == (1, 19, 0) { diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/archive.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/archive.rs new file mode 100644 index 0000000000..341e0604a6 --- /dev/null +++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/archive.rs @@ -0,0 +1,187 @@ +use super::super::options_file::{input_error, sha1_bytes}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::io::{Cursor, Read, Seek}; +use std::path::Path; +use zip::ZipArchive; + +pub(super) const MAX_LANGUAGE_BYTES: usize = 4 * 1024 * 1024; +const MAX_ARCHIVE_ENTRIES: usize = 100_000; +const MAX_NESTED_BYTES: usize = 64 * 1024 * 1024; +const MAX_TOTAL_BYTES: usize = 128 * 1024 * 1024; +const MAX_NESTED_DEPTH: usize = 4; +pub(super) type Translations = BTreeMap; + +#[derive(Default, Serialize, Deserialize)] +pub(super) struct ArchiveIndex { + #[serde(default)] + pub version: u32, + pub bundles: Vec, +} + +#[derive(Default, Serialize, Deserialize)] +pub(super) struct LanguageBundle { + pub nested_path: String, + pub locales: BTreeMap, + #[serde(default)] + pub deprecated: DeprecatedTranslations, +} + +#[derive(Default, Serialize, Deserialize)] +pub(super) struct DeprecatedTranslations { + #[serde(default)] + removed: Vec, + #[serde(default)] + renamed: BTreeMap, +} + +impl DeprecatedTranslations { + pub fn apply(&self, translations: &mut Translations) { + for key in &self.removed { + translations.remove(key); + } + for (from, to) in &self.renamed { + if let Some(value) = translations.remove(from) { + translations.insert(to.clone(), value); + } else { + translations.remove(to); + } + } + } +} + +pub(super) fn parse_language(bytes: &[u8], legacy: bool) -> crate::Result { + if bytes.len() > MAX_LANGUAGE_BYTES { + return Err(input_error("Minecraft language file exceeds size limit")); + } + let values: Translations = if legacy { + String::from_utf8_lossy(bytes) + .lines() + .filter(|line| !line.starts_with('#')) + .filter_map(|line| line.split_once('=')) + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect() + } else { + serde_json::from_slice::>(bytes)? + .into_iter() + .filter_map(|(key, value)| value.as_str().map(|s| (key, s.to_owned()))) + .collect() + }; + Ok(values.into_iter().filter(|(key, value)| { + !key.is_empty() && key.len() <= 1024 && value.len() <= 32 * 1024 + }).collect()) +} + +pub(super) fn inspect(path: &Path, expected_hash: &str) -> crate::Result { + let mut file = std::fs::File::open(path).map_err(crate::util::io::IOError::from)?; + let mut hasher = sha1_smol::Sha1::new(); + let mut buffer = [0; 64 * 1024]; + loop { + let count = file.read(&mut buffer).map_err(crate::util::io::IOError::from)?; + if count == 0 { break; } + hasher.update(&buffer[..count]); + } + if hasher.digest().to_string() != expected_hash { + return Err(input_error("Locale source changed before it could be indexed")); + } + file.rewind().map_err(crate::util::io::IOError::from)?; + let mut index = ArchiveIndex { version: 2, ..Default::default() }; + let mut budget = MAX_TOTAL_BYTES; + let mut entries = MAX_ARCHIVE_ENTRIES; + inspect_archive(file, "", 0, &mut budget, &mut entries, &mut index)?; + Ok(index) +} + +fn read_entry( + archive: &mut ZipArchive, name: &str, limit: usize, budget: &mut usize, +) -> crate::Result> { + let mut entry = archive.by_name(name).map_err(|e| input_error(e.to_string()))?; + let limit = limit.min(*budget); + if entry.size() > limit as u64 { + return Err(input_error("Locale archive entry exceeds size limit")); + } + let mut bytes = Vec::new(); + entry.by_ref().take(limit as u64 + 1).read_to_end(&mut bytes) + .map_err(crate::util::io::IOError::from)?; + if bytes.len() > limit { + return Err(input_error("Locale archive entry exceeds size limit")); + } + *budget -= bytes.len(); + Ok(bytes) +} + +fn inspect_archive( + reader: R, nested_path: &str, depth: usize, budget: &mut usize, + entries: &mut usize, index: &mut ArchiveIndex, +) -> crate::Result<()> { + let mut archive = ZipArchive::new(reader).map_err(|e| input_error(e.to_string()))?; + if archive.len() > *entries { + return Err(input_error("Too many entries in locale archive")); + } + *entries -= archive.len(); + let names: BTreeSet = archive.file_names().map(str::to_owned).collect(); + let mut bundle = LanguageBundle { nested_path: nested_path.to_owned(), ..Default::default() }; + for name in &names { + let parts: Vec<_> = name.split('/').collect(); + let (namespace, filename) = match parts.as_slice() { + ["assets", namespace, "lang", filename] => (*namespace, *filename), + ["lang", filename] => ("minecraft", *filename), + _ => continue, + }; + if filename == "deprecated.json" { + if namespace == "minecraft" { + let bytes = read_entry(&mut archive, name, MAX_LANGUAGE_BYTES, budget)?; + bundle.deprecated = serde_json::from_slice(&bytes)?; + } + continue; + } + let legacy = filename.ends_with(".lang"); + let Some(locale) = filename.strip_suffix(if legacy { ".lang" } else { ".json" }) else { continue; }; + if !valid_locale(locale) { continue; } + let bytes = read_entry(&mut archive, name, MAX_LANGUAGE_BYTES, budget)?; + match parse_language(&bytes, legacy) { + Ok(values) => bundle.locales.entry(locale.to_ascii_lowercase()).or_default().extend(values), + Err(error) => tracing::debug!(%name, %error, "Skipping malformed mod language file"), + } + } + index.bundles.push(bundle); + let mut nested = BTreeSet::new(); + for manifest in ["fabric.mod.json", "quilt.mod.json", "META-INF/jarjar/metadata.json"] { + if !names.contains(manifest) { continue; } + let bytes = read_entry(&mut archive, manifest, MAX_LANGUAGE_BYTES, budget)?; + let Ok(value) = serde_json::from_slice::(&bytes) else { continue; }; + let jars = value.get("jars").or_else(|| value.pointer("/quilt_loader/jars")); + for jar in jars.and_then(|v| v.as_array()).into_iter().flatten() { + if let Some(path) = jar.as_str().or_else(|| jar.get("file").and_then(|v| v.as_str())) + .or_else(|| jar.get("path").and_then(|v| v.as_str())) { + nested.insert(path.to_owned()); + } + } + } + if !nested.is_empty() && depth >= MAX_NESTED_DEPTH { + return Err(input_error("Locale archive nesting exceeds limit")); + } + for name in nested { + if !names.contains(&name) || !name.ends_with(".jar") { continue; } + let bytes = read_entry(&mut archive, &name, MAX_NESTED_BYTES, budget)?; + let path = if nested_path.is_empty() { name } else { format!("{nested_path}!{name}") }; + inspect_archive(Cursor::new(bytes), &path, depth + 1, budget, entries, index)?; + } + Ok(()) +} + +pub(super) fn valid_locale(locale: &str) -> bool { + !locale.is_empty() && locale.len() <= 32 + && locale.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_') +} + +pub(super) fn valid_hash(hash: &str) -> bool { + hash.len() == 40 && hash.bytes().all(|c| c.is_ascii_hexdigit()) +} + +pub(super) fn checked_bytes(bytes: Vec, hash: &str) -> crate::Result> { + if sha1_bytes(&bytes) != hash { + return Err(input_error("Minecraft locale resource checksum mismatch")); + } + Ok(bytes) +} diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/catalog.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/catalog.rs new file mode 100644 index 0000000000..95f815b608 --- /dev/null +++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/catalog.rs @@ -0,0 +1,138 @@ +/// Translation keys mapped to settings. +pub(super) fn translation_keys(raw: &str, vanilla: bool) -> Vec { + if let Some(key) = raw.strip_prefix("key_") { return vec![key.to_owned()]; } + if !vanilla { return Vec::new(); } + if let Some(key) = raw.strip_prefix("soundCategory_") { return vec![format!("soundCategory.{key}")]; } + if let Some(key) = raw.strip_prefix("modelPart_") { return vec![format!("options.modelPart.{key}")]; } + let aliases: &[&str] = match raw { + "lang" => &["options.language"], + "mouseSensitivity" => &["options.sensitivity"], + "enableVsync" => &["options.vsync"], + "bobView" => &["options.viewBobbing"], + "maxFps" | "fpsLimit" => &["options.framerateLimit"], + "fancyGraphics" | "graphicsMode" => &["options.graphics"], + "graphicsPreset" => &["options.graphics.preset"], + "renderClouds" => &["options.renderClouds", "options.clouds"], + "cloudRange" => &["options.renderCloudsDistance"], + "viewDistance" => &["options.renderDistance"], + "invertYMouse" => &["options.invertMouseY", "options.invertMouse"], + "invertXMouse" => &["options.invertMouseX"], + "toggleCrouch" => &["key.sneak"], + "toggleSprint" => &["key.sprint"], + "toggleAttack" => &["key.attack"], + "toggleUse" => &["key.use"], + "chatVisibility" => &["options.chat.visibility"], + "chatColors" => &["options.chat.color"], + "chatLinks" => &["options.chat.links"], + "chatLinksPrompt" => &["options.chat.links.prompt"], + "chatOpacity" => &["options.chat.opacity"], + "chatScale" => &["options.chat.scale"], + "chatWidth" => &["options.chat.width"], + "chatHeightFocused" => &["options.chat.height.focused"], + "chatHeightUnfocused" => &["options.chat.height.unfocused"], + "chatLineSpacing" => &["options.chat.line_spacing"], + "chatDelay" => &["options.chat.delay_instant"], + "autoSuggestions" => &["options.autoSuggestCommands"], + "textBackgroundOpacity" => &["options.accessibility.text_background_opacity"], + "backgroundForChatOnly" => &["options.accessibility.text_background"], + "darkMojangStudiosBackground" => &["options.darkMojangStudiosBackgroundColor"], + "highContrast" => &["options.accessibility.high_contrast"], + "highContrastBlockOutline" => &["options.accessibility.high_contrast_block_outline"], + "menuBackgroundBlurriness" => &["options.accessibility.menu_background_blurriness"], + "notificationDisplayTime" => &["options.notifications.display_time"], + "panoramaScrollSpeed" => &["options.accessibility.panorama_speed"], + "narratorHotkey" => &["options.accessibility.narrator_hotkey"], + "showAutosaveIndicator" => &["options.autosaveIndicator"], + "soundDevice" => &["options.audioDevice"], + "musicFrequency" => &["options.music_frequency"], + "music" => &["soundCategory.music", "options.music"], + "sound" => &["soundCategory.master", "options.sound"], + "realmsNotifications" => &["options.realmsNotifications.button"], + "telemetryOptInExtra" => &["options.telemetry.button"], + "saveChatDrafts" => &["options.chat.drafts"], + "chunkSectionFadeInTime" => &["options.chunkFade"], + "maxAnisotropyBit" => &["options.maxAnisotropy"], + "preferredGraphicsBackend" => &["options.graphicsApi"], + _ => &[], + }; + aliases.iter().map(|key| (*key).to_owned()) + .chain(std::iter::once(format!("options.{raw}"))).collect() +} + +pub(super) fn choice_keys(option_id: &str) -> &'static [(&'static str, &'static str)] { + match option_id { + "graphics" => &[("fast", "options.graphics.fast"), ("fancy", "options.graphics.fancy"), ("fabulous", "options.graphics.fabulous"), ("custom", "options.graphics.custom")], + "clouds" => &[("false", "options.off"), ("fast", "options.clouds.fast"), ("true", "options.clouds.fancy")], + "particles" => &[("0", "options.particles.all"), ("1", "options.particles.decreased"), ("2", "options.particles.minimal")], + "chat_visibility" => &[("0", "options.chat.visibility.full"), ("1", "options.chat.visibility.system"), ("2", "options.chat.visibility.hidden")], + "main_hand" => &[("left", "options.mainHand.left"), ("right", "options.mainHand.right")], + "narrator" => &[ + ("0", "options.narrator.off"), + ("1", "options.narrator.all"), + ("2", "options.narrator.chat"), + ("3", "options.narrator.system"), + ], + "ambient_occlusion" => &[ + ("off", "options.ao.off"), + ("on", "options.on"), + ("minimum", "options.ao.min"), + ("maximum", "options.ao.max"), + ], + "music_toast" => &[ + ("never", "options.musicToast.never"), + ("pause", "options.musicToast.pauseMenu"), + ("pause_and_toast", "options.musicToast.pauseMenuAndToast"), + ], + "legacy_view_distance" => &[ + ("0", "options.renderDistance.far"), + ("1", "options.renderDistance.normal"), + ("2", "options.renderDistance.short"), + ("3", "options.renderDistance.tiny"), + ], + "legacy_framerate_limit" => &[ + ("0", "options.framerateLimit.max"), + ("1", "options.framerateLimit.balanced"), + ("2", "options.framerateLimit.powersaver"), + ], + "inactivity_framerate_limit" => &[ + ("afk", "options.inactivityFpsLimit.afk"), + ("minimized", "options.inactivityFpsLimit.minimized"), + ], + "prioritize_chunk_updates" => &[ + ("0", "options.prioritizeChunkUpdates.none"), + ("1", "options.prioritizeChunkUpdates.byPlayer"), + ("2", "options.prioritizeChunkUpdates.nearby"), + ], + "attack_indicator" => &[ + ("0", "options.off"), + ("1", "options.attack.crosshair"), + ("2", "options.attack.hotbar"), + ], + "chat_preview" => &[ + ("0", "options.off"), + ("1", "options.chatPreview.live"), + ("2", "options.chatPreview.confirm"), + ], + "music_frequency" => &[ + ("CONSTANT", "options.music_frequency.constant"), + ("DEFAULT", "options.music_frequency.default"), + ("FREQUENT", "options.music_frequency.frequent"), + ], + "share_presence" => &[ + ("all", "options.sharePresence.all"), + ("limited", "options.sharePresence.limited"), + ("none", "options.sharePresence.none"), + ], + "graphics_backend" => &[ + ("default", "options.graphicsApi.default"), + ("opengl", "options.graphicsApi.opengl"), + ("vulkan", "options.graphicsApi.vulkan"), + ], + "texture_filtering" => &[ + ("0", "options.textureFiltering.none"), + ("1", "options.textureFiltering.rgss"), + ("2", "options.textureFiltering.anisotropic"), + ], + _ => &[], + } +} diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs new file mode 100644 index 0000000000..5d4e7011ee --- /dev/null +++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs @@ -0,0 +1,301 @@ +//! Caches game translations and remembers which JAR supplied each setting. + +mod archive; +mod catalog; +mod sources; +mod storage; + +use super::catalog::setting_by_file_key; +use super::options_file::{GameOptionsDocument, input_error, options_path, read_document}; +use crate::state::{InstanceMetadata, State}; +use archive::{ArchiveIndex, Translations}; +use serde::{Deserialize, Serialize}; +use sources::{ArchiveSource, AssetIndexSource, Snapshot}; +use std::collections::{BTreeMap, HashMap}; +use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; +use tokio::sync::Notify; + +#[derive(Default)] +pub(crate) struct GameLocaleIndexer { + notify: Notify, + started: AtomicBool, +} + +#[derive(Clone, Serialize, Deserialize)] +struct Observation { + snapshot: String, + raw_key: String, +} + +#[derive(Clone, Serialize, Deserialize)] +struct Origin { + instance_id: String, + game_version: String, + game_jar_hash: String, + asset_index: Option, + archive: ArchiveSource, + nested_path: String, + translation_key: String, + choices: BTreeMap, +} + +#[derive(Default, Serialize)] +pub struct GameSettingLocaleLabels { + pub settings: BTreeMap, +} + +#[derive(Serialize)] +pub struct GameSettingLocaleLabel { + pub label: String, + pub choices: BTreeMap, +} + +struct Candidate { + snapshot_id: String, + snapshot: Snapshot, + keys: BTreeMap, +} + +pub(crate) fn queue_game_locale_index() { + if let Some(state) = State::get_if_initialized() { + state.game_locale_indexer.notify.notify_one(); + } +} + +pub(crate) fn start_game_locale_indexer(state: Arc) { + if state.game_locale_indexer.started.swap(true, Ordering::AcqRel) { return; } + state.game_locale_indexer.notify.notify_one(); + tokio::spawn(async move { + loop { + state.game_locale_indexer.notify.notified().await; + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + if let Err(error) = index_installed_sources(&state).await { + tracing::warn!(%error, "Could not index game-setting translations"); + } + #[cfg(feature = "tauri")] + { + use tauri::Emitter; + let _ = crate::EventState::get().app.emit("game-option-locales-updated", ()); + } + } + }); +} + +fn document_keys(document: &GameOptionsDocument) -> BTreeMap { + document.effective_entries().keys().map(|key| { + let id = setting_by_file_key(key).map(|s| s.id.to_owned()) + .unwrap_or_else(|| format!("external:{key}")); + (id, (*key).to_owned()) + }).filter(|(id, key)| !catalog::translation_keys(key, !id.starts_with("external:")).is_empty()).collect() +} + +/// Records Minecraft and mod JAR hashes before saving new options. +pub(super) async fn capture_observation(metadata: &InstanceMetadata, state: &State) -> Option { + match sources::snapshot_instance(metadata, state).await { + Ok(id) => Some(id), + Err(error) => { + tracing::debug!(%error, "Game-setting locale source is not available yet"); + None + } + } +} + +pub(super) async fn record_observations( + tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, metadata: &InstanceMetadata, + document: &GameOptionsDocument, snapshot: Option<&str>, +) { + let Some(snapshot) = snapshot else { return; }; + for (id, raw_key) in document_keys(document) { + let observation = Observation { snapshot: snapshot.to_owned(), raw_key }; + if let Err(error) = storage::observe(tx, "", &id, &metadata.instance.id, + &metadata.applied_content_set.game_version, &observation).await { + tracing::warn!(%error, "Could not record game-setting translation provenance"); + } + } +} + +async fn cached_archive( + state: &State, source: &ArchiveSource, cache: &mut HashMap>, +) -> crate::Result> { + if let Some(index) = cache.get(&source.hash) { return Ok(index.clone()); } + let index = Arc::new(sources::archive_index(state, source).await?); + if cache.len() >= 8 { cache.clear(); } + cache.insert(source.hash.clone(), index.clone()); + Ok(index) +} + +async fn index_installed_sources(state: &State) -> crate::Result<()> { + let mut instances = crate::state::list_instances(&state.pool).await?; + instances.sort_by(|a, b| a.instance.id.cmp(&b.instance.id)); + let mut candidates = Vec::new(); + let mut archives = HashMap::new(); + for metadata in instances { + if super::super::sync_files_are_protected(&metadata) { continue; } + let Some(snapshot_id) = capture_observation(&metadata, state).await else { continue; }; + let snapshot = sources::load_snapshot(state, &snapshot_id).await?; + for source in std::iter::once(&snapshot.game_jar).chain(&snapshot.mods) { + if let Err(error) = cached_archive(state, source, &mut archives).await { + tracing::debug!(hash = source.hash, %error, "Could not index locale archive"); + } + } + if let Some(index) = &snapshot.asset_index { + let _ = sources::language_assets(state, index).await; + } + let Ok((document, _)) = read_document(&options_path(&metadata, state)).await else { continue; }; + let keys = document_keys(&document); + let mut tx = state.pool.begin().await?; + for (id, key) in &keys { + let observation = Observation { snapshot: snapshot_id.clone(), raw_key: key.clone() }; + storage::observe(&mut tx, &metadata.instance.id, id, &metadata.instance.id, + &snapshot.game_version, &observation).await?; + } + tx.commit().await?; + candidates.push(Candidate { snapshot_id, snapshot, keys }); + } + for row in storage::load(&state.pool, None).await? { + if row.origin.is_some() { continue; } + let mut observations = Vec::new(); + if let Some(observation) = &row.observation { + observations.push(observation.clone()); + } else { + if row.backfilled + && let Some(version) = &row.source_game_version + && let Some(definition) = super::catalog::setting_by_id(&row.option_id) + && let Ok(Some(snapshot)) = sources::historical_snapshot( + state, row.source_instance_id.as_deref().unwrap_or(""), version, + ).await { + observations.extend(definition.keys.iter().map(|key| Observation { + snapshot: snapshot.clone(), raw_key: (*key).to_owned(), + })); + } + let mut matching: Vec<_> = candidates.iter().filter(|c| { + (row.scope.is_empty() || row.scope == c.snapshot.instance_id) + && c.keys.contains_key(&row.option_id) + && (row.backfilled || row.source_instance_id.as_ref().is_none_or(|id| *id == c.snapshot.instance_id) + && row.source_game_version.as_ref().is_none_or(|v| *v == c.snapshot.game_version)) + }).collect(); + matching.sort_by_key(|c| ( + row.source_instance_id.as_deref() != Some(c.snapshot.instance_id.as_str()), + row.source_game_version.as_deref() != Some(c.snapshot.game_version.as_str()), + c.snapshot.instance_id.clone(), + )); + observations.extend(matching.into_iter().map(|c| Observation { + snapshot: c.snapshot_id.clone(), raw_key: c.keys[&row.option_id].clone(), + })); + } + for observation in observations { + match resolve_origin(state, &row.option_id, &observation, &mut archives).await { + Ok(Some(origin)) => { storage::pin(&state.pool, &row, &origin).await?; break; } + Ok(None) => {} + Err(error) => tracing::debug!(%error, option_id = row.option_id, "Translation origin remains unresolved"), + } + } + } + Ok(()) +} + +async fn resolve_origin( + state: &State, option_id: &str, observation: &Observation, + archives: &mut HashMap>, +) -> crate::Result> { + let snapshot = sources::load_snapshot(state, &observation.snapshot).await?; + let vanilla = !option_id.starts_with("external:"); + let keys = catalog::translation_keys(&observation.raw_key, vanilla); + let sources: Vec<_> = if vanilla { vec![&snapshot.game_jar] } + else { snapshot.mods.iter().chain(std::iter::once(&snapshot.game_jar)).collect() }; + for source in sources { + let Ok(index) = cached_archive(state, source, archives).await else { continue; }; + for bundle in &index.bundles { + let Some(english) = bundle.locales.get("en_us") else { continue; }; + let mut english = english.clone(); + bundle.deprecated.apply(&mut english); + for key in &keys { + if !english.get(key).is_some_and(|s| plain_label(s).is_some()) { continue; } + let asset_index = if source.hash == snapshot.game_jar.hash { snapshot.asset_index.clone() } else { None }; + if let Some(index) = &asset_index { + sources::language_assets(state, index).await?; + } + return Ok(Some(Origin { + instance_id: snapshot.instance_id.clone(), game_version: snapshot.game_version.clone(), + game_jar_hash: snapshot.game_jar.hash.clone(), asset_index, + archive: source.clone(), nested_path: bundle.nested_path.clone(), translation_key: key.clone(), + choices: catalog::choice_keys(option_id).iter().filter(|(_, key)| english.contains_key(*key)) + .map(|(value, key)| ((*value).to_owned(), (*key).to_owned())).collect(), + })); + } + } + } + Ok(None) +} + +fn plain_label(value: &str) -> Option { + if value.trim().is_empty() || value.contains('%') || value.contains('§') { return None; } + Some(value.to_owned()) +} + +fn minecraft_locale(locale: &str) -> String { + match locale { + "es-419" => "es_mx".to_owned(), + "fil-PH" => "tl_ph".to_owned(), + "ms-MY" => "ms_my".to_owned(), + "zh-Hans" => "zh_cn".to_owned(), + "zh-Hant" => "zh_tw".to_owned(), + _ => locale.replace('-', "_").to_ascii_lowercase(), + } +} + +/// Loads translations for the settings shown in the modal. +pub async fn get_game_setting_locale_labels( + instance_id: Option<&str>, locale: &str, option_ids: Vec, refresh_sources: bool, +) -> crate::Result { + if option_ids.len() > 16_384 { return Err(input_error("Too many requested game-setting labels")); } + let locale = minecraft_locale(locale); + if !archive::valid_locale(&locale) { return Err(input_error("Invalid Minecraft locale")); } + let state = State::get().await?; + if refresh_sources { queue_game_locale_index(); } + let rows = storage::load(&state.pool, Some(instance_id.unwrap_or(""))).await?; + let requested: std::collections::HashSet<_> = option_ids.into_iter().collect(); + let mut result = GameSettingLocaleLabels::default(); + let mut archives = HashMap::new(); + let mut assets: HashMap = HashMap::new(); + let mut dictionaries: HashMap = HashMap::new(); + for row in rows { + if !requested.contains(&row.option_id) { continue; } + let Some(mut origin) = row.origin else { continue; }; + for (value, key) in catalog::choice_keys(&row.option_id) { + origin.choices.entry((*value).to_owned()).or_insert_with(|| (*key).to_owned()); + } + let dictionary_id = format!("{}:{}:{}", origin.archive.hash, origin.nested_path, + origin.asset_index.as_ref().map(|a| a.hash.as_str()).unwrap_or("")); + if !dictionaries.contains_key(&dictionary_id) { + let Ok(index) = cached_archive(&state, &origin.archive, &mut archives).await else { continue; }; + let Some(bundle) = index.bundles.iter().find(|b| b.nested_path == origin.nested_path) else { continue; }; + let mut translations = bundle.locales.get("en_us").cloned().unwrap_or_default(); + if let Some(selected) = bundle.locales.get(&locale) { translations.extend(selected.clone()); } + if locale != "en_us" && let Some(index) = &origin.asset_index + && let Ok(available) = sources::language_assets(&state, index).await { + for name in [format!("{locale}.json"), format!("{locale}.lang")] { + if let Some(asset) = available.get(&name) { + if !assets.contains_key(&asset.hash) { + let selected = sources::asset_language(&state, asset, name.ends_with(".lang")).await.unwrap_or_default(); + assets.insert(asset.hash.clone(), selected); + } + translations.extend(assets[&asset.hash].clone()); + break; + } + } + } + bundle.deprecated.apply(&mut translations); + dictionaries.insert(dictionary_id.clone(), translations); + } + let translations = &dictionaries[&dictionary_id]; + let label = translations.get(&origin.translation_key).and_then(|s| plain_label(s)); + if let Some(label) = label { + let choices = origin.choices.iter().filter_map(|(value, key)| { + translations.get(key).and_then(|s| plain_label(s)).map(|label| (value.clone(), label)) + }).collect(); + result.settings.insert(row.option_id, GameSettingLocaleLabel { label, choices }); + } + } + Ok(result) +} diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/sources.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/sources.rs new file mode 100644 index 0000000000..df35233277 --- /dev/null +++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/sources.rs @@ -0,0 +1,196 @@ +use super::super::options_file::{input_error, sha1_bytes}; +use super::archive::{self, ArchiveIndex, Translations}; +use crate::state::{CachedEntry, InstanceMetadata, State}; +use crate::util::{fetch, io}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; + +#[derive(Clone, Serialize, Deserialize)] +pub(super) struct ArchiveSource { + pub hash: String, + pub path: PathBuf, +} + +#[derive(Clone, Serialize, Deserialize)] +pub(super) struct AssetIndexSource { + pub hash: String, + pub id: String, +} + +#[derive(Clone, Serialize, Deserialize)] +pub(super) struct Snapshot { + pub instance_id: String, + pub game_version: String, + pub game_jar: ArchiveSource, + pub asset_index: Option, + pub mods: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub(super) struct Asset { + pub hash: String, + pub size: usize, +} + +pub(super) fn root(state: &State) -> PathBuf { + state.directories.metadata_dir().join("game-locales").join("v1") +} + +pub(super) async fn write_json(path: &std::path::Path, data: &T) -> crate::Result<()> { + let parent = path.parent().ok_or_else(|| input_error("Missing locale cache directory"))?; + io::create_dir_all(parent).await?; + io::write(path, serde_json::to_vec(data)?).await?; + Ok(()) +} + +pub(super) async fn snapshot_instance(metadata: &InstanceMetadata, state: &State) -> crate::Result { + let version = &metadata.applied_content_set.game_version; + let version_id = metadata.applied_content_set.loader_version.as_ref() + .map(|loader| format!("{version}-{loader}")).unwrap_or_else(|| version.clone()); + let mut found = None; + for id in [&version_id, version] { + let directory = state.directories.version_dir(id); + let path = directory.join(format!("{id}.json")); + let Ok(bytes) = io::read(&path).await else { continue; }; + let value: serde_json::Value = serde_json::from_slice(&bytes)?; + let Some(hash) = value.pointer("/downloads/client/sha1").and_then(|v| v.as_str()) else { continue; }; + if !archive::valid_hash(hash) { continue; } + let jar_id = value.get("id").and_then(|v| v.as_str()).unwrap_or(id); + let jar = state.directories.version_dir(jar_id).join(format!("{jar_id}.jar")); + let asset_index = value.get("assetIndex").and_then(|v| Some(AssetIndexSource { + hash: v.get("sha1")?.as_str()?.to_owned(), + id: v.get("id")?.as_str()?.to_owned(), + })).filter(|a| archive::valid_hash(&a.hash) && !a.id.contains(['/', '\\'])); + found = Some((ArchiveSource { hash: hash.to_owned(), path: jar }, asset_index)); + break; + } + let (game_jar, asset_index) = found.ok_or_else(|| input_error("Minecraft locale source is not installed yet"))?; + let instance_dir = state.directories.instances_dir().join(&metadata.instance.path); + let scanned = crate::state::instances::adapters::filesystem::scan_content_files( + &state.directories.instances_dir(), &metadata.instance.path, + )?; + let scanned: Vec<_> = scanned.into_iter().filter(|f| f.enabled && f.relative_path.starts_with("mods/")).collect(); + let keys: Vec<_> = scanned.iter().map(|f| f.hash_cache_key.as_str()).collect(); + let hashes = CachedEntry::get_file_hash_many(&keys, None, &state.pool, &state.api_semaphore).await?; + let by_path: BTreeMap<_, _> = hashes.into_iter().map(|h| (h.path, h.hash)).collect(); + let mut mods = Vec::new(); + for file in scanned { + let path = instance_dir.join(&file.relative_path); + let key = format!("{}/{}", metadata.instance.path, file.relative_path); + let hash = match by_path.get(&key) { + Some(hash) => hash.clone(), + None => fetch::sha1_file_async(&path).await?.1, + }; + mods.push(ArchiveSource { hash, path }); + } + mods.sort_by(|a, b| a.path.cmp(&b.path)); + let snapshot = Snapshot { instance_id: metadata.instance.id.clone(), game_version: version.clone(), game_jar, asset_index, mods }; + let bytes = serde_json::to_vec(&snapshot)?; + let id = sha1_bytes(&bytes); + let path = root(state).join("snapshots").join(format!("{id}.json")); + if !path.exists() { write_json(&path, &snapshot).await?; } + Ok(id) +} + +pub(super) async fn load_snapshot(state: &State, id: &str) -> crate::Result { + if !archive::valid_hash(id) { return Err(input_error("Invalid locale snapshot hash")); } + let bytes = io::read(root(state).join("snapshots").join(format!("{id}.json"))).await?; + archive::checked_bytes(bytes.clone(), id)?; + Ok(serde_json::from_slice(&bytes)?) +} + +/// Old synced values retain a Minecraft version but no archive hashes. Prefer +/// that version's installed resources before considering another instance. +pub(super) async fn historical_snapshot( + state: &State, instance_id: &str, version: &str, +) -> crate::Result> { + let mut directories = tokio::fs::read_dir(state.directories.versions_dir()) + .await.map_err(io::IOError::from)?; + let mut ids = Vec::new(); + while let Some(entry) = directories.next_entry().await.map_err(io::IOError::from)? { + let id = entry.file_name().to_string_lossy().into_owned(); + if id == version || id.starts_with(&format!("{version}-")) { ids.push(id); } + } + ids.sort(); + for id in ids { + let directory = state.directories.version_dir(&id); + let Ok(bytes) = io::read(directory.join(format!("{id}.json"))).await else { continue; }; + let Ok(value) = serde_json::from_slice::(&bytes) else { continue; }; + let Some(hash) = value.pointer("/downloads/client/sha1").and_then(|v| v.as_str()) else { continue; }; + if !archive::valid_hash(hash) { continue; } + let jar_id = value.get("id").and_then(|v| v.as_str()).unwrap_or(&id); + let asset_index = value.get("assetIndex").and_then(|v| Some(AssetIndexSource { + hash: v.get("sha1")?.as_str()?.to_owned(), id: v.get("id")?.as_str()?.to_owned(), + })).filter(|a| archive::valid_hash(&a.hash) && !a.id.contains(['/', '\\'])); + let snapshot = Snapshot { + instance_id: instance_id.to_owned(), game_version: version.to_owned(), + game_jar: ArchiveSource { hash: hash.to_owned(), path: state.directories.version_dir(jar_id).join(format!("{jar_id}.jar")) }, + asset_index, mods: Vec::new(), + }; + if archive_index(state, &snapshot.game_jar).await.is_err() { continue; } + let id = sha1_bytes(&serde_json::to_vec(&snapshot)?); + write_json(&root(state).join("snapshots").join(format!("{id}.json")), &snapshot).await?; + return Ok(Some(id)); + } + Ok(None) +} + +pub(super) async fn archive_index(state: &State, source: &ArchiveSource) -> crate::Result { + if !archive::valid_hash(&source.hash) { return Err(input_error("Invalid locale archive hash")); } + let path = root(state).join("archives").join(format!("{}.json", source.hash)); + if let Ok(bytes) = io::read(&path).await + && let Ok(index) = serde_json::from_slice::(&bytes) + && (index.version >= 2 || !source.path.exists()) { return Ok(index); } + let source = source.clone(); + let index = tokio::task::spawn_blocking(move || archive::inspect(&source.path, &source.hash)) + .await.map_err(|e| input_error(e.to_string()))??; + write_json(&path, &index).await?; + Ok(index) +} + +pub(super) async fn language_assets(state: &State, source: &AssetIndexSource) -> crate::Result> { + if !archive::valid_hash(&source.hash) { return Err(input_error("Invalid locale asset index hash")); } + let path = root(state).join("indexes").join(format!("{}.json", source.hash)); + if let Ok(bytes) = io::read(&path).await { return Ok(serde_json::from_slice(&bytes)?); } + let bytes = io::read(state.directories.assets_index_dir().join(format!("{}.json", source.id))).await?; + let bytes = archive::checked_bytes(bytes, &source.hash)?; + let value: serde_json::Value = serde_json::from_slice(&bytes)?; + let mut assets = BTreeMap::new(); + for (name, asset) in value.get("objects").and_then(|v| v.as_object()).into_iter().flatten() { + let Some(name) = name.strip_prefix("minecraft/lang/") else { continue; }; + let Some(locale) = name.strip_suffix(".json").or_else(|| name.strip_suffix(".lang")) else { continue; }; + let asset: Asset = serde_json::from_value(asset.clone())?; + if archive::valid_locale(locale) && archive::valid_hash(&asset.hash) && asset.size <= archive::MAX_LANGUAGE_BYTES { + assets.insert(name.to_ascii_lowercase(), asset); + } + } + write_json(&path, &assets).await?; + Ok(assets) +} + +pub(super) async fn asset_language(state: &State, asset: &Asset, legacy: bool) -> crate::Result { + if !archive::valid_hash(&asset.hash) || asset.size > archive::MAX_LANGUAGE_BYTES { + return Err(input_error("Invalid Minecraft locale asset")); + } + let path = root(state).join("assets").join(&asset.hash); + let bytes = match io::read(&path).await { + Ok(bytes) => bytes, + Err(_) => { + let bytes = match io::read(state.directories.object_dir(&asset.hash)).await { + Ok(bytes) => bytes, + Err(_) => fetch::fetch( + &format!("https://resources.download.minecraft.net/{}/{}", &asset.hash[..2], asset.hash), + Some(&asset.hash), None, None, &state.fetch_semaphore, &state.pool, + ).await?.to_vec(), + }; + if bytes.len() != asset.size { return Err(input_error("Minecraft locale asset has unexpected size")); } + archive::checked_bytes(bytes.clone(), &asset.hash)?; + io::create_dir_all(path.parent().unwrap()).await?; + io::write(&path, &bytes).await?; + bytes + } + }; + archive::checked_bytes(bytes.clone(), &asset.hash)?; + archive::parse_language(&bytes, legacy) +} diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/storage.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/storage.rs new file mode 100644 index 0000000000..0c27b7b853 --- /dev/null +++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/storage.rs @@ -0,0 +1,63 @@ +use super::{Observation, Origin}; +use sqlx::{Sqlite, SqlitePool, Transaction}; + +pub(super) struct OriginRow { + pub scope: String, + pub option_id: String, + pub source_instance_id: Option, + pub source_game_version: Option, + pub backfilled: bool, + pub observation: Option, + pub origin: Option, +} + +pub(super) async fn load(pool: &SqlitePool, scope: Option<&str>) -> crate::Result> { + let rows = sqlx::query!( + r#"SELECT scope, option_id, source_instance_id, source_game_version, + backfilled AS "backfilled!: bool", observation_json, origin_json + FROM game_option_locale_origins + WHERE (? IS NULL OR scope = ?) + ORDER BY scope, option_id"#, + scope, scope, + ).fetch_all(pool).await?; + rows.into_iter().map(|row| Ok(OriginRow { + scope: row.scope, + option_id: row.option_id, + source_instance_id: row.source_instance_id, + source_game_version: row.source_game_version, + backfilled: row.backfilled, + observation: row.observation_json.as_deref().map(serde_json::from_str).transpose()?, + origin: row.origin_json.as_deref().map(serde_json::from_str).transpose()?, + })).collect() +} + +pub(super) async fn observe( + tx: &mut Transaction<'_, Sqlite>, scope: &str, option_id: &str, + instance_id: &str, game_version: &str, observation: &Observation, +) -> crate::Result<()> { + let json = serde_json::to_string(observation)?; + sqlx::query!( + "INSERT INTO game_option_locale_origins + (scope, option_id, source_instance_id, source_game_version, observation_json) + SELECT ?, ?, ?, ?, ? + WHERE ? <> '' OR EXISTS (SELECT 1 FROM synced_game_option_values WHERE option_id = ?) + ON CONFLICT(scope, option_id) DO UPDATE SET observation_json = excluded.observation_json + WHERE game_option_locale_origins.observation_json IS NULL + AND game_option_locale_origins.origin_json IS NULL + AND game_option_locale_origins.backfilled = 0 + AND game_option_locale_origins.source_instance_id = excluded.source_instance_id + AND game_option_locale_origins.source_game_version = excluded.source_game_version", + scope, option_id, instance_id, game_version, json, scope, option_id, + ).execute(&mut **tx).await?; + Ok(()) +} + +pub(super) async fn pin(pool: &SqlitePool, row: &OriginRow, origin: &Origin) -> crate::Result<()> { + let json = serde_json::to_string(origin)?; + sqlx::query!( + "UPDATE game_option_locale_origins SET origin_json = ? + WHERE scope = ? AND option_id = ? AND origin_json IS NULL", + json, row.scope, row.option_id, + ).execute(pool).await?; + Ok(()) +} diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/mod.rs b/packages/app-lib/src/api/instance/synced_options/game_options/mod.rs index 8a22e9163a..03eca488e2 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/mod.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/mod.rs @@ -25,6 +25,7 @@ mod fullscreen; mod instance_support; mod launch_overrides; mod local_settings_editor; +mod locales; mod options_file; mod pack_updates; mod read_instance_changes; @@ -51,6 +52,8 @@ pub use local_settings_editor::{ }; pub use pack_updates::{GameOptionsPackSource, capture_pack_base}; pub use settings_editor::{get_config, preview_changes, save_changes}; +pub use locales::{GameSettingLocaleLabels, get_game_setting_locale_labels}; +pub(crate) use locales::{GameLocaleIndexer, queue_game_locale_index, start_game_locale_indexer}; pub use source_selection::list_sync_sources; pub(crate) use fullscreen::{ diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/read_instance_changes.rs b/packages/app-lib/src/api/instance/synced_options/game_options/read_instance_changes.rs index b4022f44b2..d71afb8943 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/read_instance_changes.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/read_instance_changes.rs @@ -180,6 +180,12 @@ pub(super) async fn discover_custom_settings( let entries = document.effective_entries(); let now = Utc::now().timestamp(); let mut discovered = false; + let locale_snapshot = if document.effective_entries().keys().any(|key| { + let id = setting_by_file_key(key).map(|s| s.id.to_owned()).unwrap_or_else(|| custom_setting_id(key)); + !existing.contains_key(&id) + }) { + super::locales::capture_observation(metadata, state).await + } else { None }; let mut tx = state.pool.begin().await?; let game_version = metadata.applied_content_set.game_version.as_str(); @@ -358,6 +364,8 @@ pub(super) async fn discover_custom_settings( .execute(&mut *tx) .await?; } + super::locales::record_observations(&mut tx, metadata, document, locale_snapshot.as_deref()).await; tx.commit().await?; + if discovered { super::locales::queue_game_locale_index(); } Ok(discovered) } diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs b/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs index 05a7e53f63..3bc9541107 100644 --- a/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs +++ b/packages/app-lib/src/api/instance/synced_options/game_options/source_selection.rs @@ -37,6 +37,7 @@ pub(in crate::api::instance) async fn initialize_from_source_instance( let now = Utc::now().timestamp(); let source_version = metadata.applied_content_set.game_version.as_str(); let source_id = metadata.instance.id.as_str(); + let locale_snapshot = super::locales::capture_observation(metadata, state).await; let mut tx = state.pool.begin().await?; let catalog_revision = CATALOG_REVISION as i64; @@ -298,7 +299,9 @@ pub(in crate::api::instance) async fn initialize_from_source_instance( update_app_fullscreen_setting(&mut tx, value, fullscreen_sync_enabled) .await?; } + super::locales::record_observations(&mut tx, metadata, &document, locale_snapshot.as_deref()).await; tx.commit().await?; + super::locales::queue_game_locale_index(); Ok(()) } diff --git a/packages/app-lib/src/api/instance/synced_packs/operations.rs b/packages/app-lib/src/api/instance/synced_packs/operations.rs index 51d1bac671..500ce8f9d2 100644 --- a/packages/app-lib/src/api/instance/synced_packs/operations.rs +++ b/packages/app-lib/src/api/instance/synced_packs/operations.rs @@ -293,7 +293,17 @@ pub(in crate::api::instance) async fn seed_from_instance( { continue; } - let candidate = pack_from_item(item.clone(), metadata, state).await?; + let candidate = match pack_from_item(item.clone(), metadata, state).await { + Ok(candidate) => candidate, + Err(error) if matches!(error.raw.as_ref(), crate::ErrorKind::JSONError(_)) => { + tracing::warn!( + "Skipping pack {} from instance {instance_id} while initializing pack sync because its JSON metadata could not be parsed: {error}", + item.file_path + ); + continue; + } + Err(error) => return Err(error), + }; candidates.push((item, candidate)); } diff --git a/packages/app-lib/src/install/store.rs b/packages/app-lib/src/install/store.rs index 671c4f6eff..ed354457fd 100644 --- a/packages/app-lib/src/install/store.rs +++ b/packages/app-lib/src/install/store.rs @@ -427,6 +427,7 @@ pub async fn complete_success( } transaction.commit().await?; + crate::api::instance::queue_game_locale_index(); get_required(id, app_state).await.map(Some) } diff --git a/packages/app-lib/src/state/instances/commands/sync_content_files.rs b/packages/app-lib/src/state/instances/commands/sync_content_files.rs index a3b6ae9154..93a6349f35 100644 --- a/packages/app-lib/src/state/instances/commands/sync_content_files.rs +++ b/packages/app-lib/src/state/instances/commands/sync_content_files.rs @@ -146,6 +146,7 @@ pub(crate) async fn sync_instance_content_files( if content_changed { super::mark_shared_instance_stale(&instance.id, &state.pool).await?; + crate::api::instance::queue_game_locale_index(); } Ok(stored_files) diff --git a/packages/app-lib/src/state/instances/watcher.rs b/packages/app-lib/src/state/instances/watcher.rs index 578d7aa605..6e2a312d5f 100644 --- a/packages/app-lib/src/state/instances/watcher.rs +++ b/packages/app-lib/src/state/instances/watcher.rs @@ -99,6 +99,9 @@ pub async fn init_watcher() -> crate::Result { .entry(instance_id.clone()) .or_default() .insert(file_name.to_owned()); + if file_name == "options.txt" { + crate::api::instance::queue_game_locale_index(); + } } if first_file_name .as_ref() diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index 65c3c7b37f..fa4f4962c2 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -89,6 +89,7 @@ pub struct State { shared_instance_locks: DashMap>>, /// Serializes canonical synced-option mutations and checkpoint updates. synced_options_lock: Mutex<()>, + pub(crate) game_locale_indexer: crate::api::instance::GameLocaleIndexer, /// Discord RPC pub discord_rpc: DiscordGuard, @@ -174,6 +175,7 @@ impl State { } tokio::task::spawn(async move { + crate::api::instance::start_game_locale_indexer(Arc::clone(state)); instances::watcher::watch_instances_init( &state.file_watcher, &state.directories, @@ -301,6 +303,7 @@ impl State { instance_screenshot_locks: DashMap::new(), shared_instance_locks: DashMap::new(), synced_options_lock: Mutex::new(()), + game_locale_indexer: Default::default(), discord_rpc, process_manager, friends_socket, diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index 5cabd75085..c28fc43f79 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -182,6 +182,7 @@ import _LeftArrowIcon from './icons/left-arrow.svg?component' import _LibraryIcon from './icons/library.svg?component' import _LightBulbIcon from './icons/light-bulb.svg?component' import _LinkIcon from './icons/link.svg?component' +import _Link2Icon from './icons/link-2.svg?component' import _ListIcon from './icons/list.svg?component' import _ListBulletedIcon from './icons/list-bulleted.svg?component' import _ListEndIcon from './icons/list-end.svg?component' @@ -642,6 +643,7 @@ export const LeftArrowIcon = _LeftArrowIcon export const LibraryIcon = _LibraryIcon export const LightBulbIcon = _LightBulbIcon export const LinkIcon = _LinkIcon +export const Link2Icon = _Link2Icon export const ListIcon = _ListIcon export const ListBulletedIcon = _ListBulletedIcon export const ListEndIcon = _ListEndIcon diff --git a/packages/assets/icons/link-2.svg b/packages/assets/icons/link-2.svg new file mode 100644 index 0000000000..215f05377f --- /dev/null +++ b/packages/assets/icons/link-2.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/packages/ui/src/components/base/Slider.vue b/packages/ui/src/components/base/Slider.vue index 6cefe674fc..650a1a4e19 100644 --- a/packages/ui/src/components/base/Slider.vue +++ b/packages/ui/src/components/base/Slider.vue @@ -2,9 +2,9 @@
- {{ min }} + {{ minLabel ?? min }}
- {{ formatValue(max) }} + {{ maxLabel ?? formatValue(max) }}
@@ -90,6 +90,8 @@ interface Props { snapRange?: number disabled?: boolean unit?: string + minLabel?: string + maxLabel?: string placeholder?: string ariaLabel?: string } @@ -117,6 +119,11 @@ const heightClass = computed( })[props.size], ) const currentValue = ref(props.modelValue === null ? null : normalizeValue(props.modelValue)) +const inputWidth = computed(() => { + const digits = Math.max(String(props.min).length, String(props.max).length) + const padding = props.size === 'small' || props.size === 'standard' ? 1.5 : 2 + return `max(65px, calc(${digits}ch + ${padding}rem + 2px))` +}) const currentPercentage = computed(() => getPercentage(currentValue.value ?? props.min)) const visibleSnapPoints = computed(() => props.snapPoints.filter((snapPoint) => snapPoint >= props.min && snapPoint <= props.max), @@ -169,8 +176,10 @@ function onInputWithSnap(value: string) { inputValueValid(parsedValue) } -function onInput(value: string) { - inputValueValid(Number.parseFloat(value)) +function onInput(event: Event) { + const target = event.target as HTMLInputElement + inputValueValid(target.valueAsNumber) + target.value = currentValue.value === null ? '' : String(currentValue.value) } diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue index 3f0d53b01c..b11bf1a5fd 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue @@ -2,7 +2,7 @@ import { ArrowLeftRightIcon, DownloadIcon, - LinkIcon, + Link2Icon, LockIcon, MoreVerticalIcon, SpinnerIcon, @@ -241,10 +241,10 @@ const installTooltip = computed(() => { v-tooltip="syncStatusLabel" :aria-label="syncStatusLabel" role="img" - class="inline-flex size-5 shrink-0 cursor-help items-center justify-center" + class="inline-flex shrink-0 cursor-help items-center justify-center rounded-full border border-solid border-brand-blue bg-highlight-blue px-2.5 py-1 text-brand-blue" tabindex="0" > -