diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index 05a34ab..17fa6ec 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -452,6 +452,17 @@ BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e}); return str; }, + patchGamepadPolling(str: string) { + let index = str.indexOf('.shouldHandleGamepadInput)())return void'); + if (index === -1) { + return false; + } + + index = str.indexOf('{', index - 20) + 1; + str = str.substring(0, index) + 'if (window.BX_EXPOSED.disableGamepadPolling) return;' + str.substring(index); + return str; + }, + patchXcloudTitleInfo(str: string) { const text = 'async cloudConnect'; let index = str.indexOf(text); @@ -803,6 +814,7 @@ let PATCH_ORDERS: PatchArray = [ 'disableStreamGate', 'overrideSettings', 'broadcastPollingMode', + getPref(PrefKey.UI_CONTROLLER_FRIENDLY) && 'patchGamepadPolling', 'exposeStreamSession', 'exposeDialogRoutes', diff --git a/src/modules/patches/controller-shortcuts.js b/src/modules/patches/controller-shortcuts.js index 72bbd8e..0b88931 100644 --- a/src/modules/patches/controller-shortcuts.js +++ b/src/modules/patches/controller-shortcuts.js @@ -1,3 +1,8 @@ +if (window.BX_EXPOSED.disableGamepadPolling) { + this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(4) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 4); + return; +} + const currentGamepad = ${gamepadVar}; // Share button on XS controller diff --git a/src/modules/stream/stream-settings.ts b/src/modules/stream/stream-settings.ts index 6f52fb6..78e1db9 100644 --- a/src/modules/stream/stream-settings.ts +++ b/src/modules/stream/stream-settings.ts @@ -13,6 +13,7 @@ import { VibrationManager } from "../vibration-manager"; import { StreamStats } from "./stream-stats"; import { BxSelectElement } from "@/web-components/bx-select"; import { onChangeVideoPlayerType, updateVideoPlayer } from "./stream-settings-utils"; +import { GamepadKey } from "@/enums/mkb"; enum FocusDirection { UP, @@ -38,6 +39,21 @@ export class StreamSettings { return StreamSettings.instance; } + private static readonly GAMEPAD_POLLING_INTERVAL = 50; + private static readonly GAMEPAD_KEYS = [ + GamepadKey.UP, + GamepadKey.DOWN, + GamepadKey.LEFT, + GamepadKey.RIGHT, + GamepadKey.A, + GamepadKey.B, + GamepadKey.LB, + GamepadKey.RB, + ]; + + private gamepadPollingIntervalId: number | null = null; + private gamepadLastButtons: Array = []; + private $container: HTMLElement | undefined; private $tabs: HTMLElement | undefined; private $settings: HTMLElement | undefined; @@ -282,6 +298,12 @@ export class StreamSettings { // Add event listeners $container.addEventListener('keydown', this); + + // Start gamepad polling + this.#startGamepadPolling(); + + // Disable xCloud's navigation polling + (window as any).BX_EXPOSED.disableGamepadPolling = true; } BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN); @@ -298,9 +320,94 @@ export class StreamSettings { // Remove event listeners this.$container!.removeEventListener('keydown', this); + // Stop gamepad polling(); + this.#stopGamepadPolling(); + + // Enable xCloud's navigation polling + (window as any).BX_EXPOSED.disableGamepadPolling = false; + BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED); } + #pollGamepad() { + const gamepads = window.navigator.getGamepads(); + + let direction: FocusDirection | null = null; + for (const gamepad of gamepads) { + if (!gamepad || !gamepad.connected) { + continue; + } + + const buttons = gamepad.buttons; + let lastButton = this.gamepadLastButtons[gamepad.index]; + let pressedButton: GamepadKey | undefined = undefined; + + for (const key of StreamSettings.GAMEPAD_KEYS) { + if (typeof lastButton === 'number') { + // Key pressed + if (lastButton === key && !buttons[key].pressed) { + pressedButton = key; + break; + } + } else if (buttons[key].pressed) { + this.gamepadLastButtons[gamepad.index] = key; + break; + } + } + + if (typeof pressedButton !== 'undefined') { + this.gamepadLastButtons[gamepad.index] = null; + + if (pressedButton === GamepadKey.A) { + document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click')); + } else if (pressedButton === GamepadKey.B) { + this.hide(); + } + + if (pressedButton === GamepadKey.UP) { + direction = FocusDirection.UP; + } else if (pressedButton === GamepadKey.DOWN) { + direction = FocusDirection.DOWN; + } else if (pressedButton === GamepadKey.LEFT) { + direction = FocusDirection.LEFT; + } else if (pressedButton === GamepadKey.RIGHT) { + direction = FocusDirection.RIGHT; + } + + if (direction !== null) { + let handled = false; + if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === 'range') { + const $range = document.activeElement; + if (direction === FocusDirection.LEFT || direction === FocusDirection.RIGHT) { + $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === FocusDirection.LEFT ? -1 : 1)).toString(); + $range.dispatchEvent(new InputEvent('input')); + handled = true; + } + } + + if (!handled) { + this.#focusDirection(direction); + } + } + + return; + } + } + } + + #startGamepadPolling() { + this.#stopGamepadPolling(); + + this.gamepadPollingIntervalId = window.setInterval(this.#pollGamepad.bind(this), StreamSettings.GAMEPAD_POLLING_INTERVAL); + } + + #stopGamepadPolling() { + this.gamepadLastButtons = []; + + this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId); + this.gamepadPollingIntervalId = null; + } + #handleTabsNavigation($focusing: HTMLElement, direction: FocusDirection) { if (direction === FocusDirection.UP || direction === FocusDirection.DOWN) { let $sibling = $focusing; @@ -418,7 +525,7 @@ export class StreamSettings { case 'keydown': const $target = event.target as HTMLElement; const keyboardEvent = event as KeyboardEvent; - const keyCode = keyboardEvent.code; + const keyCode = keyboardEvent.code || keyboardEvent.key; if (keyCode === 'ArrowUp' || keyCode === 'ArrowDown') { event.preventDefault(); diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index e75c999..472c2ad 100644 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -115,4 +115,6 @@ export const BxExposed = { hasCompletedOnboarding: true, }, }, + + disableGamepadPolling: false, }; diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index cc0478b..162be9d 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -714,6 +714,7 @@ export class Preferences { default: 100, min: 0, max: 600, + steps: 20, params: { suffix: '%', ticks: 100, @@ -772,6 +773,7 @@ export class Preferences { default: 80, min: 50, max: 100, + steps: 10, params: { suffix: '%', ticks: 10,