diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index a06a536..9993aef 100644 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -31,6 +31,7 @@ var BxEvent; (function(BxEvent2) { BxEvent2["JUMP_BACK_IN_READY"] = "bx-jump-back-in-ready"; BxEvent2["POPSTATE"] = "bx-popstate"; + BxEvent2["TITLE_INFO_READY"] = "bx-title-info-ready"; BxEvent2["STREAM_LOADING"] = "bx-stream-loading"; BxEvent2["STREAM_STARTING"] = "bx-stream-starting"; BxEvent2["STREAM_STARTED"] = "bx-stream-started"; @@ -83,23 +84,99 @@ try { } catch (e) { } -// src/utils/bx-exposed.ts -var BxExposed = { - onPollingModeChanged: (mode) => { - if (!STATES.isPlaying) { - return false; +// src/utils/html.ts +var createElement = function(elmName, props = {}, ..._) { + let $elm; + const hasNs = "xmlns" in props; + if (hasNs) { + $elm = document.createElementNS(props.xmlns, elmName); + delete props.xmlns; + } else { + $elm = document.createElement(elmName); + } + for (const key in props) { + if ($elm.hasOwnProperty(key)) { + continue; } - const $screenshotBtn = document.querySelector(".bx-screenshot-button"); - const $touchControllerBar = document.getElementById("bx-touch-controller-bar"); - if (mode !== "None") { - $screenshotBtn && $screenshotBtn.classList.add("bx-gone"); - $touchControllerBar && $touchControllerBar.classList.add("bx-gone"); + if (hasNs) { + $elm.setAttributeNS(null, key, props[key]); } else { - $screenshotBtn && $screenshotBtn.classList.remove("bx-gone"); - $touchControllerBar && $touchControllerBar.classList.remove("bx-gone"); + $elm.setAttribute(key, props[key]); } } + for (let i = 2, size = arguments.length;i < size; i++) { + const arg = arguments[i]; + const argType = typeof arg; + if (argType === "string" || argType === "number") { + $elm.appendChild(document.createTextNode(arg)); + } else if (arg) { + $elm.appendChild(arg); + } + } + return $elm; }; +var CE = createElement; +var Icon; +(function(Icon2) { + Icon2["STREAM_SETTINGS"] = ''; + Icon2["STREAM_STATS"] = ''; + Icon2["CONTROLLER"] = ''; + Icon2["DISPLAY"] = ''; + Icon2["MOUSE"] = ''; + Icon2["MOUSE_SETTINGS"] = ''; + Icon2["NEW"] = ''; + Icon2["COPY"] = ''; + Icon2["TRASH"] = ''; + Icon2["CURSOR_TEXT"] = ''; + Icon2["QUESTION"] = ''; + Icon2["REMOTE_PLAY"] = ''; + Icon2["HAND_TAP"] = ''; +})(Icon || (Icon = {})); +var createSvgIcon = (icon, strokeWidth = 2) => { + const $svg = CE("svg", { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + stroke: "#fff", + "fill-rule": "evenodd", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": strokeWidth + }); + $svg.innerHTML = icon; + $svg.setAttribute("viewBox", "0 0 32 32"); + return $svg; +}; +var ButtonStyle = {}; +ButtonStyle[ButtonStyle.PRIMARY = 1] = "bx-primary"; +ButtonStyle[ButtonStyle.DANGER = 2] = "bx-danger"; +ButtonStyle[ButtonStyle.GHOST = 4] = "bx-ghost"; +ButtonStyle[ButtonStyle.FOCUSABLE = 8] = "bx-focusable"; +ButtonStyle[ButtonStyle.FULL_WIDTH = 16] = "bx-full-width"; +ButtonStyle[ButtonStyle.FULL_HEIGHT = 32] = "bx-full-height"; +var ButtonStyleIndices = Object.keys(ButtonStyle).splice(0, Object.keys(ButtonStyle).length / 2).map((i) => parseInt(i)); +var createButton = (options) => { + let $btn; + if (options.url) { + $btn = CE("a", { class: "bx-button" }); + $btn.href = options.url; + $btn.target = "_blank"; + } else { + $btn = CE("button", { class: "bx-button" }); + } + const style = options.style || 0; + style && ButtonStyleIndices.forEach((index) => { + style & index && $btn.classList.add(ButtonStyle[index]); + }); + options.classes && $btn.classList.add(...options.classes); + options.icon && $btn.appendChild(createSvgIcon(options.icon, 4)); + options.label && $btn.appendChild(CE("span", {}, options.label)); + options.title && $btn.setAttribute("title", options.title); + options.disabled && ($btn.disabled = true); + options.onClick && $btn.addEventListener("click", options.onClick); + return $btn; +}; +var CTN = document.createTextNode.bind(document); +window.BX_CE = createElement; // src/utils/translation.ts var SUPPORTED_LANGUAGES = { @@ -3329,100 +3406,6 @@ var t = Translations.get; var refreshCurrentLocale = Translations.refreshCurrentLocale; refreshCurrentLocale(); -// src/utils/html.ts -var createElement = function(elmName, props = {}, ..._) { - let $elm; - const hasNs = "xmlns" in props; - if (hasNs) { - $elm = document.createElementNS(props.xmlns, elmName); - delete props.xmlns; - } else { - $elm = document.createElement(elmName); - } - for (const key in props) { - if ($elm.hasOwnProperty(key)) { - continue; - } - if (hasNs) { - $elm.setAttributeNS(null, key, props[key]); - } else { - $elm.setAttribute(key, props[key]); - } - } - for (let i = 2, size = arguments.length;i < size; i++) { - const arg = arguments[i]; - const argType = typeof arg; - if (argType === "string" || argType === "number") { - $elm.appendChild(document.createTextNode(arg)); - } else if (arg) { - $elm.appendChild(arg); - } - } - return $elm; -}; -var CE = createElement; -var Icon; -(function(Icon2) { - Icon2["STREAM_SETTINGS"] = ''; - Icon2["STREAM_STATS"] = ''; - Icon2["CONTROLLER"] = ''; - Icon2["DISPLAY"] = ''; - Icon2["MOUSE"] = ''; - Icon2["MOUSE_SETTINGS"] = ''; - Icon2["NEW"] = ''; - Icon2["COPY"] = ''; - Icon2["TRASH"] = ''; - Icon2["CURSOR_TEXT"] = ''; - Icon2["QUESTION"] = ''; - Icon2["REMOTE_PLAY"] = ''; - Icon2["HAND_TAP"] = ''; -})(Icon || (Icon = {})); -var createSvgIcon = (icon, strokeWidth = 2) => { - const $svg = CE("svg", { - xmlns: "http://www.w3.org/2000/svg", - fill: "none", - stroke: "#fff", - "fill-rule": "evenodd", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": strokeWidth - }); - $svg.innerHTML = icon; - $svg.setAttribute("viewBox", "0 0 32 32"); - return $svg; -}; -var ButtonStyle = {}; -ButtonStyle[ButtonStyle.PRIMARY = 1] = "bx-primary"; -ButtonStyle[ButtonStyle.DANGER = 2] = "bx-danger"; -ButtonStyle[ButtonStyle.GHOST = 4] = "bx-ghost"; -ButtonStyle[ButtonStyle.FOCUSABLE = 8] = "bx-focusable"; -ButtonStyle[ButtonStyle.FULL_WIDTH = 16] = "bx-full-width"; -ButtonStyle[ButtonStyle.FULL_HEIGHT = 32] = "bx-full-height"; -var ButtonStyleIndices = Object.keys(ButtonStyle).splice(0, Object.keys(ButtonStyle).length / 2).map((i) => parseInt(i)); -var createButton = (options) => { - let $btn; - if (options.url) { - $btn = CE("a", { class: "bx-button" }); - $btn.href = options.url; - $btn.target = "_blank"; - } else { - $btn = CE("button", { class: "bx-button" }); - } - const style = options.style || 0; - style && ButtonStyleIndices.forEach((index) => { - style & index && $btn.classList.add(ButtonStyle[index]); - }); - options.classes && $btn.classList.add(...options.classes); - options.icon && $btn.appendChild(createSvgIcon(options.icon, 4)); - options.label && $btn.appendChild(CE("span", {}, options.label)); - options.title && $btn.setAttribute("title", options.title); - options.disabled && ($btn.disabled = true); - options.onClick && $btn.addEventListener("click", options.onClick); - return $btn; -}; -var CTN = document.createTextNode.bind(document); -window.BX_CE = createElement; - // src/utils/settings.ts var SettingElementType; (function(SettingElementType2) { @@ -3668,6 +3651,10 @@ class UserAgent { } return result; } + static isMobile() { + const userAgent = (UserAgent.getDefault() || "").toLowerCase(); + return /iphone|ipad|android/.test(userAgent); + } static spoof() { let newUserAgent; const profile = getPref(PrefKey.USER_AGENT_PROFILE); @@ -3677,6 +3664,7 @@ class UserAgent { if (!newUserAgent) { newUserAgent = UserAgent.get(profile); } + window.navigator.orgUserAgentData = window.navigator.userAgentData; Object.defineProperty(window.navigator, "userAgentData", {}); window.navigator.orgUserAgent = window.navigator.userAgent; Object.defineProperty(window.navigator, "userAgent", { @@ -4761,6 +4749,50 @@ var getPref = prefs.get.bind(prefs); var setPref = prefs.set.bind(prefs); var toPrefElement = prefs.toElement.bind(prefs); +// src/utils/bx-exposed.ts +var InputType; +(function(InputType2) { + InputType2["CONTROLLER"] = "Controller"; + InputType2["MKB"] = "MKB"; + InputType2["CUSTOM_TOUCH_OVERLAY"] = "CustomTouchOverlay"; + InputType2["GENERIC_TOUCH"] = "GenericTouch"; + InputType2["NATIVE_TOUCH"] = "NativeTouch"; + InputType2["BATIVE_SENSOR"] = "NativeSensor"; +})(InputType || (InputType = {})); +var BxExposed = { + onPollingModeChanged: (mode) => { + if (!STATES.isPlaying) { + return false; + } + const $screenshotBtn = document.querySelector(".bx-screenshot-button"); + const $touchControllerBar = document.getElementById("bx-touch-controller-bar"); + if (mode !== "None") { + $screenshotBtn && $screenshotBtn.classList.add("bx-gone"); + $touchControllerBar && $touchControllerBar.classList.add("bx-gone"); + } else { + $screenshotBtn && $screenshotBtn.classList.remove("bx-gone"); + $touchControllerBar && $touchControllerBar.classList.remove("bx-gone"); + } + }, + modifyTitleInfo: (titleInfo) => { + titleInfo = structuredClone(titleInfo); + const touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER); + let supportedInputTypes = titleInfo.details.supportedInputTypes; + if (UserAgent.isMobile()) { + supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); + } + titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB); + titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) && !supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) && !supportedInputTypes.includes(InputType.GENERIC_TOUCH); + if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") { + supportedInputTypes.push(InputType.GENERIC_TOUCH); + } + titleInfo.details.supportedInputTypes = supportedInputTypes; + STATES.currentStream.titleInfo = titleInfo; + BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY); + return titleInfo; + } +}; + // src/utils/region.ts function getPreferredServerRegion(shortName = false) { let preferredRegion = getPref(PrefKey.SERVER_REGION); @@ -4785,84 +4817,6 @@ function getPreferredServerRegion(shortName = false) { return "???"; } -// src/utils/titles-info.ts -class TitlesInfo { - static #INFO = {}; - static get(titleId) { - return TitlesInfo.#INFO[titleId]; - } - static update(titleId, info) { - TitlesInfo.#INFO[titleId] = TitlesInfo.#INFO[titleId] || {}; - Object.assign(TitlesInfo.#INFO[titleId], info); - } - static saveFromTitleInfo(titleInfo) { - const details = titleInfo.details; - const info = { - titleId: titleInfo.titleId, - xboxTitleId: "" + details.xboxTitleId, - hasTouchSupport: details.supportedInputTypes.length > 1 - }; - TitlesInfo.update(details.productId, info); - } - static saveFromCatalogInfo(catalogInfo) { - const titleId = catalogInfo.StoreId; - const imageHero = (catalogInfo.Image_Hero || catalogInfo.Image_Tile || {}).URL; - TitlesInfo.update(titleId, { - imageHero - }); - } - static hasTouchSupport(titleId) { - return !!TitlesInfo.#INFO[titleId]?.hasTouchSupport; - } - static requestCatalogInfo(titleId, callback) { - 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 - }, - body: JSON.stringify({ - Products: [titleId] - }) - }).then((resp) => { - callback && callback(TitlesInfo.get(titleId)); - }); - } -} - -class PreloadedState { - static override() { - Object.defineProperty(window, "__PRELOADED_STATE__", { - configurable: true, - get: () => { - const userAgent = UserAgent.spoof(); - if (userAgent) { - this._state.appContext.requestInfo.userAgent = userAgent; - } - return this._state; - }, - set: (state) => { - this._state = state; - STATES.appContext = structuredClone(state.appContext); - if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all") { - let titles = {}; - try { - titles = state.xcloud.titles.data.titles; - } catch (e) { - } - for (let id2 in titles) { - TitlesInfo.saveFromTitleInfo(titles[id2].data); - } - } - } - }); - } -} - // src/modules/loading-screen.ts class LoadingScreen { static #$bgStyle; @@ -4877,8 +4831,8 @@ class LoadingScreen { return mDisplay + sDisplay; } static setup() { - const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/); - if (!match) { + const titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) { return; } if (!LoadingScreen.#$bgStyle) { @@ -4886,15 +4840,7 @@ class LoadingScreen { document.documentElement.appendChild($bgStyle); 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) => { - 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(); } @@ -5629,6 +5575,7 @@ class MkbHandler { }; #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); #enabled = false; + #isPolling = false; #prevWheelCode = null; #wheelStoppedTimeout; #detectMouseStoppedTimeout; @@ -5708,10 +5655,15 @@ class MkbHandler { }; #onKeyboardEvent = (e) => { const isKeyDown = e.type === "keydown"; - if (isKeyDown && e.code === "F8") { - e.preventDefault(); - this.toggle(); - return; + if (isKeyDown) { + if (e.code === "F8") { + e.preventDefault(); + this.toggle(); + return; + } + if (!this.#isPolling) { + return; + } } const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code]; if (typeof buttonIndex === "undefined") { @@ -5884,6 +5836,7 @@ class MkbHandler { this.#waitForPointerLock(true); }; destroy = () => { + this.#isPolling = false; this.#enabled = false; this.stop(); this.#waitForPointerLock(false); @@ -5895,6 +5848,7 @@ class MkbHandler { window.removeEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden); }; start = () => { + this.#isPolling = true; window.navigator.getGamepads = this.#patchedGetGamepads; this.#resetGamepad(); window.addEventListener("keyup", this.#onKeyboardEvent); @@ -5911,6 +5865,7 @@ class MkbHandler { }); }; stop = () => { + this.#isPolling = false; const virtualGamepad = this.#getVirtualGamepad(); virtualGamepad.connected = false; virtualGamepad.timestamp = performance.now(); @@ -5927,8 +5882,8 @@ class MkbHandler { window.removeEventListener("contextmenu", this.#disableContextMenu); }; static setupEvents() { - window.addEventListener(BxEvent.STREAM_PLAYING, () => { - if (getPref(PrefKey.MKB_ENABLED)) { + getPref(PrefKey.MKB_ENABLED) && !UserAgent.isMobile() && window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (!STATES.currentStream.titleInfo?.details.hasMkbSupport) { console.log("Emulate MKB"); MkbHandler.INSTANCE.init(); } @@ -7843,11 +7798,11 @@ class XcloudInterceptor { return NATIVE_FETCH(request, init); } if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all") { - TouchController.disable(); - const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/); - 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(); } } const response = await NATIVE_FETCH(request, init); @@ -7872,24 +7827,6 @@ class XcloudInterceptor { response.text = () => Promise.resolve(JSON.stringify(obj)); return response; } - static async#handleCatalog(request, init) { - 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, init) { - 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, init) { let url = typeof request === "string" ? request : request.url; if (url.endsWith("/v2/login/user")) { @@ -7900,10 +7837,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.method === "GET") { return patchIceCandidates(request); } @@ -9745,14 +9678,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; } `; } @@ -9788,6 +9721,42 @@ window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged `; str2 = str2.replace(text, text + newCode); return str2; + }, + patchXcloudTitleInfo(str2) { + const text = "async cloudConnect"; + let index = str2.indexOf(text); + if (index === -1) { + return false; + } + let backetIndex = str2.indexOf("{", index); + const params = str2.substring(index, backetIndex).match(/\(([^)]+)\)/)[1]; + const titleInfoVar = params.split(",")[0]; + const newCode = ` +${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar}); +console.log(${titleInfoVar}); +`; + str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1); + return str2; + }, + patchRemotePlayMkb(str2) { + const text = "async homeConsoleConnect"; + let index = str2.indexOf(text); + if (index === -1) { + return false; + } + let backetIndex = str2.indexOf("{", index); + const params = str2.substring(index, backetIndex).match(/\(([^)]+)\)/)[1]; + const configsVar = params.split(",")[1]; + const newCode = ` +Object.assign(${configsVar}.inputConfiguration, { + enableMouseInput: false, + enableKeyboardInput: false, + enableAbsoluteMouse: false, +}); +console.log(${configsVar}); +`; + str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1); + return str2; } }; var PATCH_ORDERS = [ @@ -9820,6 +9789,8 @@ var PATCH_ORDERS = [ getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && ["forceFortniteConsole"] ]; var PLAYING_PATCH_ORDERS = [ + ["patchXcloudTitleInfo"], + getPref(PrefKey.REMOTE_PLAY_ENABLED) && ["patchRemotePlayMkb"], getPref(PrefKey.REMOTE_PLAY_ENABLED) && ["remotePlayConnectMode"], getPref(PrefKey.REMOTE_PLAY_ENABLED) && ["remotePlayGuideWorkaround"], ["patchStreamHud"], @@ -9962,6 +9933,26 @@ function onHistoryChanged(e) { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); } +// src/utils/titles-info.ts +class PreloadedState { + static override() { + Object.defineProperty(window, "__PRELOADED_STATE__", { + configurable: true, + get: () => { + const userAgent = UserAgent.spoof(); + if (userAgent) { + this._state.appContext.requestInfo.userAgent = userAgent; + } + return this._state; + }, + set: (state) => { + this._state = state; + STATES.appContext = structuredClone(state.appContext); + } + }); + } +} + // src/utils/monkey-patches.ts function patchVideoApi() { const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO); @@ -10176,8 +10167,8 @@ window.addEventListener(BxEvent.STREAM_LOADING, (e) => { STATES.currentStream.productId = ""; } setupBxUi(); - getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.setup(); }); +getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, (e) => { LoadingScreen.hide(); if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) {