mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 23:57: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
|
||||
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
|
||||
|
@ -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();
|
||||
|
@ -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'],
|
||||
|
||||
|
15
src/types/index.d.ts
vendored
15
src/types/index.d.ts
vendored
@ -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;
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user