Compare commits

...

11 Commits

Author SHA1 Message Date
0bf4c289db Bump version to 6.2.1 2025-01-16 20:37:00 +07:00
c8865bd8a0 Re-arrange patches 2025-01-16 20:26:15 +07:00
a2f062d9d5 Lite: remove LocalCoOpManager 2025-01-16 20:05:51 +07:00
b6d4c51ca9 Update dists 2025-01-16 16:49:08 +07:00
785df72972 Lite: hide unsupported features 2025-01-16 16:37:18 +07:00
48da8bc527 Update Remote Play dialog's styling 2025-01-16 07:14:52 +07:00
f9cf02b2da Fix the Y button in default MKB preset 2025-01-16 06:46:12 +07:00
77e0f2d8ba Lite: disable navigating using gamepad in Settings dialog 2025-01-16 06:45:12 +07:00
d05a68c470 Fix exception when viewing deviceCode page 2025-01-15 21:30:50 +07:00
153873e034 Reduce Virtual Controller's input latency 2025-01-08 21:16:07 +07:00
8d7fbf2804 Bump version to 6.2.0 2025-01-04 19:39:40 +07:00
26 changed files with 455 additions and 612 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 6.1.1 // @version 6.2.1
// ==/UserScript== // ==/UserScript==

File diff suppressed because one or more lines are too long

View File

@ -78,7 +78,7 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
flex: 1; flex: 1;
font-size: 1.2rem; font-size: 1.3rem;
font-weight: bold; font-weight: bold;
} }

View File

