pages235/src/optionsStorage.ts
2025-04-07 02:21:37 +03:00

293 lines
8.7 KiB
TypeScript

import { proxy, subscribe } from 'valtio/vanilla'
import { subscribeKey } from 'valtio/utils'
import { omitObj } from '@zardoy/utils'
import { appQueryParamsArray } from './appParams'
import type { AppConfig } from './appConfig'
import { appStorage } from './react/appStorageProvider'
const isDev = process.env.NODE_ENV === 'development'
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
const defaultOptions = {
renderDistance: 3,
keepChunksDistance: 1,
multiplayerRenderDistance: 3,
closeConfirmation: true,
autoFullScreen: false,
mouseRawInput: true,
autoExitFullscreen: false,
localUsername: 'wanderer',
mouseSensX: 50,
mouseSensY: -1,
chatWidth: 320,
chatHeight: 180,
chatScale: 100,
chatOpacity: 100,
chatOpacityOpened: 100,
messagesLimit: 200,
volume: 50,
enableMusic: false,
// fov: 70,
fov: 75,
guiScale: 3,
autoRequestCompletions: true,
touchButtonsSize: 40,
touchButtonsOpacity: 80,
touchButtonsPosition: 12,
touchControlsPositions: getDefaultTouchControlsPositions(),
touchControlsSize: getTouchControlsSize(),
touchMovementType: 'modern' as 'modern' | 'classic',
touchInteractionType: 'classic' as 'classic' | 'buttons',
gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power',
backgroundRendering: '20fps' as 'full' | '20fps' | '5fps',
/** @unstable */
disableAssets: false,
/** @unstable */
debugLogNotFrequentPackets: false,
unimplementedContainers: false,
dayCycleAndLighting: true,
loadPlayerSkins: true,
renderEars: true,
lowMemoryMode: false,
starfieldRendering: true,
enabledResourcepack: null as string | null,
useVersionsTextures: 'latest',
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
showHand: true,
viewBobbing: true,
displayRecordButton: true,
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
customChannels: false,
remoteContentNotSameOrigin: false as boolean | string[],
packetsReplayAutoStart: false,
preciseMouseInput: false,
// todo ui setting, maybe enable by default?
waitForChunksRender: 'sp-only' as 'sp-only' | boolean,
jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>,
preventBackgroundTimeoutKick: false,
preventSleep: false,
// antiAliasing: false,
clipWorldBelowY: undefined as undefined | number, // will be removed
disableSignsMapsSupport: false,
singleplayerAutoSave: false,
showChunkBorders: false, // todo rename option
frameLimit: false as number | false,
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
alwaysShowMobileControls: false,
excludeCommunicationDebugEvents: [],
preventDevReloadWhilePlaying: false,
numWorkers: 4,
localServerOptions: {
gameMode: 1
} as any,
preferLoadReadonly: false,
disableLoadPrompts: false,
guestUsername: 'guest',
askGuestName: true,
errorReporting: true,
/** Actually might be useful */
showCursorBlockInSpectator: false,
renderEntities: true,
smoothLighting: true,
newVersionsLighting: false,
chatSelect: true,
autoJump: 'auto' as 'auto' | 'always' | 'never',
autoParkour: false,
vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users
renderDebug: (isDev ? 'advanced' : 'basic') as 'none' | 'advanced' | 'basic',
// advanced bot options
autoRespawn: false,
mutedSounds: [] as string[],
plugins: [] as Array<{ enabled: boolean, name: string, description: string, script: string }>,
/** Wether to popup sign editor on server action */
autoSignEditor: true,
wysiwygSignEditor: 'auto' as 'auto' | 'always' | 'never',
showMinimap: 'never' as 'always' | 'singleplayer' | 'never',
minimapOptimizations: true,
displayBossBars: true,
disabledUiParts: [] as string[],
neighborChunkUpdates: true,
highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic',
activeRenderer: 'threejs',
rendererSharedOptions: {
_experimentalSmoothChunkLoading: true,
_renderByChunks: false
}
}
function getDefaultTouchControlsPositions () {
return {
action: [
70,
76
],
sneak: [
84,
76
],
break: [
70,
57
],
jump: [
84,
57
],
} as Record<string, [number, number]>
}
function getTouchControlsSize () {
return {
joystick: 55,
action: 36,
break: 36,
jump: 36,
sneak: 36,
}
}
// const qsOptionsRaw = new URLSearchParams(location.search).getAll('setting')
const qsOptionsRaw = appQueryParamsArray.setting ?? []
export const qsOptions = Object.fromEntries(qsOptionsRaw.map(o => {
const [key, value] = o.split(':')
return [key, JSON.parse(value)]
}))
// Track which settings are disabled (controlled by QS or forced by config)
export const disabledSettings = proxy({
value: new Set<string>(Object.keys(qsOptions))
})
const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
if (options.highPerformanceGpu) {
options.gpuPreference = 'high-performance'
delete options.highPerformanceGpu
}
if (Object.keys(options.touchControlsPositions ?? {}).length === 0) {
options.touchControlsPositions = defaultOptions.touchControlsPositions
}
if (options.touchControlsPositions?.jump === undefined) {
options.touchControlsPositions!.jump = defaultOptions.touchControlsPositions.jump
}
if (options.touchControlsType === 'joystick-buttons') {
options.touchInteractionType = 'buttons'
}
return options
}
const migrateOptionsLocalStorage = () => {
if (Object.keys(appStorage.options).length) {
for (const key of Object.keys(appStorage.options)) {
if (!(key in defaultOptions)) continue // drop unknown options
const defaultValue = defaultOptions[key]
if (JSON.stringify(defaultValue) !== JSON.stringify(appStorage.options[key])) {
appStorage.changedSettings[key] = appStorage.options[key]
}
}
appStorage.options = {}
}
}
export type AppOptions = typeof defaultOptions
const isDeepEqual = (a: any, b: any): boolean => {
if (a === b) return true
if (typeof a !== typeof b) return false
if (typeof a !== 'object') return false
if (a === null || b === null) return a === b
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
return a.every((item, index) => isDeepEqual(item, b[index]))
}
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
return keysA.every(key => isDeepEqual(a[key], b[key]))
}
export const getChangedSettings = () => {
return Object.fromEntries(
Object.entries(appStorage.changedSettings).filter(([key, value]) => !isDeepEqual(defaultOptions[key], value))
)
}
migrateOptionsLocalStorage()
export const options: AppOptions = proxy({
...defaultOptions,
...initialAppConfig.defaultSettings,
...migrateOptions(appStorage.changedSettings),
...qsOptions
})
window.options = window.settings = options
export const resetOptions = () => {
Object.assign(options, defaultOptions)
}
Object.defineProperty(window, 'debugChangedOptions', {
get () {
return getChangedSettings()
},
})
subscribe(options, (ops) => {
for (const op of ops) {
const [type, path, value] = op
// let patch
// let accessor = options
// for (const part of path) {
// }
const key = path[0] as string
if (disabledSettings.value.has(key)) continue
appStorage.changedSettings[key] = options[key]
}
})
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void
export const watchValue: WatchValue = (proxy, callback) => {
const watchedProps = new Set<string>()
callback(new Proxy(proxy, {
get (target, p, receiver) {
watchedProps.add(p.toString())
return Reflect.get(target, p, receiver)
},
}), false)
const unsubscribes = [] as Array<() => void>
for (const prop of watchedProps) {
unsubscribes.push(
subscribeKey(proxy, prop, () => {
callback(proxy, true)
})
)
}
return () => {
for (const unsubscribe of unsubscribes) {
unsubscribe()
}
}
}
watchValue(options, o => {
globalThis.excludeCommunicationDebugEvents = o.excludeCommunicationDebugEvents
})
watchValue(options, o => {
document.body.classList.toggle('disable-assets', o.disableAssets)
})
watchValue(options, o => {
document.body.style.setProperty('--touch-movement-buttons-opacity', (o.touchButtonsOpacity / 100).toString())
})
watchValue(options, o => {
document.body.style.setProperty('--touch-movement-buttons-position', (o.touchButtonsPosition * 2) + 'px')
})
export const useOptionValue = (setting, valueCallback) => {
valueCallback(setting)
subscribe(setting, valueCallback)
}