mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-09-26 05:59:32 +02:00
Use a better method to enable touch control for all games
This commit is contained in:
@@ -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',
|
||||
|
@@ -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;
|
||||
}
|
||||
};
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user