@ -1,23 +1,3 @@
.bx-remote-play-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
background: #1a1b1e;
border-radius: 10px;
width: 420px;
max-width: calc(100vw - 20px);
margin: 0 0 0 auto;
padding: 16px;
> .bx-button {
display: table;
margin: 0 0 0 auto;
}
}
.bx-remote-play-settings { .bx-remote-play-settings {
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 12px; padding-bottom: 12px;
@ -29,6 +9,7 @@
label { label {
flex: 1; flex: 1;
font-size: 14px;
p { p {
margin: 4px 0 0; margin: 4px 0 0;
@ -63,23 +44,24 @@
.bx-remote-play-device-info { .bx-remote-play-device-info {
flex: 1; flex: 1;
align-self: center;
padding: 4px 0; padding: 4px 0;
} }
.bx-remote-play-device-name { .bx-remote-play-device-name {
font-size: 20px; font-size: 14px;
font-weight: bold; font-weight: bold;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
.bx-remote-play-console-type { .bx-remote-play-console-type {
font-size: 12px; font-size: 8px;
background: #004c87; background: #004c87;
color: #fff; color: #fff;
display: inline-block; display: inline-block;
border-radius: 14px; border-radius: 8px;
padding: 2px 10px; padding: 2px 6px;
margin-left: 8px; margin-left: 8px;
vertical-align: middle; vertical-align: middle;
} }

View File

@ -164,7 +164,9 @@ document.addEventListener('readystatechange', e => {
if (STATES.isSignedIn) { if (STATES.isSignedIn) {
// Preload Remote Play // Preload Remote Play
if (isFullVersion()) {
RemotePlayManager.getInstance()?.initialize(); RemotePlayManager.getInstance()?.initialize();
}
} else { } else {
// Show Settings button in the header when not signed in // Show Settings button in the header when not signed in
window.setTimeout(HeaderSection.watchHeader, 2000); window.setTimeout(HeaderSection.watchHeader, 2000);
@ -234,8 +236,10 @@ BxEventBus.Stream.on('state.starting', () => {
}); });
BxEventBus.Stream.on('state.playing', payload => { BxEventBus.Stream.on('state.playing', payload => {
if (isFullVersion()) {
window.BX_STREAM_SETTINGS = StreamSettings.settings; window.BX_STREAM_SETTINGS = StreamSettings.settings;
StreamSettings.refreshAllSettings(); StreamSettings.refreshAllSettings();
}
STATES.isPlaying = true; STATES.isPlaying = true;
StreamUiHandler.observe(); StreamUiHandler.observe();
@ -357,10 +361,12 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
function main() { function main() {
GhPagesUtils.fetchLatestCommit(); GhPagesUtils.fetchLatestCommit();
if (isFullVersion()) {
if (getPref(PrefKey.NATIVE_MKB_MODE) !== NativeMkbMode.OFF) { if (getPref(PrefKey.NATIVE_MKB_MODE) !== NativeMkbMode.OFF) {
const customList = getPref(PrefKey.NATIVE_MKB_FORCED_GAMES); const customList = getPref(PrefKey.NATIVE_MKB_FORCED_GAMES);
BX_FLAGS.ForceNativeMkbTitles.push(...customList); BX_FLAGS.ForceNativeMkbTitles.push(...customList);
} }
}
StreamSettings.setup(); StreamSettings.setup();

View File

@ -19,6 +19,7 @@ import type { MkbConvertedPresetData } from "@/types/presets";
import { StreamSettings } from "@/utils/stream-settings"; import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions"; import { ShortcutAction } from "@/enums/shortcut-actions";
import { BxEventBus } from "@/utils/bx-event-bus"; import { BxEventBus } from "@/utils/bx-event-bus";
import { generateVirtualControllerMapping, toXcloudGamepadKey } from "@/utils/gamepad";
const PointerToMouseButton = { const PointerToMouseButton = {
1: 0, 1: 0,
@ -152,6 +153,8 @@ export class EmulatedMkbHandler extends MkbHandler {
}; };
private nativeGetGamepads: Navigator['getGamepads']; private nativeGetGamepads: Navigator['getGamepads'];
private xCloudGamepad: XcloudGamepad = generateVirtualControllerMapping(0);
private initialized = false; private initialized = false;
private enabled = false; private enabled = false;
private mouseDataProvider: MouseDataProvider | undefined; private mouseDataProvider: MouseDataProvider | undefined;
@ -171,16 +174,16 @@ export class EmulatedMkbHandler extends MkbHandler {
private popup: MkbPopup; private popup: MkbPopup;
private STICK_MAP: { [key in GamepadKey]?: [GamepadKey[], number, number] } = { private STICK_MAP: { [key in GamepadKey]?: [GamepadKey[], number] } = {
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1], [GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, -1],
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1], [GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 1],
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1], [GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1],
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1], [GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, -1],
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1], [GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, -1],
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1], [GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 1],
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1], [GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 1],
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1], [GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, -1],
}; };
private constructor() { private constructor() {
@ -205,11 +208,16 @@ export class EmulatedMkbHandler extends MkbHandler {
private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
private updateStick(stick: GamepadStick, x: number, y: number) { private updateStick(stick: GamepadStick, x: number, y: number) {
const virtualGamepad = this.getVirtualGamepad(); const gamepad = this.xCloudGamepad;
virtualGamepad.axes[stick * 2] = x; if (stick === GamepadStick.LEFT) {
virtualGamepad.axes[stick * 2 + 1] = y; gamepad.LeftThumbXAxis = x;
gamepad.LeftThumbYAxis = -y;
} else {
gamepad.RightThumbXAxis = x;
gamepad.RightThumbYAxis = -y;
}
virtualGamepad.timestamp = performance.now(); window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
} }
/* /*
@ -224,29 +232,20 @@ export class EmulatedMkbHandler extends MkbHandler {
private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2); private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
private resetGamepad() { resetXcloudGamepads() {
const gamepad = this.getVirtualGamepad(); const index = getPref(PrefKey.MKB_P1_SLOT) - 1;
// Reset axes this.xCloudGamepad = generateVirtualControllerMapping(0, {
gamepad.axes = [0, 0, 0, 0]; GamepadIndex: getPref(PrefKey.LOCAL_CO_OP_ENABLED) ? index : 0,
Dirty: true,
// Reset buttons });
for (const button of gamepad.buttons) { this.VIRTUAL_GAMEPAD.index = index;
button.pressed = false;
button.value = 0;
}
gamepad.timestamp = performance.now();
} }
private pressButton(buttonIndex: GamepadKey, pressed: boolean) { private pressButton(buttonIndex: GamepadKey, pressed: boolean) {
const virtualGamepad = this.getVirtualGamepad(); const xCloudKey = toXcloudGamepadKey(buttonIndex)!;
if (buttonIndex >= 100) { if (buttonIndex >= 100) {
let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!; let [valueArr]: [GamepadKey[], number] = this.STICK_MAP[buttonIndex]!;
valueArr = valueArr as number[];
axisIndex = axisIndex as number;
// Remove old index of the array // Remove old index of the array
for (let i = valueArr.length - 1; i >= 0; i--) { for (let i = valueArr.length - 1; i >= 0; i--) {
if (valueArr[i] === buttonIndex) { if (valueArr[i] === buttonIndex) {
@ -259,18 +258,19 @@ export class EmulatedMkbHandler extends MkbHandler {
let value; let value;
if (valueArr.length) { if (valueArr.length) {
// Get value of the last key of the axis // Get value of the last key of the axis
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number; value = this.STICK_MAP[valueArr[valueArr.length - 1]]![1] as number;
} else { } else {
value = 0; value = 0;
} }
virtualGamepad.axes[axisIndex] = value; // @ts-ignore
this.xCloudGamepad[xCloudKey] = value;
} else { } else {
virtualGamepad.buttons[buttonIndex].pressed = pressed; // @ts-ignore
virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
} }
virtualGamepad.timestamp = performance.now(); window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
} }
private onKeyboardEvent = (e: KeyboardEvent) => { private onKeyboardEvent = (e: KeyboardEvent) => {
@ -453,7 +453,7 @@ export class EmulatedMkbHandler extends MkbHandler {
refreshPresetData() { refreshPresetData() {
this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset; this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset;
this.resetGamepad(); this.resetXcloudGamepads();
} }
waitForMouseData(showPopup: boolean) { waitForMouseData(showPopup: boolean) {
@ -581,11 +581,6 @@ export class EmulatedMkbHandler extends MkbHandler {
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
} }
updateGamepadSlots() {
// Set gamepad slot
this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1;
}
start() { start() {
if (!this.enabled) { if (!this.enabled) {
this.enabled = true; this.enabled = true;
@ -595,8 +590,8 @@ export class EmulatedMkbHandler extends MkbHandler {
this.isPolling = true; this.isPolling = true;
this.escKeyDownTime = -1; this.escKeyDownTime = -1;
this.resetGamepad(); window.BX_EXPOSED.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED));
this.updateGamepadSlots(); this.resetXcloudGamepads();
window.navigator.getGamepads = this.patchedGetGamepads; window.navigator.getGamepads = this.patchedGetGamepads;
this.waitForMouseData(false); this.waitForMouseData(false);
@ -625,7 +620,7 @@ export class EmulatedMkbHandler extends MkbHandler {
const virtualGamepad = this.getVirtualGamepad(); const virtualGamepad = this.getVirtualGamepad();
if (virtualGamepad.connected) { if (virtualGamepad.connected) {
// Dispatch "gamepaddisconnected" event // Dispatch "gamepaddisconnected" event
this.resetGamepad(); this.resetXcloudGamepads();
virtualGamepad.connected = false; virtualGamepad.connected = false;
virtualGamepad.timestamp = performance.now(); virtualGamepad.timestamp = performance.now();

View File

@ -13,19 +13,7 @@ import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions"; import { ShortcutAction } from "@/enums/shortcut-actions";
import { NativeMkbMode } from "@/enums/pref-values"; import { NativeMkbMode } from "@/enums/pref-values";
import { BxEventBus } from "@/utils/bx-event-bus"; import { BxEventBus } from "@/utils/bx-event-bus";
import type { NativeMouseData, XcloudInputChannel } from "@/utils/gamepad";
type NativeMouseData = {
X: number,
Y: number,
Buttons: number,
WheelX: number,
WheelY: number,
Type?: 0, // 0: Relative, 1: Absolute
}
type XcloudInputSink = {
onMouseInput: (data: NativeMouseData) => void;
}
export class NativeMkbHandler extends MkbHandler { export class NativeMkbHandler extends MkbHandler {
private static instance: NativeMkbHandler | null | undefined; private static instance: NativeMkbHandler | null | undefined;
@ -54,7 +42,7 @@ export class NativeMkbHandler extends MkbHandler {
private mouseVerticalMultiply = 0; private mouseVerticalMultiply = 0;
private mouseHorizontalMultiply = 0; private mouseHorizontalMultiply = 0;
private inputSink: XcloudInputSink | undefined; private inputChannel: XcloudInputChannel | undefined;
private popup!: MkbPopup; private popup!: MkbPopup;
@ -114,7 +102,7 @@ export class NativeMkbHandler extends MkbHandler {
init() { init() {
this.pointerClient = PointerClient.getInstance(); this.pointerClient = PointerClient.getInstance();
this.inputSink = window.BX_EXPOSED.inputSink; this.inputChannel = window.BX_EXPOSED.inputChannel;
// Stop keyboard input at startup // Stop keyboard input at startup
this.updateInputConfigurationAsync(false); this.updateInputConfigurationAsync(false);
@ -274,7 +262,7 @@ export class NativeMkbHandler extends MkbHandler {
private sendMouseInput(data: NativeMouseData) { private sendMouseInput(data: NativeMouseData) {
data.Type = 0; // Relative data.Type = 0; // Relative
this.inputSink?.onMouseInput(data); this.inputChannel?.queueMouseInput(data);
} }
private resetMouseInput() { private resetMouseInput() {

View File

@ -643,15 +643,14 @@ true` + text;
return str; return str;
}, },
exposeInputSink(str: string) { exposeInputChannel(str: string) {
let text = 'this.controlChannel=null,this.inputChannel=null'; let index = str.indexOf('this.flushData=');
if (!str.includes(text)) { if (index < 0) {
return false; return false;
} }
const newCode = 'window.BX_EXPOSED.inputSink = this;'; const newCode = 'window.BX_EXPOSED.inputChannel = this,';
str = PatcherUtils.insertAt(str, index, newCode);
str = str.replace(text, newCode + text);
return str; return str;
}, },
@ -1120,7 +1119,6 @@ ${subsVar} = subs;
let PATCH_ORDERS = PatcherUtils.filterPatches([ let PATCH_ORDERS = PatcherUtils.filterPatches([
...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ ...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'enableNativeMkb', 'enableNativeMkb',
'exposeInputSink',
'disableAbsoluteMouse', 'disableAbsoluteMouse',
] : []), ] : []),
@ -1130,7 +1128,6 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [ ...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [
'setImageQuality', 'setImageQuality',
'setBackgroundImageQuality',
] : []), ] : []),
'modifyPreloadedState', 'modifyPreloadedState',
@ -1189,10 +1186,6 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'enableConsoleLogging', 'enableConsoleLogging',
'enableXcloudLogger', 'enableXcloudLogger',
] : []), ] : []),
...(blockSomeNotifications() ? [
'changeNotificationsSubscription',
] : []),
]); ]);
const hideSections = getPref(PrefKey.UI_HIDE_SECTIONS); const hideSections = getPref(PrefKey.UI_HIDE_SECTIONS);
@ -1202,12 +1195,22 @@ let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
hideSections.includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection', hideSections.includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
STATES.browser.capabilities.touch && hideSections.includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection', STATES.browser.capabilities.touch && hideSections.includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
hideSections.some(value => [UiSection.NATIVE_MKB, UiSection.MOST_POPULAR].includes(value)) && 'ignoreSiglSections', hideSections.some(value => [UiSection.NATIVE_MKB, UiSection.MOST_POPULAR].includes(value)) && 'ignoreSiglSections',
...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [
'setBackgroundImageQuality',
] : []),
...(blockSomeNotifications() ? [
'changeNotificationsSubscription',
] : []),
]); ]);
// Only when playing // Only when playing
// TODO: check this // TODO: check this
// @ts-ignore // @ts-ignore
let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
'exposeInputChannel',
'patchXcloudTitleInfo', 'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen', 'disableGamepadDisconnectedScreen',
'patchStreamHud', 'patchStreamHud',
@ -1377,6 +1380,7 @@ export class Patcher {
// Apply patched functions // Apply patched functions
if (modified) { if (modified) {
BX_FLAGS.Debug && console.time(LOG_TAG);
try { try {
chunkData[chunkId] = eval(patchedFuncStr); chunkData[chunkId] = eval(patchedFuncStr);
} catch (e: unknown) { } catch (e: unknown) {
@ -1384,6 +1388,7 @@ export class Patcher {
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr); BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr);
} }
} }
BX_FLAGS.Debug && console.timeEnd(LOG_TAG);
} }
// Save to cache // Save to cache

View File

@ -7,6 +7,6 @@ const { productId } = $param$;
// Remove controller icon // Remove controller icon
supportedInputIcons.shift(); supportedInputIcons.shift();
if (window.BX_EXPOSED.localCoOpManager.isSupported(productId)) { if (window.BX_EXPOSED.localCoOpManager!.isSupported(productId)) {
supportedInputIcons.push(window.BX_EXPOSED.createReactLocalCoOpIcon); supportedInputIcons.push(window.BX_EXPOSED.createReactLocalCoOpIcon);
} }

View File

@ -7,8 +7,8 @@ export class VirtualControllerShortcut {
return; return;
} }
const released = generateVirtualControllerMapping(); const released = generateVirtualControllerMapping(0);
const pressed = generateVirtualControllerMapping({ const pressed = generateVirtualControllerMapping(0, {
Nexus: 1, Nexus: 1,
VirtualPhysicality: 1024, // Home VirtualPhysicality: 1024, // Home
}); });

View File

@ -1,3 +1,5 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { GamepadKey } from "@/enums/gamepad"; import { GamepadKey } from "@/enums/gamepad";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler"; import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { BxEvent } from "@/utils/bx-event"; import { BxEvent } from "@/utils/bx-event";
@ -408,9 +410,6 @@ export class NavigationDialogManager {
BxEventBus.Script.emit('dialog.shown', {}); BxEventBus.Script.emit('dialog.shown', {});
// Stop xCloud's navigation polling
window.BX_EXPOSED.disableGamepadPolling = true;
// Lock scroll bar // Lock scroll bar
document.body.classList.add('bx-no-scroll'); document.body.classList.add('bx-no-scroll');
@ -437,11 +436,18 @@ export class NavigationDialogManager {
this.$container.addEventListener('keydown', this); this.$container.addEventListener('keydown', this);
// Start gamepad polling // Start gamepad polling
if (isFullVersion()) {
this.startGamepadPolling(); this.startGamepadPolling();
} }
}
hide() { hide() {
// Stop gamepad polling
if (isFullVersion()) {
this.clearGamepadHoldingInterval(); this.clearGamepadHoldingInterval();
this.stopGamepadPolling();
}
if (!this.isShowing()) { if (!this.isShowing()) {
return; return;
} }
@ -459,9 +465,6 @@ export class NavigationDialogManager {
// Remove event listeners // Remove event listeners
this.$container.removeEventListener('keydown', this); this.$container.removeEventListener('keydown', this);
// Stop gamepad polling
this.stopGamepadPolling();
// Remove current dialog and everything after it from dialogs stack // Remove current dialog and everything after it from dialogs stack
if (this.dialog) { if (this.dialog) {
const dialogIndex = this.dialogsStack.indexOf(this.dialog); const dialogIndex = this.dialogsStack.indexOf(this.dialog);
@ -473,9 +476,6 @@ export class NavigationDialogManager {
// Unmount dialog // Unmount dialog
this.unmountCurrentDialog(); this.unmountCurrentDialog();
// Enable xCloud's navigation polling
window.BX_EXPOSED.disableGamepadPolling = false;
// Show the last dialog in dialogs stack // Show the last dialog in dialogs stack
if (this.dialogsStack.length) { if (this.dialogsStack.length) {
this.dialogsStack[this.dialogsStack.length - 1].show(); this.dialogsStack[this.dialogsStack.length - 1].show();
@ -639,14 +639,18 @@ export class NavigationDialogManager {
} }
private startGamepadPolling() { private startGamepadPolling() {
this.stopGamepadPolling(); // Stop xCloud's navigation polling
window.BX_EXPOSED.disableGamepadPolling = true;
this.stopGamepadPolling();
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
} }
private stopGamepadPolling() { private stopGamepadPolling() {
this.gamepadLastStates = []; // Enable xCloud's navigation polling
window.BX_EXPOSED.disableGamepadPolling = false;
this.gamepadLastStates = [];
this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId); this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId);
this.gamepadPollingIntervalId = null; this.gamepadPollingIntervalId = null;
} }

View File

@ -32,7 +32,11 @@ export class RemotePlayDialog extends NavigationDialog {
} }
private setupDialog() { private setupDialog() {
const $fragment = CE('div', { class: 'bx-remote-play-container' }); const $fragment = CE('div', { class: 'bx-centered-dialog' },
CE('div', { class: 'bx-dialog-title' },
CE('p', false, t('remote-play')),
),
);
const $settingNote = CE('p', {}); const $settingNote = CE('p', {});

View File

@ -313,7 +313,7 @@ export class SettingsDialog extends NavigationDialog {
items: [ items: [
PrefKey.BLOCK_TRACKING, PrefKey.BLOCK_TRACKING,
], ],
}, { }, isFullVersion() && {
group: 'advanced', group: 'advanced',
label: t('advanced'), label: t('advanced'),
items: [ items: [
@ -495,24 +495,23 @@ export class SettingsDialog extends NavigationDialog {
}], }],
}]; }];
private readonly TAB_CONTROLLER_ITEMS: Array<SettingTabSection | HTMLElement | false> = [{ private readonly TAB_CONTROLLER_ITEMS: Array<SettingTabSection | HTMLElement | false> = isFullVersion() ? [{
group: 'controller', group: 'controller',
label: t('controller'), label: t('controller'),
helpUrl: 'https://better-xcloud.github.io/ingame-features/#controller', helpUrl: 'https://better-xcloud.github.io/ingame-features/#controller',
items: [ items: [
isFullVersion() && { {
pref: PrefKey.LOCAL_CO_OP_ENABLED, pref: PrefKey.LOCAL_CO_OP_ENABLED,
onChange: () => { BxExposed.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED)); }, onChange: () => { BxExposed.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED)); },
}, }, {
isFullVersion() && {
pref: PrefKey.CONTROLLER_POLLING_RATE, pref: PrefKey.CONTROLLER_POLLING_RATE,
onChange: () => StreamSettings.refreshControllerSettings(), onChange: () => StreamSettings.refreshControllerSettings(),
}, isFullVersion() && ($parent => { }, ($parent => {
$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this)); $parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));
})], })],
}, },
isFullVersion() && STATES.userAgent.capabilities.touch && { STATES.userAgent.capabilities.touch && {
group: 'touch-control', group: 'touch-control',
label: t('touch-controller'), label: t('touch-controller'),
items: [{ items: [{
@ -564,7 +563,9 @@ export class SettingsDialog extends NavigationDialog {
}); });
}, },
}], }],
}, isFullVersion() && STATES.browser.capabilities.deviceVibration && { },
STATES.browser.capabilities.deviceVibration && {
group: 'device', group: 'device',
label: t('device'), label: t('device'),
items: [{ items: [{
@ -577,22 +578,22 @@ export class SettingsDialog extends NavigationDialog {
unsupported: !STATES.browser.capabilities.deviceVibration, unsupported: !STATES.browser.capabilities.deviceVibration,
onChange: () => StreamSettings.refreshControllerSettings(), onChange: () => StreamSettings.refreshControllerSettings(),
}], }],
}]; }] : [];
private readonly TAB_MKB_ITEMS: (() => Array<SettingTabSection | false>) = () => [ private readonly TAB_MKB_ITEMS: (() => Array<SettingTabSection | false>) = isFullVersion() ? () => [
isFullVersion() && { {
requiredVariants: 'full', requiredVariants: 'full',
group: 'mkb', group: 'mkb',
label: t('mouse-and-keyboard'), label: t('mouse-and-keyboard'),
helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/', helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/',
items: [ items: [
isFullVersion() && (($parent: HTMLElement) => { ($parent: HTMLElement) => {
$parent.appendChild(MkbExtraSettings.renderSettings.apply(this)); $parent.appendChild(MkbExtraSettings.renderSettings.apply(this));
}) },
], ],
}, },
isFullVersion() && NativeMkbHandler.isAllowed() && { NativeMkbHandler.isAllowed() && {
requiredVariants: 'full', requiredVariants: 'full',
group: 'native-mkb', group: 'native-mkb',
label: t('native-mkb'), label: t('native-mkb'),
@ -607,7 +608,7 @@ export class SettingsDialog extends NavigationDialog {
NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);
}, },
}] : [], }] : [],
}]; }] : () => [];
private readonly TAB_STATS_ITEMS: Array<SettingTabSection | false> = [{ private readonly TAB_STATS_ITEMS: Array<SettingTabSection | false> = [{
group: 'stats', group: 'stats',

View File

@ -69,7 +69,7 @@ export class MkbExtraSettings extends HTMLElement {
createSettingRow( createSettingRow(
t('virtual-controller-slot'), t('virtual-controller-slot'),
SettingElement.fromPref(PrefKey.MKB_P1_SLOT, STORAGE.Global, () => { SettingElement.fromPref(PrefKey.MKB_P1_SLOT, STORAGE.Global, () => {
EmulatedMkbHandler.getInstance()?.updateGamepadSlots(); EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();
}), }),
), ),
] : []), ] : []),

View File

@ -1,3 +1,5 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { SCRIPT_VERSION } from "@utils/global"; import { SCRIPT_VERSION } from "@utils/global";
import { createButton, ButtonStyle, CE, isElementVisible } from "@utils/html"; import { createButton, ButtonStyle, CE, isElementVisible } from "@utils/html";
import { BxIcon } from "@utils/bx-icon"; import { BxIcon } from "@utils/bx-icon";
@ -14,7 +16,7 @@ export class HeaderSection {
public static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection()); public static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection());
private readonly LOG_TAG = 'HeaderSection'; private readonly LOG_TAG = 'HeaderSection';
private $btnRemotePlay: HTMLElement; private $btnRemotePlay: HTMLElement | null;
private $btnSettings: HTMLElement; private $btnSettings: HTMLElement;
private $buttonsWrapper: HTMLElement; private $buttonsWrapper: HTMLElement;
@ -24,6 +26,7 @@ export class HeaderSection {
constructor() { constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()'); BxLogger.info(this.LOG_TAG, 'constructor()');
if (isFullVersion()) {
this.$btnRemotePlay = createButton({ this.$btnRemotePlay = createButton({
classes: ['bx-header-remote-play-button', 'bx-gone'], classes: ['bx-header-remote-play-button', 'bx-gone'],
icon: BxIcon.REMOTE_PLAY, icon: BxIcon.REMOTE_PLAY,
@ -31,6 +34,9 @@ export class HeaderSection {
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR, style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
onClick: e => RemotePlayManager.getInstance()?.togglePopup(), onClick: e => RemotePlayManager.getInstance()?.togglePopup(),
}); });
} else {
this.$btnRemotePlay = null;
}
this.$btnSettings = createButton({ this.$btnSettings = createButton({
classes: ['bx-header-settings-button'], classes: ['bx-header-settings-button'],
@ -98,7 +104,7 @@ export class HeaderSection {
} }
showRemotePlayButton() { showRemotePlayButton() {
this.$btnRemotePlay.classList.remove('bx-gone'); this.$btnRemotePlay?.classList.remove('bx-gone');
} }
static watchHeader() { static watchHeader() {

View File

@ -5,6 +5,7 @@ import type { StreamSettings, type StreamSettingsData } from "@/utils/stream-set
import type { BxEvent } from "@/utils/bx-event"; import type { BxEvent } from "@/utils/bx-event";
import type { BxEventBus } from "@/utils/bx-event-bus"; import type { BxEventBus } from "@/utils/bx-event-bus";
import type { BxLogger } from "@/utils/bx-logger"; import type { BxLogger } from "@/utils/bx-logger";
import type { XcloudInputChannel } from "@/utils/gamepad";
export {}; export {};
@ -20,7 +21,7 @@ declare global {
closeAll: () => void; closeAll: () => void;
}; };
showStreamMenu: () => void; showStreamMenu: () => void;
inputSink: any; inputChannel: XcloudInputChannel | undefined;
streamSession: any; streamSession: any;
touchLayoutManager: any; touchLayoutManager: any;
}>; }>;

View File

@ -232,7 +232,7 @@ export const BxExposed = {
Patcher.patchPage(page); Patcher.patchPage(page);
} : () => {}, } : () => {},
localCoOpManager: LocalCoOpManager.getInstance(), localCoOpManager: isFullVersion() ? LocalCoOpManager.getInstance() : null,
reactCreateElement: function(...args: any[]) {}, reactCreateElement: function(...args: any[]) {},
createReactLocalCoOpIcon: isFullVersion() ? (attrs: any): any => { createReactLocalCoOpIcon: isFullVersion() ? (attrs: any): any => {

View File

@ -15,6 +15,7 @@ export function addCss() {
if (isLiteVersion()) { if (isLiteVersion()) {
// Hide Controller icon in Game tiles // Hide Controller icon in Game tiles
selectorToHide.push('div[class*=SupportedInputsBadge] svg:first-of-type'); selectorToHide.push('div[class*=SupportedInputsBadge] svg:first-of-type');
selectorToHide.push('div[class*=SupportedInputsBadge]:not(:has(:nth-child(2)))');
} }
// Hide "News" section // Hide "News" section

View File

@ -4,7 +4,21 @@ import { Toast } from "@utils/toast";
import { BxLogger } from "@utils/bx-logger"; import { BxLogger } from "@utils/bx-logger";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage"; import { getPref } from "./settings-storages/global-settings-storage";
import { GamepadKeyName, type GamepadKey } from "@/enums/gamepad"; import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
export type NativeMouseData = {
X: number,
Y: number,
Buttons: number,
WheelX: number,
WheelY: number,
Type?: 0, // 0: Relative, 1: Absolute
}
export type XcloudInputChannel = {
sendGamepadInput: (timestamp: number, gamepads: XcloudGamepad[]) => void;
queueMouseInput: (data: NativeMouseData) => void;
}
// Show a toast when connecting/disconecting controller // Show a toast when connecting/disconecting controller
export function showGamepadToast(gamepad: Gamepad) { export function showGamepadToast(gamepad: Gamepad) {
@ -59,9 +73,9 @@ export function hasGamepad() {
return false; return false;
} }
export function generateVirtualControllerMapping(override: {}={}) { export function generateVirtualControllerMapping(index: number, override: Partial<XcloudGamepad>={}) {
const mapping = { const mapping = {
GamepadIndex: 0, GamepadIndex: index,
A: 0, A: 0,
B: 0, B: 0,
X: 0, X: 0,
@ -95,3 +109,44 @@ export function generateVirtualControllerMapping(override: {}={}) {
export function getGamepadPrompt(gamepadKey: GamepadKey): string { export function getGamepadPrompt(gamepadKey: GamepadKey): string {
return GamepadKeyName[gamepadKey][1]; return GamepadKeyName[gamepadKey][1];
} }
const XCLOUD_GAMEPAD_KEY_MAPPING: { [key in GamepadKey]?: keyof XcloudGamepad } = {
[GamepadKey.A]: 'A',
[GamepadKey.B]: 'B',
[GamepadKey.X]: 'X',
[GamepadKey.Y]: 'Y',
[GamepadKey.UP]: 'DPadUp',
[GamepadKey.RIGHT]: 'DPadRight',
[GamepadKey.DOWN]: 'DPadDown',
[GamepadKey.LEFT]: 'DPadLeft',
[GamepadKey.LB]: 'LeftShoulder',
[GamepadKey.RB]: 'RightShoulder',
[GamepadKey.LT]: 'LeftTrigger',
[GamepadKey.RT]: 'RightTrigger',
[GamepadKey.L3]: 'LeftThumb',
[GamepadKey.R3]: 'RightThumb',
[GamepadKey.LS]: 'LeftStickAxes',
[GamepadKey.RS]: 'RightStickAxes',
[GamepadKey.SELECT]: 'View',
[GamepadKey.START]: 'Menu',
[GamepadKey.HOME]: 'Nexus',
[GamepadKey.SHARE]: 'Share',
[GamepadKey.LS_LEFT]: 'LeftThumbXAxis',
[GamepadKey.LS_RIGHT]: 'LeftThumbXAxis',
[GamepadKey.LS_UP]: 'LeftThumbYAxis',
[GamepadKey.LS_DOWN]: 'LeftThumbYAxis',
[GamepadKey.RS_LEFT]: 'RightThumbXAxis',
[GamepadKey.RS_RIGHT]: 'RightThumbXAxis',
[GamepadKey.RS_UP]: 'RightThumbYAxis',
[GamepadKey.RS_DOWN]: 'RightThumbYAxis',
};
export function toXcloudGamepadKey(gamepadKey: GamepadKey) {
return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
}

View File

@ -1,3 +1,5 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { BxEvent } from "@utils/bx-event"; import { BxEvent } from "@utils/bx-event";
import { LoadingScreen } from "@modules/loading-screen"; import { LoadingScreen } from "@modules/loading-screen";
import { RemotePlayManager } from "@/modules/remote-play-manager"; import { RemotePlayManager } from "@/modules/remote-play-manager";
@ -25,7 +27,9 @@ export function onHistoryChanged(e: PopStateEvent) {
return; return;
} }
if (isFullVersion()) {
window.setTimeout(RemotePlayManager.detect, 10); window.setTimeout(RemotePlayManager.detect, 10);
}
// Hide Navigation dialog // Hide Navigation dialog
NavigationDialogManager.getInstance().hide(); NavigationDialogManager.getInstance().hide();

View File

@ -46,7 +46,7 @@ export class MkbMappingPresetsTable extends BasePresetsTable<MkbPresetRecord> {
[GamepadKey.A]: ['Space', 'KeyE'], [GamepadKey.A]: ['Space', 'KeyE'],
[GamepadKey.X]: ['KeyR'], [GamepadKey.X]: ['KeyR'],
[GamepadKey.B]: ['KeyC', 'Backspace'], [GamepadKey.B]: ['KeyC', 'Backspace'],
[GamepadKey.Y]: ['KeyE'], [GamepadKey.Y]: ['KeyV'],
[GamepadKey.START]: ['Enter'], [GamepadKey.START]: ['Enter'],
[GamepadKey.SELECT]: ['Tab'], [GamepadKey.SELECT]: ['Tab'],

View File

@ -221,10 +221,14 @@ export function interceptHttpRequests() {
} }
// Ignore domains // Ignore domains
try {
const domain = (new URL(url)).hostname; const domain = (new URL(url)).hostname;
if (IGNORED_DOMAINS.includes(domain)) { if (IGNORED_DOMAINS.includes(domain)) {
return NATIVE_FETCH(request, init); return NATIVE_FETCH(request, init);
} }
} catch (e) {
return NATIVE_FETCH(request, init);
}
// BxLogger.info('fetch', url); // BxLogger.info('fetch', url);

View File

@ -194,6 +194,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
default: false, default: false,
}, },
[PrefKey.UI_IMAGE_QUALITY]: { [PrefKey.UI_IMAGE_QUALITY]: {
requiredVariants: 'full',
label: t('image-quality'), label: t('image-quality'),
default: 90, default: 90,
min: 10, min: 10,
@ -617,6 +618,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
default: false, default: false,
}, },
[PrefKey.BLOCK_FEATURES]: { [PrefKey.BLOCK_FEATURES]: {
requiredVariants: 'full',
label: t('disable-features'), label: t('disable-features'),
default: [], default: [],
multipleOptions: { multipleOptions: {

View File

@ -6,7 +6,7 @@ import type { ControllerCustomizationConvertedPresetData, ControllerCustomizatio
import { STATES } from "./global"; import { STATES } from "./global";
import { DeviceVibrationMode } from "@/enums/pref-values"; import { DeviceVibrationMode } from "@/enums/pref-values";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler"; import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { hasGamepad } from "./gamepad"; import { hasGamepad, toXcloudGamepadKey } from "./gamepad";
import { MkbMappingPresetsTable } from "./local-db/mkb-mapping-presets-table"; import { MkbMappingPresetsTable } from "./local-db/mkb-mapping-presets-table";
import { GamepadKey } from "@/enums/gamepad"; import { GamepadKey } from "@/enums/gamepad";
import { MkbPresetKey, MouseConstant } from "@/enums/mkb"; import { MkbPresetKey, MouseConstant } from "@/enums/mkb";
@ -51,32 +51,6 @@ export class StreamSettings {
keyboardShortcuts: {}, keyboardShortcuts: {},
}; };
private static CONTROLLER_CUSTOMIZATION_MAPPING: { [key in GamepadKey]?: keyof XcloudGamepad } = {
[GamepadKey.A]: 'A',
[GamepadKey.B]: 'B',
[GamepadKey.X]: 'X',
[GamepadKey.Y]: 'Y',
[GamepadKey.UP]: 'DPadUp',
[GamepadKey.RIGHT]: 'DPadRight',
[GamepadKey.DOWN]: 'DPadDown',
[GamepadKey.LEFT]: 'DPadLeft',
[GamepadKey.LB]: 'LeftShoulder',
[GamepadKey.RB]: 'RightShoulder',
[GamepadKey.LT]: 'LeftTrigger',
[GamepadKey.RT]: 'RightTrigger',
[GamepadKey.L3]: 'LeftThumb',
[GamepadKey.R3]: 'RightThumb',
[GamepadKey.LS]: 'LeftStickAxes',
[GamepadKey.RS]: 'RightStickAxes',
[GamepadKey.SELECT]: 'View',
[GamepadKey.START]: 'Menu',
[GamepadKey.SHARE]: 'Share',
};
static getPref<T extends keyof PrefTypeMap>(key: T) { static getPref<T extends keyof PrefTypeMap>(key: T) {
return getPref<T>(key); return getPref<T>(key);
} }
@ -146,14 +120,14 @@ export class StreamSettings {
// Swap GamepadKey.A with "A" // Swap GamepadKey.A with "A"
let gamepadKey: unknown; let gamepadKey: unknown;
for (gamepadKey in customization.mapping) { for (gamepadKey in customization.mapping) {
const gamepadStr = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[gamepadKey as GamepadKey]; const gamepadStr = toXcloudGamepadKey(gamepadKey as GamepadKey);
if (!gamepadStr) { if (!gamepadStr) {
continue; continue;
} }
const mappedKey = customization.mapping[gamepadKey as GamepadKey]; const mappedKey = customization.mapping[gamepadKey as GamepadKey];
if (typeof mappedKey === 'number') { if (typeof mappedKey === 'number') {
converted.mapping[gamepadStr] = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[mappedKey as GamepadKey]; converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey as GamepadKey);
} else { } else {
converted.mapping[gamepadStr] = false; converted.mapping[gamepadStr] = false;
} }

View File

@ -59,7 +59,9 @@ export class XcloudInterceptor {
const obj = await response.clone().json(); const obj = await response.clone().json();
// Store xCloud token // Store xCloud token
if (isFullVersion()) {
RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);
}
// Get server list // Get server list
const serverRegex = /\/\/(\w+)\./; const serverRegex = /\/\/(\w+)\./;