mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 07:37:19 +02:00
Use a better method to enable touch control for all games
This commit is contained in:
parent
102a4657f0
commit
cd6dd1e22d
@ -124,10 +124,11 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => {
|
|||||||
// Setup UI
|
// Setup UI
|
||||||
setupBxUi();
|
setupBxUi();
|
||||||
|
|
||||||
// Setup loading screen
|
|
||||||
getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.setup();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup loading screen
|
||||||
|
getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup);
|
||||||
|
|
||||||
window.addEventListener(BxEvent.STREAM_STARTING, e => {
|
window.addEventListener(BxEvent.STREAM_STARTING, e => {
|
||||||
// Hide loading screen
|
// Hide loading screen
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { CE } from "../utils/html";
|
import { CE } from "../utils/html";
|
||||||
import { getPreferredServerRegion } from "../utils/region";
|
import { getPreferredServerRegion } from "../utils/region";
|
||||||
import { TitlesInfo } from "../utils/titles-info";
|
|
||||||
import { PrefKey, getPref } from "../utils/preferences";
|
import { PrefKey, getPref } from "../utils/preferences";
|
||||||
import { t } from "../utils/translation";
|
import { t } from "../utils/translation";
|
||||||
|
import { STATES } from "../utils/global";
|
||||||
|
|
||||||
export class LoadingScreen {
|
export class LoadingScreen {
|
||||||
static #$bgStyle: HTMLElement;
|
static #$bgStyle: HTMLElement;
|
||||||
@ -21,9 +21,8 @@ export class LoadingScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static setup() {
|
static setup() {
|
||||||
// Get titleId from location
|
const titleInfo = STATES.currentStream.titleInfo;
|
||||||
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
if (!titleInfo) {
|
||||||
if (!match) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,15 +32,7 @@ export class LoadingScreen {
|
|||||||
LoadingScreen.#$bgStyle = $bgStyle;
|
LoadingScreen.#$bgStyle = $bgStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleId = match[1];
|
LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl);
|
||||||
const titleInfo = TitlesInfo.get(titleId);
|
|
||||||
if (titleInfo && titleInfo.imageHero) {
|
|
||||||
LoadingScreen.#setBackground(titleInfo.imageHero);
|
|
||||||
} else {
|
|
||||||
TitlesInfo.requestCatalogInfo(titleId, (info: TitleInfo) => {
|
|
||||||
info && info.imageHero && LoadingScreen.#setBackground(info.imageHero);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') {
|
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') {
|
||||||
LoadingScreen.#hideRocket();
|
LoadingScreen.#hideRocket();
|
||||||
|
@ -332,14 +332,14 @@ const gamepads = window.navigator.getGamepads();
|
|||||||
let gamepadFound = false;
|
let gamepadFound = false;
|
||||||
|
|
||||||
for (let gamepad of gamepads) {
|
for (let gamepad of gamepads) {
|
||||||
if (gamepad && gamepad.connected) {
|
if (gamepad && gamepad.connected) {
|
||||||
gamepadFound = true;
|
gamepadFound = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gamepadFound) {
|
if (gamepadFound) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -386,6 +386,24 @@ window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged
|
|||||||
str = str.replace(text, text + newCode);
|
str = str.replace(text, text + newCode);
|
||||||
return str;
|
return str;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
patchXcloudTitleInfo(str: string) {
|
||||||
|
const text = 'async cloudConnect';
|
||||||
|
let index = str.indexOf(text);
|
||||||
|
if (index === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the next "{" backet
|
||||||
|
index = str.indexOf('{', index) + 1;
|
||||||
|
|
||||||
|
const newCode = `
|
||||||
|
e = window.BX_EXPOSED.modifyTitleInfo(e);
|
||||||
|
console.log(e);
|
||||||
|
`;
|
||||||
|
str = str.substring(0, index) + newCode + str.substring(index);
|
||||||
|
return str;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let PATCH_ORDERS = [
|
let PATCH_ORDERS = [
|
||||||
@ -432,6 +450,8 @@ let PATCH_ORDERS = [
|
|||||||
|
|
||||||
// Only when playing
|
// Only when playing
|
||||||
const PLAYING_PATCH_ORDERS = [
|
const PLAYING_PATCH_ORDERS = [
|
||||||
|
['patchXcloudTitleInfo'],
|
||||||
|
|
||||||
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'],
|
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'],
|
||||||
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'],
|
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'],
|
||||||
|
|
||||||
|
15
src/types/index.d.ts
vendored
15
src/types/index.d.ts
vendored
@ -31,6 +31,7 @@ type BxStates = {
|
|||||||
titleId: string;
|
titleId: string;
|
||||||
xboxTitleId: string;
|
xboxTitleId: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
|
titleInfo: XcloudTitleInfo;
|
||||||
|
|
||||||
$video: HTMLVideoElement | null;
|
$video: HTMLVideoElement | null;
|
||||||
$screenshotCanvas: HTMLCanvasElement | null;
|
$screenshotCanvas: HTMLCanvasElement | null;
|
||||||
@ -50,3 +51,17 @@ type BxStates = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DualEnum = {[index: string]: number} & {[index: number]: string};
|
type DualEnum = {[index: string]: number} & {[index: number]: string};
|
||||||
|
|
||||||
|
type XcloudTitleInfo = {
|
||||||
|
details: {
|
||||||
|
productId: string;
|
||||||
|
supportedInputTypes: InputType[];
|
||||||
|
hasTouchSupport: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
product: {
|
||||||
|
heroImageUrl: string;
|
||||||
|
titledHeroImageUrl: string;
|
||||||
|
tileImageUrl: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -4,6 +4,8 @@ export enum BxEvent {
|
|||||||
JUMP_BACK_IN_READY = 'bx-jump-back-in-ready',
|
JUMP_BACK_IN_READY = 'bx-jump-back-in-ready',
|
||||||
POPSTATE = 'bx-popstate',
|
POPSTATE = 'bx-popstate',
|
||||||
|
|
||||||
|
TITLE_INFO_READY = 'bx-title-info-ready',
|
||||||
|
|
||||||
STREAM_LOADING = 'bx-stream-loading',
|
STREAM_LOADING = 'bx-stream-loading',
|
||||||
STREAM_STARTING = 'bx-stream-starting',
|
STREAM_STARTING = 'bx-stream-starting',
|
||||||
STREAM_STARTED = 'bx-stream-started',
|
STREAM_STARTED = 'bx-stream-started',
|
||||||
|
@ -1,4 +1,16 @@
|
|||||||
|
import { BxEvent } from "./bx-event";
|
||||||
import { STATES } from "./global";
|
import { STATES } from "./global";
|
||||||
|
import { getPref, PrefKey } from "./preferences";
|
||||||
|
import { UserAgent } from "./user-agent";
|
||||||
|
|
||||||
|
enum InputType {
|
||||||
|
CONTROLLER = 'Controller',
|
||||||
|
MKB = 'MKB',
|
||||||
|
CUSTOM_TOUCH_OVERLAY = 'CustomTouchOverlay',
|
||||||
|
GENERIC_TOUCH = 'GenericTouch',
|
||||||
|
NATIVE_TOUCH = 'NativeTouch',
|
||||||
|
BATIVE_SENSOR = 'NativeSensor',
|
||||||
|
}
|
||||||
|
|
||||||
export const BxExposed = {
|
export const BxExposed = {
|
||||||
onPollingModeChanged: (mode: 'All' | 'None') => {
|
onPollingModeChanged: (mode: 'All' | 'None') => {
|
||||||
@ -23,4 +35,35 @@ export const BxExposed = {
|
|||||||
$touchControllerBar && $touchControllerBar.classList.remove('bx-gone');
|
$touchControllerBar && $touchControllerBar.classList.remove('bx-gone');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
modifyTitleInfo: (titleInfo: XcloudTitleInfo): XcloudTitleInfo => {
|
||||||
|
// Clone the object since the original is read-only
|
||||||
|
titleInfo = structuredClone(titleInfo);
|
||||||
|
|
||||||
|
const touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
|
||||||
|
|
||||||
|
let supportedInputTypes = titleInfo.details.supportedInputTypes;
|
||||||
|
|
||||||
|
// Remove MKB support on mobile browsers
|
||||||
|
if (UserAgent.isMobile()) {
|
||||||
|
supportedInputTypes = supportedInputTypes.filter(i => i !== 'MKB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom property
|
||||||
|
titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) &&
|
||||||
|
!supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) &&
|
||||||
|
!supportedInputTypes.includes(InputType.GENERIC_TOUCH);
|
||||||
|
|
||||||
|
// Add generic touch support for non touch-supported games
|
||||||
|
if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === 'all') {
|
||||||
|
supportedInputTypes.push(InputType.GENERIC_TOUCH);
|
||||||
|
}
|
||||||
|
titleInfo.details.supportedInputTypes = supportedInputTypes;
|
||||||
|
|
||||||
|
// Save this info in STATES
|
||||||
|
STATES.currentStream.titleInfo = titleInfo;
|
||||||
|
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
|
||||||
|
|
||||||
|
return titleInfo;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,6 @@ import { StreamBadges } from "../modules/stream/stream-badges";
|
|||||||
import { TouchController } from "../modules/touch-controller";
|
import { TouchController } from "../modules/touch-controller";
|
||||||
import { STATES } from "./global";
|
import { STATES } from "./global";
|
||||||
import { getPreferredServerRegion } from "./region";
|
import { getPreferredServerRegion } from "./region";
|
||||||
import { TitlesInfo } from "./titles-info";
|
|
||||||
|
|
||||||
export const NATIVE_FETCH = window.fetch;
|
export const NATIVE_FETCH = window.fetch;
|
||||||
|
|
||||||
@ -402,14 +401,11 @@ class XcloudInterceptor {
|
|||||||
|
|
||||||
// Touch controller for all games
|
// Touch controller for all games
|
||||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
|
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
|
||||||
TouchController.disable();
|
const titleInfo = STATES.currentStream.titleInfo;
|
||||||
|
if (titleInfo?.details.hasTouchSupport) {
|
||||||
// Get game ID from window.location
|
TouchController.disable();
|
||||||
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
} else {
|
||||||
// Check touch support
|
TouchController.enable();
|
||||||
if (match) {
|
|
||||||
const titleId = match[1];
|
|
||||||
!TitlesInfo.hasTouchSupport(titleId) && TouchController.enable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,30 +442,6 @@ class XcloudInterceptor {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #handleCatalog(request: RequestInfo | URL, init?: RequestInit) {
|
|
||||||
const response = await NATIVE_FETCH(request, init);
|
|
||||||
const json = await response.clone().json()
|
|
||||||
|
|
||||||
for (let productId in json.Products) {
|
|
||||||
TitlesInfo.saveFromCatalogInfo(json.Products[productId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async #handleTitles(request: RequestInfo | URL, init?: RequestInit) {
|
|
||||||
const response = await NATIVE_FETCH(request, init);
|
|
||||||
|
|
||||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
|
|
||||||
const json = await response.clone().json()
|
|
||||||
for (let game of json.results) {
|
|
||||||
TitlesInfo.saveFromTitleInfo(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async handle(request: RequestInfo | URL, init?: RequestInit) {
|
static async handle(request: RequestInfo | URL, init?: RequestInit) {
|
||||||
let url = (typeof request === 'string') ? request : (request as Request).url;
|
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||||
|
|
||||||
@ -482,10 +454,6 @@ class XcloudInterceptor {
|
|||||||
return XcloudInterceptor.#handleWaitTime(request, init);
|
return XcloudInterceptor.#handleWaitTime(request, init);
|
||||||
} else if (url.endsWith('/configuration')) {
|
} else if (url.endsWith('/configuration')) {
|
||||||
return XcloudInterceptor.#handleConfiguration(request, init);
|
return XcloudInterceptor.#handleConfiguration(request, init);
|
||||||
} else if (url.startsWith('https://catalog.gamepass.com') && url.includes('/products')) {
|
|
||||||
return XcloudInterceptor.#handleCatalog(request, init);
|
|
||||||
} else if (url.includes('/v2/titles') || url.includes('/mru')) {
|
|
||||||
return XcloudInterceptor.#handleTitles(request, init);
|
|
||||||
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
|
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
|
||||||
return patchIceCandidates(request as Request);
|
return patchIceCandidates(request as Request);
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,6 @@
|
|||||||
import { PrefKey } from "./preferences";
|
|
||||||
import { getPref } from "./preferences";
|
|
||||||
import { STATES } from "./global";
|
import { STATES } from "./global";
|
||||||
import { UserAgent } from "./user-agent";
|
import { UserAgent } from "./user-agent";
|
||||||
|
|
||||||
export class TitlesInfo {
|
|
||||||
static #INFO: {[index: string]: TitleInfo} = {};
|
|
||||||
|
|
||||||
static get(titleId: string) {
|
|
||||||
return TitlesInfo.#INFO[titleId];
|
|
||||||
}
|
|
||||||
|
|
||||||
static update(titleId: string, info: TitleInfo) {
|
|
||||||
TitlesInfo.#INFO[titleId] = TitlesInfo.#INFO[titleId] || {};
|
|
||||||
Object.assign(TitlesInfo.#INFO[titleId], info);
|
|
||||||
}
|
|
||||||
|
|
||||||
static saveFromTitleInfo(titleInfo: ApiTitleInfo) {
|
|
||||||
const details = titleInfo.details;
|
|
||||||
const info: TitleInfo = {
|
|
||||||
titleId: titleInfo.titleId,
|
|
||||||
xboxTitleId: '' + details.xboxTitleId,
|
|
||||||
// Has more than one input type -> must have touch support
|
|
||||||
hasTouchSupport: (details.supportedInputTypes.length > 1),
|
|
||||||
};
|
|
||||||
TitlesInfo.update(details.productId, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
static saveFromCatalogInfo(catalogInfo: ApiCatalogInfo) {
|
|
||||||
const titleId = catalogInfo.StoreId;
|
|
||||||
const imageHero = (catalogInfo.Image_Hero || catalogInfo.Image_Tile || {}).URL;
|
|
||||||
TitlesInfo.update(titleId, {
|
|
||||||
imageHero: imageHero,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static hasTouchSupport(titleId: string): boolean {
|
|
||||||
return !!TitlesInfo.#INFO[titleId]?.hasTouchSupport;
|
|
||||||
}
|
|
||||||
|
|
||||||
static requestCatalogInfo(titleId: string, callback: any) {
|
|
||||||
const url = `https://catalog.gamepass.com/v3/products?market=${STATES.appContext.marketInfo.market}&language=${STATES.appContext.marketInfo.locale}&hydration=RemoteHighSapphire0`;
|
|
||||||
const appVersion = document.querySelector('meta[name=gamepass-app-version]')!.getAttribute('content');
|
|
||||||
|
|
||||||
fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Ms-Cv': STATES.appContext.telemetryInfo.initialCv,
|
|
||||||
'Calling-App-Name': 'Xbox Cloud Gaming Web',
|
|
||||||
'Calling-App-Version': appVersion,
|
|
||||||
} as any,
|
|
||||||
body: JSON.stringify({
|
|
||||||
Products: [titleId],
|
|
||||||
}),
|
|
||||||
}).then(resp => {
|
|
||||||
callback && callback(TitlesInfo.get(titleId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class PreloadedState {
|
export class PreloadedState {
|
||||||
static override() {
|
static override() {
|
||||||
@ -76,18 +18,6 @@ export class PreloadedState {
|
|||||||
set: state => {
|
set: state => {
|
||||||
(this as any)._state = state;
|
(this as any)._state = state;
|
||||||
STATES.appContext = structuredClone(state.appContext);
|
STATES.appContext = structuredClone(state.appContext);
|
||||||
|
|
||||||
// Get a list of touch-supported games
|
|
||||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
|
|
||||||
let titles: {[index: string]: any} = {};
|
|
||||||
try {
|
|
||||||
titles = state.xcloud.titles.data.titles;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
for (let id in titles) {
|
|
||||||
TitlesInfo.saveFromTitleInfo(titles[id].data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export class UserAgent {
|
|||||||
return (UserAgent.#USER_AGENTS as any)[profile] || defaultUserAgent;
|
return (UserAgent.#USER_AGENTS as any)[profile] || defaultUserAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
static isSafari(mobile=false) {
|
static isSafari(mobile=false): boolean {
|
||||||
const userAgent = (UserAgent.getDefault() || '').toLowerCase();
|
const userAgent = (UserAgent.getDefault() || '').toLowerCase();
|
||||||
let result = userAgent.includes('safari') && !userAgent.includes('chrom');
|
let result = userAgent.includes('safari') && !userAgent.includes('chrom');
|
||||||
|
|
||||||
@ -54,6 +54,11 @@ export class UserAgent {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isMobile(): boolean {
|
||||||
|
const userAgent = (UserAgent.getDefault() || '').toLowerCase();
|
||||||
|
return /iphone|ipad|android/.test(userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
static spoof() {
|
static spoof() {
|
||||||
let newUserAgent;
|
let newUserAgent;
|
||||||
|
|
||||||
@ -67,6 +72,7 @@ export class UserAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear data of navigator.userAgentData, force xCloud to detect browser based on navigator.userAgent
|
// Clear data of navigator.userAgentData, force xCloud to detect browser based on navigator.userAgent
|
||||||
|
(window.navigator as any).orgUserAgentData = (window.navigator as any).userAgentData;
|
||||||
Object.defineProperty(window.navigator, 'userAgentData', {});
|
Object.defineProperty(window.navigator, 'userAgentData', {});
|
||||||
|
|
||||||
// Override navigator.userAgent
|
// Override navigator.userAgent
|
||||||
|
Loading…
x
Reference in New Issue
Block a user