From cd6dd1e22d902bac55c5edc1a60068bafef08134 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:05:22 +0700 Subject: [PATCH] Use a better method to enable touch control for all games --- src/index.ts | 5 ++- src/modules/loading-screen.ts | 17 ++------- src/modules/patcher.ts | 30 ++++++++++++--- src/types/index.d.ts | 15 ++++++++ src/utils/bx-event.ts | 2 + src/utils/bx-exposed.ts | 43 +++++++++++++++++++++ src/utils/network.ts | 42 +++------------------ src/utils/titles-info.ts | 70 ----------------------------------- src/utils/user-agent.ts | 8 +++- 9 files changed, 104 insertions(+), 128 deletions(-) diff --git a/src/index.ts b/src/index.ts index dc0e2b5..4b326b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,10 +124,11 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => { // Setup UI 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 => { // Hide loading screen diff --git a/src/modules/loading-screen.ts b/src/modules/loading-screen.ts index 469b884..13d914c 100644 --- a/src/modules/loading-screen.ts +++ b/src/modules/loading-screen.ts @@ -1,8 +1,8 @@ import { CE } from "../utils/html"; import { getPreferredServerRegion } from "../utils/region"; -import { TitlesInfo } from "../utils/titles-info"; import { PrefKey, getPref } from "../utils/preferences"; import { t } from "../utils/translation"; +import { STATES } from "../utils/global"; export class LoadingScreen { static #$bgStyle: HTMLElement; @@ -21,9 +21,8 @@ export class LoadingScreen { } static setup() { - // Get titleId from location - const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/); - if (!match) { + const titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) { return; } @@ -33,15 +32,7 @@ export class LoadingScreen { LoadingScreen.#$bgStyle = $bgStyle; } - const titleId = match[1]; - 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); - }); - } + LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl); if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') { LoadingScreen.#hideRocket(); diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index 26ed959..687f4fe 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -332,14 +332,14 @@ const gamepads = window.navigator.getGamepads(); let gamepadFound = false; for (let gamepad of gamepads) { -if (gamepad && gamepad.connected) { - gamepadFound = true; - break; -} + if (gamepad && gamepad.connected) { + gamepadFound = true; + break; + } } if (gamepadFound) { -return; + return; } `; } @@ -386,6 +386,24 @@ window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged str = str.replace(text, text + newCode); 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 = [ @@ -432,6 +450,8 @@ let PATCH_ORDERS = [ // Only when playing const PLAYING_PATCH_ORDERS = [ + ['patchXcloudTitleInfo'], + getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'], getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'], diff --git a/src/types/index.d.ts b/src/types/index.d.ts index de0c2f2..f69a7a5 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -31,6 +31,7 @@ type BxStates = { titleId: string; xboxTitleId: string; productId: string; + titleInfo: XcloudTitleInfo; $video: HTMLVideoElement | null; $screenshotCanvas: HTMLCanvasElement | null; @@ -50,3 +51,17 @@ type BxStates = { } type DualEnum = {[index: string]: number} & {[index: number]: string}; + +type XcloudTitleInfo = { + details: { + productId: string; + supportedInputTypes: InputType[]; + hasTouchSupport: boolean; + }; + + product: { + heroImageUrl: string; + titledHeroImageUrl: string; + tileImageUrl: string; + }; +}; diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index 739fdd4..20d00fb 100644 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -4,6 +4,8 @@ export enum BxEvent { JUMP_BACK_IN_READY = 'bx-jump-back-in-ready', POPSTATE = 'bx-popstate', + TITLE_INFO_READY = 'bx-title-info-ready', + STREAM_LOADING = 'bx-stream-loading', STREAM_STARTING = 'bx-stream-starting', STREAM_STARTED = 'bx-stream-started', diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index af206ec..3214575 100644 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -1,4 +1,16 @@ +import { BxEvent } from "./bx-event"; 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 = { onPollingModeChanged: (mode: 'All' | 'None') => { @@ -23,4 +35,35 @@ export const BxExposed = { $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; + } }; diff --git a/src/utils/network.ts b/src/utils/network.ts index b8e1b05..29dc473 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -7,7 +7,6 @@ import { StreamBadges } from "../modules/stream/stream-badges"; import { TouchController } from "../modules/touch-controller"; import { STATES } from "./global"; import { getPreferredServerRegion } from "./region"; -import { TitlesInfo } from "./titles-info"; export const NATIVE_FETCH = window.fetch; @@ -402,14 +401,11 @@ class XcloudInterceptor { // Touch controller for all games if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') { - TouchController.disable(); - - // Get game ID from window.location - const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/); - // Check touch support - if (match) { - const titleId = match[1]; - !TitlesInfo.hasTouchSupport(titleId) && TouchController.enable(); + const titleInfo = STATES.currentStream.titleInfo; + if (titleInfo?.details.hasTouchSupport) { + TouchController.disable(); + } else { + TouchController.enable(); } } @@ -446,30 +442,6 @@ class XcloudInterceptor { 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) { let url = (typeof request === 'string') ? request : (request as Request).url; @@ -482,10 +454,6 @@ class XcloudInterceptor { return XcloudInterceptor.#handleWaitTime(request, init); } else if (url.endsWith('/configuration')) { 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') { return patchIceCandidates(request as Request); } diff --git a/src/utils/titles-info.ts b/src/utils/titles-info.ts index cffe173..afd8248 100644 --- a/src/utils/titles-info.ts +++ b/src/utils/titles-info.ts @@ -1,64 +1,6 @@ -import { PrefKey } from "./preferences"; -import { getPref } from "./preferences"; import { STATES } from "./global"; 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 { static override() { @@ -76,18 +18,6 @@ export class PreloadedState { set: state => { (this as any)._state = state; 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); - } - } } }); } diff --git a/src/utils/user-agent.ts b/src/utils/user-agent.ts index b44c1ee..e204ef1 100644 --- a/src/utils/user-agent.ts +++ b/src/utils/user-agent.ts @@ -43,7 +43,7 @@ export class UserAgent { return (UserAgent.#USER_AGENTS as any)[profile] || defaultUserAgent; } - static isSafari(mobile=false) { + static isSafari(mobile=false): boolean { const userAgent = (UserAgent.getDefault() || '').toLowerCase(); let result = userAgent.includes('safari') && !userAgent.includes('chrom'); @@ -54,6 +54,11 @@ export class UserAgent { return result; } + static isMobile(): boolean { + const userAgent = (UserAgent.getDefault() || '').toLowerCase(); + return /iphone|ipad|android/.test(userAgent); + } + static spoof() { let newUserAgent; @@ -67,6 +72,7 @@ export class 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', {}); // Override navigator.userAgent