From 0f48cb891f6516b92c5fd1a41620f56354aa48b5 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Wed, 29 May 2024 17:28:39 +0700 Subject: [PATCH] Support emulated MKB in Android app commit ad365d4ee854971122f0e8cb9157ed44b3aac0d8 Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 17:19:57 2024 +0700 Fix not able to reconnect to WebSocket server when switching game commit ca9369318d4cbb831650e8ca631e7997dc7706cb Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 17:19:23 2024 +0700 Stop emulated MKB when losing pointer capture commit 8cca1a0554c46b8f61455e79d5b16f1dff9a8014 Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 17:17:42 2024 +0700 Allow fine-tuning maximum video bitrate commit 763d414d560d9d2aa6710fd60e3f80bf43a534d6 Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 08:13:56 2024 +0700 Update mouse settings commit d65c5ab4e4a33ed8ad13acf0a15c4bb5ace870eb Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 08:10:49 2024 +0700 Increase MKB dialog's bg opacity commit 3e72f2ad2700737c8148ef47629528954a606578 Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 08:02:57 2024 +0700 Show/hide MKB dialog properly commit e7786f36508e3aa843604d9886861930bada5d60 Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 07:47:21 2024 +0700 Fix connecting to WebSocket server when it's not ready commit 512d8c227a057e5c0399bf128bc1c52a88fcf853 Author: redphx <96280+redphx@users.noreply.github.com> Date: Wed May 29 07:18:06 2024 +0700 Fix arrow keys not working in Android app commit 0ce90f47f37d057d5a4fab0003e2bec8960d1eee Author: redphx <96280+redphx@users.noreply.github.com> Date: Tue May 28 17:36:56 2024 +0700 Set mouse's default sensitivities to 50 commit 16eb48660dd44497e16ca22343a880d9a2e53a30 Author: redphx <96280+redphx@users.noreply.github.com> Date: Tue May 28 17:33:37 2024 +0700 Allow emulated MKB feature in Android app commit c3d0e64f8502e19cd4f167fea4cdbdfc2e14b65e Author: redphx <96280+redphx@users.noreply.github.com> Date: Tue May 28 17:32:49 2024 +0700 Remove stick decay settings commit d289d2a0dea61a440c1bc6b9392920b8e6ab6298 Author: redphx <96280+redphx@users.noreply.github.com> Date: Tue May 28 17:21:39 2024 +0700 Remove stick decaying feature commit 76bd001d98bac53f757f4ae793b2850aad055007 Author: redphx <96280+redphx@users.noreply.github.com> Date: Tue May 28 17:21:14 2024 +0700 Update data structure commit c5d3c87da9e6624ebefb288f6d7c8d06dc00916b Author: redphx <96280+redphx@users.noreply.github.com> Date: Tue May 28 08:14:27 2024 +0700 Fix not toggling the MKB feature correctly commit 9615535cf0e4d4372e201aefb6f1231ddbc22536 Author: redphx <96280+redphx@users.noreply.github.com> Date: Mon May 27 20:51:57 2024 +0700 Handle mouse data from the app --- src/assets/css/mkb.styl | 2 +- src/index.ts | 3 + src/modules/game-bar/game-bar.ts | 12 + src/modules/mkb/definitions.ts | 3 - src/modules/mkb/key-helper.ts | 4 +- src/modules/mkb/mkb-handler.ts | 379 ++++++++++++++++++------------ src/modules/mkb/mkb-preset.ts | 43 +--- src/modules/mkb/pointer-client.ts | 152 ++++++++++++ src/modules/patcher.ts | 2 +- src/modules/stream/stream-ui.ts | 8 - src/types/index.d.ts | 20 ++ src/types/preferences.d.ts | 2 +- src/utils/bx-event.ts | 5 +- src/utils/bx-exposed.ts | 17 -- src/utils/monkey-patches.ts | 2 +- src/utils/preferences.ts | 24 +- 16 files changed, 449 insertions(+), 229 deletions(-) create mode 100644 src/modules/mkb/pointer-client.ts diff --git a/src/assets/css/mkb.styl b/src/assets/css/mkb.styl index 4ab0b25..25d46a0 100644 --- a/src/assets/css/mkb.styl +++ b/src/assets/css/mkb.styl @@ -25,7 +25,7 @@ top: 50%; transform: translateX(-50%) translateY(-50%); margin: auto; - background: #000000e5; + background: #000000b3; z-index: var(--bx-mkb-pointer-lock-msg-z-index); color: #fff; text-align: center; diff --git a/src/index.ts b/src/index.ts index fa16006..dbebc2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -282,6 +282,9 @@ function main() { if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') { TouchController.setup(); } + + // Start PointerProviderServer + (getPref(PrefKey.MKB_ENABLED)) && AppInterface && AppInterface.startPointerServer(); } main(); diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts index 1012a07..ad5a018 100644 --- a/src/modules/game-bar/game-bar.ts +++ b/src/modules/game-bar/game-bar.ts @@ -82,6 +82,18 @@ export class GameBar { document.documentElement.appendChild($gameBar); this.$gameBar = $gameBar; this.$container = $container; + + // Enable/disable Game Bar when playing/pausing + getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => { + if (!STATES.isPlaying) { + this.disable(); + return; + } + + // Toggle Game bar + const mode = (e as any).mode; + mode !== 'None' ? this.disable() : this.enable(); + }).bind(this)); } private beginHideTimeout() { diff --git a/src/modules/mkb/definitions.ts b/src/modules/mkb/definitions.ts index 181f4f6..2ab7431 100644 --- a/src/modules/mkb/definitions.ts +++ b/src/modules/mkb/definitions.ts @@ -99,7 +99,4 @@ export enum MkbPresetKey { MOUSE_SENSITIVITY_Y = 'sensitivity_y', MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzone_counterweight', - - MOUSE_STICK_DECAY_STRENGTH = 'stick_decay_strength', - MOUSE_STICK_DECAY_MIN = 'stick_decay_min', } diff --git a/src/modules/mkb/key-helper.ts b/src/modules/mkb/key-helper.ts index efd520c..47a8161 100644 --- a/src/modules/mkb/key-helper.ts +++ b/src/modules/mkb/key-helper.ts @@ -20,7 +20,7 @@ export class KeyHelper { let name; if (e instanceof KeyboardEvent) { - code = e.code; + code = e.code || e.key; } else if (e instanceof WheelEvent) { if (e.deltaY < 0) { code = WheelCode.SCROLL_UP; @@ -28,7 +28,7 @@ export class KeyHelper { code = WheelCode.SCROLL_DOWN; } else if (e.deltaX < 0) { code = WheelCode.SCROLL_LEFT; - } else { + } else if (e.deltaX > 0) { code = WheelCode.SCROLL_RIGHT; } } else if (e instanceof MouseEvent) { diff --git a/src/modules/mkb/mkb-handler.ts b/src/modules/mkb/mkb-handler.ts index f341063..3f287a1 100644 --- a/src/modules/mkb/mkb-handler.ts +++ b/src/modules/mkb/mkb-handler.ts @@ -9,13 +9,152 @@ import { LocalDb } from "@utils/local-db"; import { KeyHelper } from "./key-helper"; import type { MkbStoredPreset } from "@/types/mkb"; import { showStreamSettings } from "@modules/stream/stream-ui"; -import { STATES } from "@utils/global"; +import { AppInterface, STATES } from "@utils/global"; import { UserAgent } from "@utils/user-agent"; import { BxLogger } from "@utils/bx-logger"; import { BxIcon } from "@utils/bx-icon"; +import { PointerClient } from "./pointer-client"; const LOG_TAG = 'MkbHandler'; + +abstract class MouseDataProvider { + protected mkbHandler: MkbHandler; + constructor(handler: MkbHandler) { + this.mkbHandler = handler; + } + + abstract init(): void; + abstract start(): void; + abstract stop(): void; + abstract destroy(): void; + abstract toggle(enabled: boolean): void; +} + +class WebSocketMouseDataProvider extends MouseDataProvider { + #pointerClient: PointerClient | undefined + #connected = false + + init(): void { + this.#pointerClient = PointerClient.getInstance(); + this.#connected = false; + try { + this.#pointerClient.start(this.mkbHandler); + this.#connected = true; + } catch (e) { + Toast.show('Cannot enable Mouse & Keyboard feature'); + } + } + + start(): void { + this.#connected && AppInterface.requestPointerCapture(); + } + + stop(): void { + this.#connected && AppInterface.releasePointerCapture(); + } + + destroy(): void { + this.#connected && this.#pointerClient?.stop(); + } + + toggle(enabled: boolean): void { + if (!this.#connected) { + enabled = false; + } + + enabled ? this.mkbHandler.start() : this.mkbHandler.stop(); + this.mkbHandler.waitForMouseData(!enabled); + } +} + +class PointerLockMouseDataProvider extends MouseDataProvider { + init(): void { + document.addEventListener('pointerlockchange', this.#onPointerLockChange); + document.addEventListener('pointerlockerror', this.#onPointerLockError); + } + + start(): void { + if (!document.pointerLockElement) { + document.body.requestPointerLock(); + } + + window.addEventListener('mousemove', this.#onMouseMoveEvent); + window.addEventListener('mousedown', this.#onMouseEvent); + window.addEventListener('mouseup', this.#onMouseEvent); + window.addEventListener('wheel', this.#onWheelEvent); + window.addEventListener('contextmenu', this.#disableContextMenu); + } + + stop(): void { + window.removeEventListener('mousemove', this.#onMouseMoveEvent); + window.removeEventListener('mousedown', this.#onMouseEvent); + window.removeEventListener('mouseup', this.#onMouseEvent); + window.removeEventListener('wheel', this.#onWheelEvent); + window.removeEventListener('contextmenu', this.#disableContextMenu); + } + + destroy(): void { + document.removeEventListener('pointerlockchange', this.#onPointerLockChange); + document.removeEventListener('pointerlockerror', this.#onPointerLockError); + } + + toggle(enabled: boolean): void { + enabled ? document.pointerLockElement && this.mkbHandler.start() : this.mkbHandler.stop(); + + if (enabled) { + !document.pointerLockElement && this.mkbHandler.waitForMouseData(true); + } else { + this.mkbHandler.waitForMouseData(false); + document.pointerLockElement && document.exitPointerLock(); + } + } + + #onPointerLockChange = () => { + if (this.mkbHandler.isEnabled() && !document.pointerLockElement) { + this.mkbHandler.stop(); + } + } + + #onPointerLockError = (e: Event) => { + console.log(e); + this.stop(); + } + + #onMouseMoveEvent = (e: MouseEvent) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY, + }); + } + + #onMouseEvent = (e: MouseEvent) => { + e.preventDefault(); + + const isMouseDown = e.type === 'mousedown'; + const key = KeyHelper.getKeyFromEvent(e); + const data: MkbMouseClick = { + key: key, + pressed: isMouseDown + }; + + this.mkbHandler.handleMouseClick(data); + } + + #onWheelEvent = (e: WheelEvent) => { + const key = KeyHelper.getKeyFromEvent(e); + if (!key) { + return; + } + + if (this.mkbHandler.handleMouseWheel({key})) { + e.preventDefault(); + } + } + + #disableContextMenu = (e: Event) => e.preventDefault(); +} + /* This class uses some code from Yuzu emulator to handle mouse's movements Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp @@ -33,7 +172,6 @@ export class MkbHandler { #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); static readonly DEFAULT_PANNING_SENSITIVITY = 0.0010; - static readonly DEFAULT_STICK_SENSITIVITY = 0.0006; static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; static readonly MAXIMUM_STICK_RANGE = 1.1; @@ -55,13 +193,13 @@ export class MkbHandler { #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); #enabled = false; + #mouseDataProvider: MouseDataProvider | undefined; #isPolling = false; #prevWheelCode = null; #wheelStoppedTimeout?: number | null; #detectMouseStoppedTimeout?: number | null; - #allowStickDecaying = false; #$message?: HTMLElement; @@ -85,6 +223,8 @@ export class MkbHandler { }; } + isEnabled = () => this.#enabled; + #patchedGetGamepads = () => { const gamepads = this.#nativeGetGamepads() || []; (gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD; @@ -102,6 +242,7 @@ export class MkbHandler { virtualGamepad.timestamp = performance.now(); } + /* #getStickAxes(stick: GamepadStick) { const virtualGamepad = this.#getVirtualGamepad(); return { @@ -109,11 +250,10 @@ export class MkbHandler { y: virtualGamepad.axes[stick * 2 + 1], }; } + */ #vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2); - #disableContextMenu = (e: Event) => e.preventDefault(); - #resetGamepad = () => { const gamepad = this.#getVirtualGamepad(); @@ -172,6 +312,10 @@ export class MkbHandler { e.preventDefault(); this.toggle(); return; + } else if (e.code === 'Escape') { + e.preventDefault(); + this.#enabled && this.stop(); + return; } if (!this.#isPolling) { @@ -179,7 +323,7 @@ export class MkbHandler { } } - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code]!; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]!; if (typeof buttonIndex === 'undefined') { return; } @@ -193,89 +337,29 @@ export class MkbHandler { this.#pressButton(buttonIndex, isKeyDown); } - #onMouseEvent = (e: MouseEvent) => { - const isMouseDown = e.type === 'mousedown'; - const key = KeyHelper.getKeyFromEvent(e); - if (!key) { - return; - } - - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!; - if (typeof buttonIndex === 'undefined') { - return; - } - - e.preventDefault(); - this.#pressButton(buttonIndex, isMouseDown); - } - - #onWheelEvent = (e: WheelEvent) => { - const key = KeyHelper.getKeyFromEvent(e); - if (!key) { - return; - } - - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!; - if (typeof buttonIndex === 'undefined') { - return; - } - - e.preventDefault(); - - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) { - this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout); - this.#pressButton(buttonIndex, true); - } - - this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null; - this.#pressButton(buttonIndex, false); - }, 20); - } - - #decayStick = () => { - if (!this.#allowStickDecaying) { - return; - } + #onMouseStopped = () => { + // Reset stick position + this.#detectMouseStoppedTimeout = null; const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO]; - if (mouseMapTo === MouseMapTo.OFF) { + const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT; + this.#updateStick(analog, 0, 0); + } + + handleMouseClick = (data: MkbMouseClick) => { + if (!data || !data.key) { return; } - const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT; - - let { x, y } = this.#getStickAxes(analog); - const length = this.#vectorLength(x, y); - - const clampedLength = Math.min(1.0, length); - const decayStrength = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]; - const decay = 1 - clampedLength * clampedLength * decayStrength; - const minDecay = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN]; - const clampedDecay = Math.min(1 - minDecay, decay); - - x *= clampedDecay; - y *= clampedDecay; - - const deadzoneCounterweight = 20 * MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - if (Math.abs(x) <= deadzoneCounterweight && Math.abs(y) <= deadzoneCounterweight) { - x = 0; - y = 0; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[data.key.code]!; + if (typeof buttonIndex === 'undefined') { + return; } - if (this.#allowStickDecaying) { - this.#updateStick(analog, x, y); - - (x !== 0 || y !== 0) && requestAnimationFrame(this.#decayStick); - } + this.#pressButton(buttonIndex, data.pressed); } - #onMouseStopped = () => { - this.#allowStickDecaying = true; - requestAnimationFrame(this.#decayStick); - } - - #onMouseMoveEvent = (e: MouseEvent) => { + handleMouseMove = (data: MkbMouseMove) => { // TODO: optimize this const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO]; if (mouseMapTo === MouseMapTo.OFF) { @@ -283,17 +367,13 @@ export class MkbHandler { return; } - this.#allowStickDecaying = false; this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout); - this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 10); - - const deltaX = e.movementX; - const deltaY = e.movementY; + this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]; - let x = deltaX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X]; - let y = deltaY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y]; + let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X]; + let y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y]; let length = this.#vectorLength(x, y); if (length !== 0 && length < deadzoneCounterweight) { @@ -308,18 +388,33 @@ export class MkbHandler { this.#updateStick(analog, x, y); } + handleMouseWheel = (data: MkbMouseWheel): boolean => { + if (!data || !data.key) { + return false; + } + + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[data.key.code]!; + if (typeof buttonIndex === 'undefined') { + return false; + } + + if (this.#prevWheelCode === null || this.#prevWheelCode === data.key.code) { + this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout); + this.#pressButton(buttonIndex, true); + } + + this.#wheelStoppedTimeout = window.setTimeout(() => { + this.#prevWheelCode = null; + this.#pressButton(buttonIndex, false); + }, 20); + + return true; + } + toggle = () => { this.#enabled = !this.#enabled; - this.#enabled ? document.pointerLockElement && this.start() : this.stop(); - Toast.show(t('mouse-and-keyboard'), t(this.#enabled ? 'enabled' : 'disabled'), {instant: true}); - - if (this.#enabled) { - !document.pointerLockElement && this.#waitForPointerLock(true); - } else { - this.#waitForPointerLock(false); - document.pointerLockElement && document.exitPointerLock(); - } + this.#mouseDataProvider?.toggle(this.#enabled); } #getCurrentPreset = (): Promise => { @@ -338,47 +433,35 @@ export class MkbHandler { }); } - #onPointerLockChange = () => { - if (this.#enabled && !document.pointerLockElement) { - this.stop(); - this.#waitForPointerLock(true); - } - } - - #onPointerLockError = (e: Event) => { - console.log(e); - this.stop(); - } - - #onActivatePointerLock = () => { - if (!document.pointerLockElement) { - document.body.requestPointerLock(); - } - - this.#waitForPointerLock(false); - this.start(); - } - - #waitForPointerLock = (wait: boolean) => { + waitForMouseData = (wait: boolean) => { this.#$message && this.#$message.classList.toggle('bx-gone', !wait); } - #onStreamMenuShown = () => { - this.#enabled && this.#waitForPointerLock(false); - } + #onPollingModeChanged = (e: Event) => { + if (!this.#$message) { + return; + } - #onStreamMenuHidden = () => { - this.#enabled && this.#waitForPointerLock(true); + const mode = (e as any).mode; + if (mode === 'None') { + this.#$message.classList.remove('bx-offscreen'); + } else { + this.#$message.classList.add('bx-offscreen'); + } } init = () => { this.refreshPresetData(); this.#enabled = true; - window.addEventListener('keydown', this.#onKeyboardEvent); + if (AppInterface) { + this.#mouseDataProvider = new WebSocketMouseDataProvider(this); + } else { + this.#mouseDataProvider = new PointerLockMouseDataProvider(this); + } + this.#mouseDataProvider.init(); - document.addEventListener('pointerlockchange', this.#onPointerLockChange); - document.addEventListener('pointerlockerror', this.#onPointerLockError); + window.addEventListener('keydown', this.#onKeyboardEvent); this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'}, createButton({ @@ -397,13 +480,12 @@ export class MkbHandler { ), ); - this.#$message.addEventListener('click', this.#onActivatePointerLock); + this.#$message.addEventListener('click', this.start.bind(this)); document.documentElement.appendChild(this.#$message); - window.addEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown); - window.addEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden); + window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); - this.#waitForPointerLock(true); + this.waitForMouseData(true); } destroy = () => { @@ -411,31 +493,31 @@ export class MkbHandler { this.#enabled = false; this.stop(); - this.#waitForPointerLock(false); + this.waitForMouseData(false); document.pointerLockElement && document.exitPointerLock(); window.removeEventListener('keydown', this.#onKeyboardEvent); - document.removeEventListener('pointerlockchange', this.#onPointerLockChange); - document.removeEventListener('pointerlockerror', this.#onPointerLockError); + this.#mouseDataProvider?.destroy(); - window.removeEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown); - window.removeEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); } start = () => { + if (!this.#enabled) { + this.#enabled = true; + Toast.show(t('mouse-and-keyboard'), t('enabled'), {instant: true}); + } + this.#isPolling = true; - window.navigator.getGamepads = this.#patchedGetGamepads; this.#resetGamepad(); + window.navigator.getGamepads = this.#patchedGetGamepads; + + this.waitForMouseData(false); window.addEventListener('keyup', this.#onKeyboardEvent); - - window.addEventListener('mousemove', this.#onMouseMoveEvent); - window.addEventListener('mousedown', this.#onMouseEvent); - window.addEventListener('mouseup', this.#onMouseEvent); - window.addEventListener('wheel', this.#onWheelEvent); - window.addEventListener('contextmenu', this.#disableContextMenu); + this.#mouseDataProvider?.start(); // Dispatch "gamepadconnected" event const virtualGamepad = this.#getVirtualGamepad(); @@ -451,6 +533,8 @@ export class MkbHandler { this.#isPolling = false; // Dispatch "gamepaddisconnected" event + this.#resetGamepad(); + const virtualGamepad = this.#getVirtualGamepad(); virtualGamepad.connected = false; virtualGamepad.timestamp = performance.now(); @@ -461,19 +545,14 @@ export class MkbHandler { window.navigator.getGamepads = this.#nativeGetGamepads; - this.#resetGamepad(); - window.removeEventListener('keyup', this.#onKeyboardEvent); - window.removeEventListener('mousemove', this.#onMouseMoveEvent); - window.removeEventListener('mousedown', this.#onMouseEvent); - window.removeEventListener('mouseup', this.#onMouseEvent); - window.removeEventListener('wheel', this.#onWheelEvent); - window.removeEventListener('contextmenu', this.#disableContextMenu); + this.waitForMouseData(true); + this.#mouseDataProvider?.stop(); } static setupEvents() { - getPref(PrefKey.MKB_ENABLED) && !UserAgent.isMobile() && window.addEventListener(BxEvent.STREAM_PLAYING, () => { + getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile()) && window.addEventListener(BxEvent.STREAM_PLAYING, () => { // Enable MKB if (!STATES.currentStream.titleInfo?.details.hasMkbSupport) { BxLogger.info(LOG_TAG, 'Emulate MKB'); diff --git a/src/modules/mkb/mkb-preset.ts b/src/modules/mkb/mkb-preset.ts index 42be4d1..5e0ad13 100644 --- a/src/modules/mkb/mkb-preset.ts +++ b/src/modules/mkb/mkb-preset.ts @@ -24,11 +24,11 @@ export class MkbPreset { type: SettingElementType.NUMBER_STEPPER, default: 50, min: 1, - max: 200, + max: 300, params: { suffix: '%', - exactTicks: 20, + exactTicks: 50, }, }, @@ -37,11 +37,11 @@ export class MkbPreset { type: SettingElementType.NUMBER_STEPPER, default: 50, min: 1, - max: 200, + max: 300, params: { suffix: '%', - exactTicks: 20, + exactTicks: 50, }, }, @@ -50,38 +50,13 @@ export class MkbPreset { type: SettingElementType.NUMBER_STEPPER, default: 20, min: 1, - max: 100, + max: 50, params: { suffix: '%', exactTicks: 10, }, }, - - [MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: { - label: t('stick-decay-strength'), - type: SettingElementType.NUMBER_STEPPER, - default: 100, - min: 10, - max: 100, - - params: { - suffix: '%', - exactTicks: 10, - }, - }, - - [MkbPresetKey.MOUSE_STICK_DECAY_MIN]: { - label: t('stick-decay-minimum'), - type: SettingElementType.NUMBER_STEPPER, - default: 10, - min: 1, - max: 10, - - params: { - suffix: '%', - }, - }, }; static DEFAULT_PRESET: MkbPresetData = { @@ -124,11 +99,9 @@ export class MkbPreset { 'mouse': { [MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS], - [MkbPresetKey.MOUSE_SENSITIVITY_X]: 50, - [MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50, + [MkbPresetKey.MOUSE_SENSITIVITY_X]: 100, + [MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100, [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20, - [MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 100, - [MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 10, }, }; @@ -149,8 +122,6 @@ export class MkbPreset { mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY; mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY; mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01; - mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01; const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]!]; if (typeof mouseMapTo !== 'undefined') { diff --git a/src/modules/mkb/pointer-client.ts b/src/modules/mkb/pointer-client.ts new file mode 100644 index 0000000..d0b5814 --- /dev/null +++ b/src/modules/mkb/pointer-client.ts @@ -0,0 +1,152 @@ +import { BxLogger } from "@/utils/bx-logger"; +import type { MkbHandler } from "./mkb-handler"; +import { KeyHelper } from "./key-helper"; +import { WheelCode } from "./definitions"; +import { Toast } from "@/utils/toast"; + +const LOG_TAG = 'PointerClient'; + +enum PointerAction { + MOVE = 1, + BUTTON_PRESS = 2, + BUTTON_RELEASE = 3, + SCROLL = 4, + POINTER_CAPTURE_CHANGED = 5, +} + +const FixedMouseIndex = { + 1: 0, + 2: 2, + 4: 1, +} + +export class PointerClient { + static #PORT = 9269; + + private static instance: PointerClient; + public static getInstance(): PointerClient { + if (!PointerClient.instance) { + PointerClient.instance = new PointerClient(); + } + + return PointerClient.instance; + } + + #socket: WebSocket | undefined | null; + #mkbHandler: MkbHandler | undefined; + + start(mkbHandler: MkbHandler) { + this.#mkbHandler = mkbHandler; + + // Create WebSocket connection. + this.#socket = new WebSocket(`ws://localhost:${PointerClient.#PORT}`); + this.#socket.binaryType = 'arraybuffer'; + + // Connection opened + this.#socket.addEventListener('open', (event) => { + BxLogger.info(LOG_TAG, 'connected') + }); + + // Error + this.#socket.addEventListener('error', (event) => { + BxLogger.error(LOG_TAG, event); + Toast.show('Cannot setup mouse'); + }); + + this.#socket.addEventListener('close', (event) => { + this.#socket = null; + }); + + // Listen for messages + this.#socket.addEventListener('message', (event) => { + const dataView = new DataView(event.data); + + let messageType = dataView.getInt8(0); + let offset = Int8Array.BYTES_PER_ELEMENT; + switch (messageType) { + case PointerAction.MOVE: + this.onMove(dataView, offset); + break; + + case PointerAction.BUTTON_PRESS: + case PointerAction.BUTTON_RELEASE: + this.onPress(messageType, dataView, offset); + break; + + case PointerAction.SCROLL: + this.onScroll(dataView, offset); + break; + + case PointerAction.POINTER_CAPTURE_CHANGED: + this.onPointerCaptureChanged(dataView, offset); + } + }); + } + + onMove(dataView: DataView, offset: number) { + // [X, Y] + const x = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const y = dataView.getInt16(offset); + + this.#mkbHandler?.handleMouseMove({ + movementX: x, + movementY: y, + }); + // BxLogger.info(LOG_TAG, 'move', x, y); + } + + onPress(messageType: PointerAction, dataView: DataView, offset: number) { + const buttonIndex = dataView.getInt8(offset); + const fixedIndex = FixedMouseIndex[buttonIndex as keyof typeof FixedMouseIndex]; + const keyCode = 'Mouse' + fixedIndex; + + this.#mkbHandler?.handleMouseClick({ + key: { + code: keyCode, + name: KeyHelper.codeToKeyName(keyCode), + }, + pressed: messageType === PointerAction.BUTTON_PRESS, + }); + + // BxLogger.info(LOG_TAG, 'press', buttonIndex); + } + + onScroll(dataView: DataView, offset: number) { + // [V_SCROLL, H_SCROLL] + const vScroll = dataView.getInt8(offset); + offset += Int8Array.BYTES_PER_ELEMENT; + const hScroll = dataView.getInt8(offset); + + let code = ''; + if (vScroll < 0) { + code = WheelCode.SCROLL_UP; + } else if (vScroll > 0) { + code = WheelCode.SCROLL_DOWN; + } else if (hScroll < 0) { + code = WheelCode.SCROLL_LEFT; + } else if (hScroll > 0) { + code = WheelCode.SCROLL_RIGHT; + } + + code && this.#mkbHandler?.handleMouseWheel({ + key: { + code: code, + name: KeyHelper.codeToKeyName(code), + }, + }); + + // BxLogger.info(LOG_TAG, 'scroll', vScroll, hScroll); + } + + onPointerCaptureChanged(dataView: DataView, offset: number) { + const hasCapture = dataView.getInt8(offset) === 1; + !hasCapture && this.#mkbHandler?.stop(); + } + + stop() { + try { + this.#socket?.close(); + } catch (e) {} + } +} diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index 0dc46e8..a788c50 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -407,7 +407,7 @@ e.guideUI = null; } const newCode = ` -window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged(e); +BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e}); `; str = str.replace(text, text + newCode); return str; diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts index 97b0420..1cdc112 100644 --- a/src/modules/stream/stream-ui.ts +++ b/src/modules/stream/stream-ui.ts @@ -105,12 +105,6 @@ export function injectStreamMenuButtons() { if (!($node as HTMLElement).className || !($node as HTMLElement).className.startsWith) { return; } - - if (($node as HTMLElement).className.startsWith('StreamMenu')) { - if (!document.querySelector('div[class^=PureInStreamConfirmationModal]')) { - BxEvent.dispatch(window, BxEvent.STREAM_MENU_HIDDEN); - } - } }); item.addedNodes.forEach(async $node => { @@ -139,8 +133,6 @@ export function injectStreamMenuButtons() { // Render badges if ($elm.className?.startsWith('StreamMenu-module__container')) { - BxEvent.dispatch(window, BxEvent.STREAM_MENU_SHOWN); - const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]'); if (!$btnCloseHud) { return; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index f533aea..65a791b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -74,3 +74,23 @@ type XcloudTitleInfo = { declare module '*.js'; declare module '*.svg'; declare module '*.styl'; + +type MkbMouseMove = { + movementX: number; + movementY: number; +} + +type MkbMouseClick = { + key: { + code: string; + name: string; + } | null; + pressed: boolean; +} + +type MkbMouseWheel = { + key: { + code: string; + name: string; + } | null; +} diff --git a/src/types/preferences.d.ts b/src/types/preferences.d.ts index e072301..33a4c29 100644 --- a/src/types/preferences.d.ts +++ b/src/types/preferences.d.ts @@ -6,7 +6,7 @@ export type PreferenceSetting = { note?: string | HTMLElement; type?: SettingElementType; ready?: (setting: PreferenceSetting) => void; - migrate?: (savedPrefs: any, value: any) => {}; + migrate?: (this: Preferences, savedPrefs: any, value: any) => void; min?: number; max?: number; steps?: number; diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index d804ed8..e10d014 100644 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -13,9 +13,6 @@ export enum BxEvent { STREAM_STOPPED = 'bx-stream-stopped', STREAM_ERROR_PAGE = 'bx-stream-error-page', - STREAM_MENU_SHOWN = 'bx-stream-menu-shown', - STREAM_MENU_HIDDEN = 'bx-stream-menu-hidden', - STREAM_WEBRTC_CONNECTED = 'bx-stream-webrtc-connected', STREAM_WEBRTC_DISCONNECTED = 'bx-stream-webrtc-disconnected', @@ -41,6 +38,8 @@ export enum BxEvent { // xCloud Dialog events XCLOUD_DIALOG_SHOWN = 'bx-xcloud-dialog-shown', XCLOUD_DIALOG_DISMISSED = 'bx-xcloud-dialog-dismissed', + + XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed', } export enum XcloudEvent { diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index 2545b62..41a498e 100644 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -16,23 +16,6 @@ export enum InputType { } export const BxExposed = { - // Enable/disable Game Bar when playing/pausing - onPollingModeChanged: (mode: 'All' | 'None') => { - if (getPref(PrefKey.GAME_BAR_POSITION) === 'off') { - return; - } - - const gameBar = GameBar.getInstance(); - - if (!STATES.isPlaying) { - gameBar.disable(); - return; - } - - // Toggle Game bar - mode !== 'None' ? gameBar.disable() : gameBar.enable(); - }, - getTitleInfo: () => STATES.currentStream.titleInfo, modifyTitleInfo: (titleInfo: XcloudTitleInfo): XcloudTitleInfo => { diff --git a/src/utils/monkey-patches.ts b/src/utils/monkey-patches.ts index 11bf256..82d4b64 100644 --- a/src/utils/monkey-patches.ts +++ b/src/utils/monkey-patches.ts @@ -103,7 +103,7 @@ export function patchRtcPeerConnection() { try { const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX); if (maxVideoBitrate > 0) { - arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, maxVideoBitrate * 1000); + arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); } } catch (e) { BxLogger.error('setLocalDescription', e); diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index 21750b7..9e88db8 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -4,7 +4,7 @@ import { SettingElement, SettingElementType } from "@utils/settings"; import { UserAgentProfile } from "@utils/user-agent"; import { StreamStat } from "@modules/stream/stream-stats"; import type { PreferenceSetting, PreferenceSettings } from "@/types/preferences"; -import { STATES } from "@utils/global"; +import { AppInterface, STATES } from "@utils/global"; export enum PrefKey { LAST_UPDATE_CHECK = 'version_last_check', @@ -325,21 +325,33 @@ export class Preferences { note: '⚠️ ' + t('unexpected-behavior'), default: 0, min: 0, - max: 14, - steps: 1, + max: 14 * 1024 * 1000, + steps: 100 * 1024, params: { - suffix: ' Mb/s', - exactTicks: 5, + exactTicks: 5 * 1024 * 1000, customTextValue: (value: any) => { value = parseInt(value); if (value === 0) { return t('unlimited'); + } else { + return (value / (1024 * 1000)).toFixed(1) + ' Mb/s'; } return null; }, }, + migrate: function(savedPrefs: any, value: any) { + try { + value = parseInt(value); + if (value < 100) { + value *= 1024 * 1000; + } + + this.set(PrefKey.BITRATE_VIDEO_MAX, value); + savedPrefs[PrefKey.BITRATE_VIDEO_MAX] = value; + } catch (e) {} + }, }, [PrefKey.GAME_BAR_POSITION]: { @@ -405,7 +417,7 @@ export class Preferences { default: false, unsupported: ((): string | boolean => { const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase(); - return userAgent.match(/(android|iphone|ipad)/) ? t('browser-unsupported-feature') : false; + return !AppInterface && userAgent.match(/(android|iphone|ipad)/) ? t('browser-unsupported-feature') : false; })(), ready: (setting: PreferenceSetting) => { let note;