719 lines
20 KiB
TypeScript
719 lines
20 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { useSnapshot } from 'valtio'
|
|
import { openURL } from 'renderer/viewer/lib/simpleUtils'
|
|
import { noCase } from 'change-case'
|
|
import { versionToNumber } from 'mc-assets/dist/utils'
|
|
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
|
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
|
|
import Button from './react/Button'
|
|
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
|
import Slider from './react/Slider'
|
|
import { getScreenRefreshRate } from './utils'
|
|
import { setLoadingScreenStatus } from './appStatus'
|
|
import { openFilePicker, resetLocalStorage } from './browserfs'
|
|
import { completeResourcepackPackInstall, getResourcePackNames, resourcepackReload, resourcePackState, uninstallResourcePack } from './resourcePack'
|
|
import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy'
|
|
import { showInputsModal, showOptionsModal } from './react/SelectOption'
|
|
import { ClientMod, getAllMods, modsUpdateStatus } from './clientMods'
|
|
import supportedVersions from './supportedVersions.mjs'
|
|
import { getVersionAutoSelect } from './connect'
|
|
import { createNotificationProgressReporter } from './core/progressReporter'
|
|
import { customKeymaps } from './controls'
|
|
import { appStorage } from './react/appStorageProvider'
|
|
import { exportData, importData } from './core/importExport'
|
|
|
|
export const guiOptionsScheme: {
|
|
[t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom? }>
|
|
} = {
|
|
render: [
|
|
{
|
|
custom () {
|
|
const frameLimitValue = useSnapshot(options).frameLimit
|
|
const [frameLimitMax, setFrameLimitMax] = useState(null as number | null)
|
|
|
|
return <div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<Slider
|
|
style={{ width: 130 }}
|
|
label='Frame Limit'
|
|
disabledReason={frameLimitMax ? undefined : 'press lock button first'}
|
|
unit={frameLimitValue ? 'fps' : ''}
|
|
valueDisplay={frameLimitValue || 'VSync'}
|
|
value={frameLimitValue || frameLimitMax! + 1} min={20}
|
|
max={frameLimitMax! + 1} updateValue={(newVal) => {
|
|
options.frameLimit = newVal > frameLimitMax! ? false : newVal
|
|
}}
|
|
/>
|
|
<Button
|
|
style={{ width: 20 }} icon='pixelarticons:lock-open' onClick={async () => {
|
|
const rate = await getScreenRefreshRate()
|
|
setFrameLimitMax(rate)
|
|
}}
|
|
/>
|
|
</div>
|
|
}
|
|
},
|
|
{
|
|
gpuPreference: {
|
|
text: 'GPU Preference',
|
|
tooltip: 'You will need to reload the page for this to take effect.',
|
|
values: [['default', 'Auto'], ['high-performance', 'Dedicated'], ['low-power', 'Low Power']]
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Guide: Disable VSync' onClick={() => openURL('https://gist.github.com/zardoy/6e5ce377d2b4c1e322e660973da069cd')} inScreen />
|
|
},
|
|
backgroundRendering: {
|
|
text: 'Background FPS limit',
|
|
values: [
|
|
['full', 'NO'],
|
|
['5fps', '5 FPS'],
|
|
['20fps', '20 FPS'],
|
|
],
|
|
},
|
|
activeRenderer: {
|
|
text: 'Renderer',
|
|
values: [
|
|
['threejs', 'Three.js (stable)'],
|
|
],
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Experimental</Category>
|
|
},
|
|
experimentalLightingV1: {
|
|
text: 'Experimental Lighting',
|
|
tooltip: 'Once stable this setting will be removed and always enabled',
|
|
},
|
|
smoothLighting: {},
|
|
lightingStrategy: {
|
|
values: [
|
|
['prefer-server', 'Prefer Server'],
|
|
['always-client', 'Always Client'],
|
|
['always-server', 'Always Server'],
|
|
],
|
|
},
|
|
lowMemoryMode: {
|
|
text: 'Low Memory Mode',
|
|
enableWarning: 'Enabling it will make chunks load ~4x slower. When in the game, app needs to be reloaded to apply this setting.',
|
|
},
|
|
starfieldRendering: {},
|
|
keepChunksDistance: {
|
|
max: 5,
|
|
unit: '',
|
|
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
|
|
},
|
|
renderEars: {
|
|
tooltip: 'Enable rendering Deadmau5 ears for all players if their skin contains textures for it',
|
|
},
|
|
renderDebug: {
|
|
values: [
|
|
'advanced',
|
|
'basic',
|
|
'none'
|
|
],
|
|
},
|
|
rendererPerfDebugOverlay: {
|
|
text: 'Performance Debug',
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions
|
|
return <Button
|
|
inScreen
|
|
label={`Batch Chunks Display ${_renderByChunks ? 'ON' : 'OFF'}`}
|
|
onClick={() => {
|
|
options.rendererSharedOptions._renderByChunks = !_renderByChunks
|
|
}}
|
|
/>
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Resource Packs</Category>
|
|
},
|
|
serverResourcePacks: {
|
|
text: 'Download From Server',
|
|
values: [
|
|
'prompt',
|
|
'always',
|
|
'never'
|
|
],
|
|
}
|
|
}
|
|
],
|
|
main: [
|
|
{
|
|
fov: {
|
|
min: 30,
|
|
max: 110,
|
|
unit: '',
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
const sp = miscUiState.singleplayer || !miscUiState.gameLoaded
|
|
const id = sp ? 'renderDistance' : 'multiplayerRenderDistance' // cant be changed when settings are open
|
|
return <OptionSlider item={{
|
|
type: 'slider',
|
|
id,
|
|
text: 'Render Distance',
|
|
unit: '',
|
|
max: sp ? 16 : 12,
|
|
min: 1
|
|
}}
|
|
/>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Render...' onClick={() => openOptionsMenu('render')} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Interface...' onClick={() => openOptionsMenu('interface')} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Controls...' onClick={() => openOptionsMenu('controls')} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Sound...' onClick={() => openOptionsMenu('sound')} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { resourcePackInstalled } = useSnapshot(resourcePackState)
|
|
const { usingServerResourcePack } = useSnapshot(gameAdditionalState)
|
|
const { enabledResourcepack } = useSnapshot(options)
|
|
return <Button
|
|
label={`Resource Pack: ${usingServerResourcePack ? 'SERVER ON' : resourcePackInstalled ? enabledResourcepack ? 'ON' : 'OFF' : 'NO'}`} inScreen onClick={async () => {
|
|
if (resourcePackState.resourcePackInstalled) {
|
|
const names = Object.keys(await getResourcePackNames())
|
|
const name = names[0]
|
|
const choices = [
|
|
options.enabledResourcepack ? 'Disable' : 'Enable',
|
|
'Uninstall',
|
|
]
|
|
const choice = await showOptionsModal(`Resource Pack ${name} action`, choices)
|
|
if (!choice) return
|
|
if (choice === 'Disable') {
|
|
options.enabledResourcepack = null
|
|
await resourcepackReload()
|
|
return
|
|
}
|
|
if (choice === 'Enable') {
|
|
options.enabledResourcepack = name
|
|
await completeResourcepackPackInstall(name, name, false, createNotificationProgressReporter())
|
|
return
|
|
}
|
|
if (choice === 'Uninstall') {
|
|
// todo make hidable
|
|
setLoadingScreenStatus('Uninstalling texturepack')
|
|
await uninstallResourcePack()
|
|
setLoadingScreenStatus(undefined)
|
|
}
|
|
} else {
|
|
// if (!fsState.inMemorySave && isGameActive(false)) {
|
|
// alert('Unable to install resource pack in loaded save for now')
|
|
// return
|
|
// }
|
|
openFilePicker('resourcepack')
|
|
}
|
|
}}
|
|
/>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Advanced...' onClick={() => openOptionsMenu('advanced')} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { appConfig } = useSnapshot(miscUiState)
|
|
const modsUpdateSnapshot = useSnapshot(modsUpdateStatus)
|
|
const [clientMods, setClientMods] = useState<ClientMod[]>([])
|
|
useEffect(() => {
|
|
void getAllMods().then(setClientMods)
|
|
}, [])
|
|
|
|
if (appConfig?.showModsButton === false) return null
|
|
const enabledModsCount = Object.keys(clientMods.filter(mod => mod.enabled)).length
|
|
return <Button label={`Client Mods: ${enabledModsCount} (${Object.keys(modsUpdateSnapshot).length})`} onClick={() => showModal({ reactType: 'mods' })} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='VR...' onClick={() => openOptionsMenu('VR')} inScreen />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { appConfig } = useSnapshot(miscUiState)
|
|
if (!appConfig?.displayLanguageSelector) return null
|
|
return <Button
|
|
label='Language...' onClick={async () => {
|
|
const newLang = await showOptionsModal('Set Language', (appConfig.supportedLanguages ?? []) as string[])
|
|
if (!newLang) return
|
|
options.language = newLang.split(' - ')[0]
|
|
}} inScreen />
|
|
},
|
|
}
|
|
],
|
|
interface: [
|
|
{
|
|
guiScale: {
|
|
max: 4,
|
|
min: 1,
|
|
unit: '',
|
|
delayApply: true,
|
|
},
|
|
custom () {
|
|
return <Category>Chat</Category>
|
|
},
|
|
chatWidth: {
|
|
max: 320,
|
|
unit: 'px',
|
|
},
|
|
chatHeight: {
|
|
max: 180,
|
|
unit: 'px',
|
|
},
|
|
chatOpacity: {
|
|
},
|
|
chatOpacityOpened: {
|
|
},
|
|
chatSelect: {
|
|
text: 'Text Select',
|
|
},
|
|
chatPingExtension: {
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Map</Category>
|
|
},
|
|
showMinimap: {
|
|
text: 'Enable Minimap',
|
|
enableWarning: 'App reload is required to apply this setting',
|
|
values: [
|
|
'always',
|
|
'singleplayer',
|
|
'never'
|
|
],
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>World</Category>
|
|
},
|
|
highlightBlockColor: {
|
|
text: 'Block Highlight Color',
|
|
values: [
|
|
['auto', 'Auto'],
|
|
['blue', 'Blue'],
|
|
['classic', 'Classic']
|
|
],
|
|
},
|
|
showHand: {
|
|
text: 'Show Hand',
|
|
},
|
|
viewBobbing: {
|
|
text: 'View Bobbing',
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Sign Editor</Category>
|
|
},
|
|
autoSignEditor: {
|
|
text: 'Enable Sign Editor',
|
|
},
|
|
wysiwygSignEditor: {
|
|
text: 'WYSIWG Editor',
|
|
values: [
|
|
'auto',
|
|
'always',
|
|
'never'
|
|
],
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Experimental</Category>
|
|
},
|
|
displayBossBars: {
|
|
text: 'Boss Bars',
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <UiToggleButton name='title' addUiText />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <UiToggleButton name='chat' addUiText />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <UiToggleButton name='scoreboard' addUiText />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <UiToggleButton name='effects' label='Effects' />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <UiToggleButton name='indicators' label='Game Indicators' />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <UiToggleButton name='hotbar' />
|
|
},
|
|
},
|
|
],
|
|
controls: [
|
|
{
|
|
custom () {
|
|
return <Category>Keyboard & Mouse</Category>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
onClick={() => {
|
|
showModal({ reactType: 'keybindings' })
|
|
}}
|
|
>Keybindings
|
|
</Button>
|
|
},
|
|
mouseSensX: {},
|
|
mouseSensY: {
|
|
min: -1,
|
|
valueText (value) {
|
|
return value === -1 ? 'Same as X' : `${value}`
|
|
},
|
|
},
|
|
mouseRawInput: {
|
|
tooltip: 'Wether to disable any mouse acceleration (MC does it by default). Most probably it is still supported only by Chrome.',
|
|
// eslint-disable-next-line no-extra-boolean-cast
|
|
disabledReason: Boolean(document.documentElement.requestPointerLock) ? undefined : 'Your browser does not support pointer lock.',
|
|
},
|
|
autoFullScreen: {
|
|
tooltip: 'Auto Fullscreen allows you to use Ctrl+W and Escape having to wait/click on screen again.',
|
|
disabledReason: navigator['keyboard'] ? undefined : 'Your browser doesn\'t support keyboard lock API'
|
|
},
|
|
autoExitFullscreen: {
|
|
tooltip: 'Exit fullscreen on escape (pause menu open). But note you can always do it with F11.',
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Touch Controls</Category>
|
|
},
|
|
alwaysShowMobileControls: {
|
|
text: 'Always Mobile Controls',
|
|
},
|
|
touchButtonsSize: {
|
|
min: 40,
|
|
disableIf: [
|
|
'touchMovementType',
|
|
'modern'
|
|
],
|
|
},
|
|
touchButtonsOpacity: {
|
|
min: 10,
|
|
max: 90,
|
|
disableIf: [
|
|
'touchMovementType',
|
|
'modern'
|
|
],
|
|
},
|
|
touchButtonsPosition: {
|
|
max: 80,
|
|
disableIf: [
|
|
'touchMovementType',
|
|
'modern'
|
|
],
|
|
},
|
|
touchMovementType: {
|
|
text: 'Movement Controls',
|
|
values: [['modern', 'Modern'], ['classic', 'Classic']],
|
|
},
|
|
touchInteractionType: {
|
|
text: 'Interaction Controls',
|
|
values: [['classic', 'Classic'], ['buttons', 'Buttons']],
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { touchInteractionType, touchMovementType } = useSnapshot(options)
|
|
return <Button label='Setup Touch Buttons' onClick={() => showModal({ reactType: 'touch-buttons-setup' })} inScreen disabled={touchInteractionType === 'classic' && touchMovementType === 'classic'} />
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Auto Jump</Category>
|
|
},
|
|
autoJump: {
|
|
values: [
|
|
'always',
|
|
'auto',
|
|
'never'
|
|
],
|
|
disableIf: [
|
|
'autoParkour',
|
|
true
|
|
],
|
|
},
|
|
autoParkour: {},
|
|
}
|
|
],
|
|
sound: [
|
|
{ volume: {} },
|
|
{
|
|
custom () {
|
|
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
|
|
},
|
|
}
|
|
// { ignoreSilentSwitch: {} },
|
|
],
|
|
|
|
VR: [
|
|
{
|
|
custom () {
|
|
return (
|
|
<>
|
|
<span style={{ fontSize: 9, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
VR currently has basic support
|
|
</span>
|
|
<div />
|
|
</>
|
|
)
|
|
},
|
|
vrSupport: {},
|
|
vrPageGameRendering: {
|
|
text: 'Page Game Rendering',
|
|
tooltip: 'Wether to continue rendering page even when vr is active.',
|
|
}
|
|
},
|
|
],
|
|
advanced: [
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
onClick={() => {
|
|
if (confirm('Are you sure you want to reset all settings?')) resetOptions()
|
|
}}
|
|
>Reset settings</Button>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
onClick={() => {
|
|
if (confirm('Are you sure you want to remove all data (settings, keybindings, servers, username, auth, proxies)?')) resetLocalStorage()
|
|
}}
|
|
>Remove all data</Button>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button label='Export/Import...' onClick={() => openOptionsMenu('export-import')} inScreen />
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
const { cookieStorage } = useSnapshot(appStorage)
|
|
return <Button
|
|
label={`Storage: ${cookieStorage ? 'Synced Cookies' : 'Local Storage'}`} onClick={() => {
|
|
appStorage.cookieStorage = !cookieStorage
|
|
alert('Reload the page to apply this change')
|
|
}}
|
|
inScreen
|
|
/>
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Server Connection</Category>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { serversAutoVersionSelect } = useSnapshot(options)
|
|
const allVersions = [...[...supportedVersions].sort((a, b) => versionToNumber(a) - versionToNumber(b)), 'latest', 'auto']
|
|
const currentIndex = allVersions.indexOf(serversAutoVersionSelect)
|
|
|
|
const getDisplayValue = (version: string) => {
|
|
const versionAutoSelect = getVersionAutoSelect(version)
|
|
if (version === 'latest') return `latest (${versionAutoSelect})`
|
|
if (version === 'auto') return `auto (${versionAutoSelect})`
|
|
return version
|
|
}
|
|
|
|
return <div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<Slider
|
|
style={{ width: 150 }}
|
|
label='Default Version'
|
|
title='First version to try to connect with'
|
|
value={currentIndex}
|
|
min={0}
|
|
max={allVersions.length - 1}
|
|
unit=''
|
|
valueDisplay={getDisplayValue(serversAutoVersionSelect)}
|
|
updateValue={(newVal) => {
|
|
options.serversAutoVersionSelect = allVersions[newVal]
|
|
}}
|
|
/>
|
|
</div>
|
|
},
|
|
},
|
|
{
|
|
preventBackgroundTimeoutKick: {},
|
|
preventSleep: {
|
|
text: 'Prevent Device Sleep',
|
|
disabledReason: navigator.wakeLock ? undefined : 'Your browser does not support wake lock API',
|
|
enableWarning: 'When connected to a server, prevent PC from sleeping or screen dimming. Useful for purpusely staying AFK for long time. Some events might still prevent this like loosing tab focus or going low power mode.',
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
return <Category>Developer</Category>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { active } = useSnapshot(packetsRecordingState)
|
|
return <Button
|
|
inScreen
|
|
onClick={() => {
|
|
packetsRecordingState.active = !active
|
|
}}
|
|
>{active ? 'Stop' : 'Start'} Packets Replay Logging</Button>
|
|
},
|
|
},
|
|
{
|
|
custom () {
|
|
const { active, hasRecordedPackets } = useSnapshot(packetsRecordingState)
|
|
return <Button
|
|
disabled={!hasRecordedPackets}
|
|
inScreen
|
|
onClick={() => {
|
|
void downloadPacketsReplay()
|
|
}}
|
|
>Download Packets Replay</Button>
|
|
},
|
|
},
|
|
{
|
|
packetsLoggerPreset: {
|
|
text: 'Packets Logger Preset',
|
|
values: [
|
|
['all', 'All'],
|
|
['no-buffers', 'No Buffers']
|
|
],
|
|
},
|
|
},
|
|
{
|
|
debugContro: {
|
|
text: 'Debug Controls',
|
|
},
|
|
},
|
|
{
|
|
debugResponseTimeIndicator: {
|
|
text: 'Debug Input Lag',
|
|
},
|
|
},
|
|
{
|
|
debugChatScroll: {
|
|
},
|
|
}
|
|
],
|
|
'export-import': [
|
|
{
|
|
custom () {
|
|
return <Category>Export/Import Data</Category>
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
onClick={importData}
|
|
>Import Data</Button>
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
onClick={exportData}
|
|
>Export Data</Button>
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
disabled
|
|
>Export Worlds</Button>
|
|
}
|
|
},
|
|
{
|
|
custom () {
|
|
return <Button
|
|
inScreen
|
|
disabled
|
|
>Export Resource Pack</Button>
|
|
}
|
|
}
|
|
],
|
|
}
|
|
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' | 'export-import'
|
|
|
|
const Category = ({ children }) => <div style={{
|
|
fontSize: 9,
|
|
textAlign: 'center',
|
|
gridColumn: 'span 2'
|
|
}}>{children}</div>
|
|
|
|
const UiToggleButton = ({ name, addUiText = false, label = noCase(name) }: { name: string, addUiText?: boolean, label?: string }) => {
|
|
const { disabledUiParts } = useSnapshot(options)
|
|
|
|
const currentlyDisabled = disabledUiParts.includes(name)
|
|
if (addUiText) label = `${label} UI`
|
|
return <Button
|
|
inScreen
|
|
onClick={() => {
|
|
const newDisabledUiParts = currentlyDisabled ? disabledUiParts.filter(x => x !== name) : [...disabledUiParts, name]
|
|
options.disabledUiParts = newDisabledUiParts
|
|
}}
|
|
>{currentlyDisabled ? 'Enable' : 'Disable'} {label}</Button>
|
|
}
|
|
|
|
export const tryFindOptionConfig = (option: keyof AppOptions) => {
|
|
for (const group of Object.values(guiOptionsScheme)) {
|
|
for (const optionConfig of group) {
|
|
if (option in optionConfig) {
|
|
return optionConfig[option]
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|