mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-09 00:57:19 +02:00
497 lines
17 KiB
TypeScript
497 lines
17 KiB
TypeScript
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<StreamPref> {
|
|
static readonly DEFINITIONS: SettingDefinitions<StreamPref> = {
|
|
[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<K extends keyof PrefTypeMap<K>>(key: K, checkUnsupported?: boolean): PrefTypeMap<K>[K] {
|
|
return this.getSettingByGame(this.xboxTitleId, key, checkUnsupported)!;
|
|
}
|
|
|
|
getSettingByGame<K extends keyof PrefTypeMap<K>>(id: number, key: K, checkUnsupported?: boolean): PrefTypeMap<K>[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<V = any>(key: StreamPref, value: V, origin: SettingActionOrigin): V {
|
|
return this.setSettingByGame(this.xboxTitleId, key, value, origin);
|
|
}
|
|
|
|
setSettingByGame<V = any>(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;
|
|
}
|
|
}
|