Use a better method to enable touch control for all games

This commit is contained in:
redphx
2024-04-29 10:05:22 +07:00
parent 102a4657f0
commit cd6dd1e22d
9 changed files with 104 additions and 128 deletions

View File

@@ -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',

View File

@@ -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;
}
};

View File

@@ -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);
}

View File

@@ -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);
}
}
}
});
}

View File

@@ -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