import { StreamPref, StorageKey, type PrefTypeMap } from "@/enums/pref-keys"; import { DeviceVibrationMode, StreamPlayerType, StreamVideoProcessing, VideoPowerPreference, VideoRatio, VideoPosition, StreamStat, StreamStatPosition } from "@/enums/pref-values"; import { STATES } from "../global"; import { KeyboardShortcutDefaultId } from "../local-db/keyboard-shortcuts-table"; import { MkbMappingDefaultPresetId } from "../local-db/mkb-mapping-presets-table"; import { t } from "../translation"; import { BaseSettingsStorage } from "./base-settings-storage"; import { CE } from "../html"; import type { SettingActionOrigin, SettingDefinitions } from "@/types/setting-definition"; import { BxIcon } from "../bx-icon"; import { GameSettingsStorage } from "./game-settings-storage"; import { BxLogger } from "../bx-logger"; import { ControllerCustomizationDefaultPresetId } from "../local-db/controller-customizations-table"; import { ControllerShortcutDefaultId } from "../local-db/controller-shortcuts-table"; import { BxEventBus } from "../bx-event-bus"; import { WebGPUPlayer } from "@/modules/player/webgpu/webgpu-player"; export class StreamSettingsStorage extends BaseSettingsStorage { static readonly DEFINITIONS: SettingDefinitions = { [StreamPref.DEVICE_VIBRATION_MODE]: { requiredVariants: 'full', label: t('device-vibration'), default: DeviceVibrationMode.OFF, options: { [DeviceVibrationMode.OFF]: t('off'), [DeviceVibrationMode.ON]: t('on'), [DeviceVibrationMode.AUTO]: t('device-vibration-not-using-gamepad'), }, }, [StreamPref.DEVICE_VIBRATION_INTENSITY]: { requiredVariants: 'full', label: t('vibration-intensity'), default: 50, min: 10, max: 100, params: { steps: 10, suffix: '%', exactTicks: 20, }, }, [StreamPref.CONTROLLER_POLLING_RATE]: { requiredVariants: 'full', label: t('polling-rate'), default: 4, min: 4, max: 60, params: { steps: 4, exactTicks: 20, reverse: true, customTextValue(value: any) { value = parseInt(value); let text = +(1000 / value).toFixed(2) + ' Hz'; if (value === 4) { text = `${text} (${t('default')})`; } return text; }, }, }, [StreamPref.CONTROLLER_SETTINGS]: { default: {}, }, [StreamPref.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY]: { requiredVariants: 'full', label: t('horizontal-scroll-sensitivity'), default: 0, min: 0, max: 100 * 100, params: { steps: 10, exactTicks: 20 * 100, customTextValue: (value: any) => { if (!value) { return t('default'); } return (value / 100).toFixed(1) + 'x'; }, }, }, [StreamPref.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY]: { requiredVariants: 'full', label: t('vertical-scroll-sensitivity'), default: 0, min: 0, max: 100 * 100, params: { steps: 10, exactTicks: 20 * 100, customTextValue: (value: any) => { if (!value) { return t('default'); } return (value / 100).toFixed(1) + 'x'; }, }, }, [StreamPref.MKB_P1_MAPPING_PRESET_ID]: { requiredVariants: 'full', default: MkbMappingDefaultPresetId.DEFAULT, }, [StreamPref.MKB_P1_SLOT]: { requiredVariants: 'full', default: 1, min: 1, max: 4, params: { hideSlider: true, }, }, [StreamPref.MKB_P2_MAPPING_PRESET_ID]: { requiredVariants: 'full', default: MkbMappingDefaultPresetId.OFF, }, [StreamPref.MKB_P2_SLOT]: { requiredVariants: 'full', default: 0, min: 0, max: 4, params: { hideSlider: true, customTextValue(value: any) { value = parseInt(value); return (value === 0) ? t('off') : value.toString(); }, }, }, [StreamPref.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID]: { requiredVariants: 'full', default: KeyboardShortcutDefaultId.DEFAULT, }, [StreamPref.VIDEO_PLAYER_TYPE]: { label: t('renderer'), default: StreamPlayerType.VIDEO, options: { [StreamPlayerType.VIDEO]: t('default'), [StreamPlayerType.WEBGL2]: t('webgl2'), [StreamPlayerType.WEBGPU]: `${t('webgpu')} (${t('experimental')})`, }, suggest: { lowest: StreamPlayerType.VIDEO, highest: StreamPlayerType.WEBGL2, }, ready: (setting: any) => { BxEventBus.Script.on('webgpu.ready', () => { if (!WebGPUPlayer.device) { // Remove WebGPU option on unsupported browsers delete setting.options[StreamPlayerType.WEBGPU]; } }); }, }, [StreamPref.VIDEO_PROCESSING]: { label: t('clarity-boost'), default: StreamVideoProcessing.USM, options: { [StreamVideoProcessing.USM]: t('unsharp-masking'), [StreamVideoProcessing.CAS]: t('amd-fidelity-cas'), }, suggest: { lowest: StreamVideoProcessing.USM, highest: StreamVideoProcessing.CAS, }, }, [StreamPref.VIDEO_POWER_PREFERENCE]: { label: t('renderer-configuration'), default: VideoPowerPreference.DEFAULT, options: { [VideoPowerPreference.DEFAULT]: t('default'), [VideoPowerPreference.LOW_POWER]: t('battery-saving'), [VideoPowerPreference.HIGH_PERFORMANCE]: t('high-performance'), }, suggest: { highest: 'low-power', }, }, [StreamPref.VIDEO_MAX_FPS]: { label: t('limit-fps'), default: 60, min: 10, max: 60, params: { steps: 10, exactTicks: 10, customTextValue: (value: any) => { value = parseInt(value); return value === 60 ? t('unlimited') : value + 'fps'; }, }, }, [StreamPref.VIDEO_SHARPNESS]: { label: t('sharpness'), default: 0, min: 0, max: 10, params: { exactTicks: 2, customTextValue: (value: any) => { value = parseInt(value); return value === 0 ? t('off') : value.toString(); }, }, suggest: { lowest: 0, highest: 2, }, }, [StreamPref.VIDEO_RATIO]: { label: t('aspect-ratio'), note: STATES.browser.capabilities.touch ? t('aspect-ratio-note') : undefined, default: VideoRatio['16:9'], options: { [VideoRatio['16:9']]: `16:9 (${t('default')})`, [VideoRatio['18:9']]: '18:9', [VideoRatio['21:9']]: '21:9', [VideoRatio['16:10']]: '16:10', [VideoRatio['4:3']]: '4:3', [VideoRatio.FILL]: t('stretch'), //'cover': 'Cover', }, }, [StreamPref.VIDEO_POSITION]: { label: t('position'), note: STATES.browser.capabilities.touch ? t('aspect-ratio-note') : undefined, default: VideoPosition.CENTER, options: { [VideoPosition.TOP]: t('top'), [VideoPosition.TOP_HALF]: t('top-half'), [VideoPosition.CENTER]: `${t('center')} (${t('default')})`, [VideoPosition.BOTTOM_HALF]: t('bottom-half'), [VideoPosition.BOTTOM]: t('bottom'), }, }, [StreamPref.VIDEO_SATURATION]: { label: t('saturation'), default: 100, min: 50, max: 150, params: { suffix: '%', ticks: 25, }, }, [StreamPref.VIDEO_CONTRAST]: { label: t('contrast'), default: 100, min: 50, max: 150, params: { suffix: '%', ticks: 25, }, }, [StreamPref.VIDEO_BRIGHTNESS]: { label: t('brightness'), default: 100, min: 50, max: 150, params: { suffix: '%', ticks: 25, }, }, [StreamPref.AUDIO_VOLUME]: { label: t('volume'), default: 100, min: 0, max: 600, params: { steps: 10, suffix: '%', ticks: 100, }, }, [StreamPref.STATS_ITEMS]: { label: t('stats'), default: [StreamStat.PING, StreamStat.FPS, StreamStat.BITRATE, StreamStat.DECODE_TIME, StreamStat.PACKETS_LOST, StreamStat.FRAMES_LOST], multipleOptions: { [StreamStat.CLOCK]: t('clock'), [StreamStat.PLAYTIME]: t('playtime'), [StreamStat.BATTERY]: t('battery'), [StreamStat.PING]: t('stat-ping'), [StreamStat.JITTER]: t('jitter'), [StreamStat.FPS]: t('stat-fps'), [StreamStat.BITRATE]: t('stat-bitrate'), [StreamStat.DECODE_TIME]: t('stat-decode-time'), [StreamStat.PACKETS_LOST]: t('stat-packets-lost'), [StreamStat.FRAMES_LOST]: t('stat-frames-lost'), [StreamStat.DOWNLOAD]: t('downloaded'), [StreamStat.UPLOAD]: t('uploaded'), }, params: { size: 0, }, ready: (setting: any) => { // Remove Battery option in unsupported browser const multipleOptions = (setting as any).multipleOptions; if (!STATES.browser.capabilities.batteryApi) { delete multipleOptions[StreamStat.BATTERY]; } // Update texts for (const key in multipleOptions) { multipleOptions[key] = (key as string).toUpperCase() + ': ' + multipleOptions[key]; } }, }, [StreamPref.STATS_SHOW_WHEN_PLAYING]: { label: t('show-stats-on-startup'), default: false, }, [StreamPref.STATS_QUICK_GLANCE_ENABLED]: { label: '👀 ' + t('enable-quick-glance-mode'), default: true, }, [StreamPref.STATS_POSITION]: { label: t('position'), default: StreamStatPosition.TOP_RIGHT, options: { [StreamStatPosition.TOP_LEFT]: t('top-left'), [StreamStatPosition.TOP_CENTER]: t('top-center'), [StreamStatPosition.TOP_RIGHT]: t('top-right'), }, }, [StreamPref.STATS_TEXT_SIZE]: { label: t('text-size'), default: '0.9rem', options: { '0.9rem': t('small'), '1.0rem': t('normal'), '1.1rem': t('large'), }, }, [StreamPref.STATS_OPACITY_ALL]: { label: t('opacity'), default: 80, min: 50, max: 100, params: { steps: 10, suffix: '%', ticks: 10, }, }, [StreamPref.STATS_OPACITY_BACKGROUND]: { label: t('background-opacity'), default: 100, min: 0, max: 100, params: { steps: 10, suffix: '%', ticks: 10, }, }, [StreamPref.STATS_CONDITIONAL_FORMATTING]: { label: t('conditional-formatting'), default: false, }, [StreamPref.LOCAL_CO_OP_ENABLED]: { requiredVariants: 'full', label: t('enable-local-co-op-support'), labelIcon: BxIcon.LOCAL_CO_OP, default: false, note: () => CE('div', false, CE('a', { href: 'https://github.com/redphx/better-xcloud/discussions/275', target: '_blank', }, t('enable-local-co-op-support-note')), CE('br'), '⚠️ ' + t('unexpected-behavior'), ), }, }; private gameSettings: {[key: number]: GameSettingsStorage} = {}; private xboxTitleId: number = -1; constructor() { super(StorageKey.STREAM, StreamSettingsStorage.DEFINITIONS); } setGameId(id: number) { this.xboxTitleId = id; } getGameSettings(id: number) { if (id > -1) { if (!this.gameSettings[id]) { const gameStorage = new GameSettingsStorage(id); this.gameSettings[id] = gameStorage; // Remove values same as global's for (const key in gameStorage.settings) { this.getSettingByGame(id, key); } } return this.gameSettings[id]; } return null; } getSetting>(key: K, checkUnsupported?: boolean): PrefTypeMap[K] { return this.getSettingByGame(this.xboxTitleId, key, checkUnsupported)!; } getSettingByGame>(id: number, key: K, checkUnsupported?: boolean): PrefTypeMap[K] | undefined { const gameSettings = this.getGameSettings(id); if (gameSettings?.hasSetting(key as StreamPref)) { let gameValue = gameSettings.getSetting(key, checkUnsupported); const globalValue = super.getSetting(key, checkUnsupported); // Remove value if it's the same as global's if (globalValue === gameValue) { this.deleteSettingByGame(id, key as StreamPref); gameValue = globalValue; } return gameValue; } return super.getSetting(key, checkUnsupported); } setSetting(key: StreamPref, value: V, origin: SettingActionOrigin): V { return this.setSettingByGame(this.xboxTitleId, key, value, origin); } setSettingByGame(id: number, key: StreamPref, value: V, origin: SettingActionOrigin): V { const gameSettings = this.getGameSettings(id); if (gameSettings) { BxLogger.info('setSettingByGame', id, key, value); return gameSettings.setSetting(key, value, origin); } BxLogger.info('setSettingByGame', id, key, value); return super.setSetting(key, value, origin); } deleteSettingByGame(id: number, key: StreamPref): boolean { const gameSettings = this.getGameSettings(id); if (gameSettings) { return gameSettings.deleteSetting(key); } return false; } hasGameSetting(id: number, key: StreamPref): boolean { const gameSettings = this.getGameSettings(id); return !!(gameSettings && gameSettings.hasSetting(key)); } getControllerSetting(gamepadId: string): ControllerSetting { const controllerSettings = this.getSetting(StreamPref.CONTROLLER_SETTINGS); let controllerSetting = controllerSettings[gamepadId]; if (!controllerSetting) { controllerSetting = {} as ControllerSetting; } // Set missing settings if (!controllerSetting.hasOwnProperty('shortcutPresetId')) { controllerSetting.shortcutPresetId = ControllerShortcutDefaultId.DEFAULT; } if (!controllerSetting.hasOwnProperty('customizationPresetId')) { controllerSetting.customizationPresetId = ControllerCustomizationDefaultPresetId.DEFAULT; } return controllerSetting; } }