From 1ea1afe4d4d878866dbfcb12a93269436bba1378 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:13:24 +0700 Subject: [PATCH] Optimize Patcher --- dist/better-xcloud.lite.user.js | 39 +--- dist/better-xcloud.user.js | 96 ++++++--- src/index.ts | 6 +- src/modules/patcher/patcher-utils.ts | 45 ++++ src/modules/{ => patcher}/patcher.ts | 199 ++++++++---------- .../patches/controller-shortcuts.js | 0 .../patches/expose-stream-session.js | 0 .../patches/local-co-op-enable.js | 0 .../patches/remote-play-enable.js | 0 .../patches/remote-play-keep-alive.js | 0 .../set-currently-focused-interactable.js | 0 .../{ => patcher}/patches/vibration-adjust.js | 0 src/modules/ui/dialog/settings-dialog.ts | 2 +- src/utils/bx-exposed.ts | 8 +- 14 files changed, 207 insertions(+), 188 deletions(-) create mode 100644 src/modules/patcher/patcher-utils.ts rename src/modules/{ => patcher}/patcher.ts (91%) rename src/modules/{ => patcher}/patches/controller-shortcuts.js (100%) rename src/modules/{ => patcher}/patches/expose-stream-session.js (100%) rename src/modules/{ => patcher}/patches/local-co-op-enable.js (100%) rename src/modules/{ => patcher}/patches/remote-play-enable.js (100%) rename src/modules/{ => patcher}/patches/remote-play-keep-alive.js (100%) rename src/modules/{ => patcher}/patches/set-currently-focused-interactable.js (100%) rename src/modules/{ => patcher}/patches/vibration-adjust.js (100%) diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js index d4cd212..765aa3d 100755 --- a/dist/better-xcloud.lite.user.js +++ b/dist/better-xcloud.lite.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud (Lite) // @namespace https://github.com/redphx -// @version 6.0.0 +// @version 6.0.1-beta-1 // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -105,7 +105,7 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "6.0.0", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.0.1-beta-1", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { supportedRegion: !0, @@ -4775,7 +4775,8 @@ var BxExposed = { / {2,}/g, / /g ], - toggleLocalCoOp: (enable) => {} + toggleLocalCoOp(enable) {}, + beforePageLoad: () => {} }; function localRedirect(path) { let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); @@ -6159,35 +6160,6 @@ function patchCanvasContext() { return nativeGetContext.apply(this, [contextType, contextAttributes]); }; } -class ProductDetailsPage { - static $btnShortcut = AppInterface && createButton({ - icon: BxIcon.CREATE_SHORTCUT, - label: t("create-shortcut"), - style: 64, - onClick: (e) => { - AppInterface.createShortcut(window.location.pathname.substring(6)); - } - }); - static $btnWallpaper = AppInterface && createButton({ - icon: BxIcon.DOWNLOAD, - label: t("wallpaper"), - style: 64, - onClick: (e) => { - let details = parseDetailsPath(window.location.pathname); - details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); - } - }); - static injectTimeoutId = null; - static injectButtons() { - if (!AppInterface) return; - ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => { - let $container = document.querySelector("div[class*=ActionButtons-module__container]"); - if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", { - class: "bx-product-details-buttons" - }, BX_FLAGS.DeviceInfo.deviceType === "android" && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper)); - }, 500); - } -} class StreamUiHandler { static $btnStreamSettings; static $btnStreamStats; @@ -6422,9 +6394,6 @@ window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); -window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { - if (e.component === "product-details") ProductDetailsPage.injectButtons(); -}); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { let dataChannel = e.dataChannel; if (!dataChannel || dataChannel.label !== "message") return; diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 8ec8b1e..1429cba 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 6.0.0 +// @version 6.0.1-beta-1 // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -107,7 +107,7 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "6.0.0", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.0.1-beta-1", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { supportedRegion: !0, @@ -4063,8 +4063,16 @@ class PatcherUtils { static replaceWith(txt, index, fromString, toString) { return txt.substring(0, index) + toString + txt.substring(index + fromString.length); } + static filterPatches(patches) { + return patches.filter((item2) => !!item2); + } + static patchBeforePageLoad(str, page) { + let text = `chunkName:()=>"${page}-page",`; + if (!str.includes(text)) return !1; + return str = str.replace("requireAsync(e){", `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str = str.replace("requireSync(e){", `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str; + } } -var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG2 = "Patcher", PATCHES = { +var LOG_TAG2 = "Patcher", PATCHES = { disableAiTrack(str) { let text = ".track=function(", index = str.indexOf(text); if (index < 0) return !1; @@ -4196,11 +4204,6 @@ logFunc(logTag, '//', logMessage); let newCode = "e.enableTouchInput = true;"; return str = str.replace(text, text + newCode), str; }, - loadingEndingChunks(str) { - let text = '"FamilySagaManager"'; - if (!str.includes(text)) return !1; - return BxLogger.info(LOG_TAG2, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str; - }, disableStreamGate(str) { let index = str.indexOf('case"partially-ready":'); if (index < 0) return !1; @@ -4471,10 +4474,10 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { if (index < 0) return !1; return index = str.indexOf("{", index) + 1, str = str.substring(0, index) + set_currently_focused_interactable_default + str.substring(index), str; }, - detectProductDetailsPage(str) { + detectProductDetailPage(str) { let index = str.indexOf('{location:"ProductDetailPage",'); - if (index >= 0 && (index = PatcherUtils.lastIndexOf("return", str, index, 200)), index < 0) return !1; - return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-details" });' + str.substring(index), str; + if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; + return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str; }, detectBrowserRouterReady(str) { let index = str.indexOf("{history:this.history,"); @@ -4501,8 +4504,17 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { let text = "=window.__PRELOADED_STATE__;"; if (!str.includes(text)) return !1; return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str; + }, + homePageBeforeLoad(str) { + return PatcherUtils.patchBeforePageLoad(str, "home"); + }, + productDetailPageBeforeLoad(str) { + return PatcherUtils.patchBeforePageLoad(str, "product-detail"); + }, + streamPageBeforeLoad(str) { + return PatcherUtils.patchBeforePageLoad(str, "stream"); } -}, PATCH_ORDERS = [ +}, PATCH_ORDERS = PatcherUtils.filterPatches([ ...getPref("nativeMkb.mode") === "on" ? [ "enableNativeMkb", "exposeInputSink" @@ -4517,6 +4529,9 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "patchGamepadPolling", "exposeStreamSession", "exposeDialogRoutes", + "homePageBeforeLoad", + "productDetailPageBeforeLoad", + "streamPageBeforeLoad", "guideAchievementsDefaultLocked", "enableTvRoutes", "supportLocalCoOp", @@ -4524,10 +4539,6 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { getPref("ui.gameCard.waitTime.show") && "patchSetCurrentlyFocusedInteractable", getPref("ui.layout") !== "default" && "websiteLayout", getPref("game.fortnite.forceConsole") && "forceFortniteConsole", - getPref("ui.hideSections").includes("friends") && "ignorePlayWithFriendsSection", - getPref("ui.hideSections").includes("all-games") && "ignoreAllGamesSection", - getPref("ui.hideSections").includes("touch") && "ignorePlayWithTouchSection", - (getPref("ui.hideSections").includes("native-mkb") || getPref("ui.hideSections").includes("most-popular")) && "ignoreSiglSections", ...STATES.userAgent.capabilities.touch ? [ "disableTouchContextMenu" ] : [], @@ -4550,7 +4561,12 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "enableConsoleLogging", "enableXcloudLogger" ] : [] -].filter((item2) => !!item2), PLAYING_PATCH_ORDERS = [ +]), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ + getPref("ui.hideSections").includes("friends") && "ignorePlayWithFriendsSection", + getPref("ui.hideSections").includes("all-games") && "ignoreAllGamesSection", + STATES.browser.capabilities.touch && getPref("ui.hideSections").includes("touch") && "ignorePlayWithTouchSection", + (getPref("ui.hideSections").includes("native-mkb") || getPref("ui.hideSections").includes("most-popular")) && "ignoreSiglSections" +]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ "patchXcloudTitleInfo", "disableGamepadDisconnectedScreen", "patchStreamHud", @@ -4562,7 +4578,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { ...STATES.userAgent.capabilities.touch ? [ getPref("touchController.mode") === "all" && "patchShowSensorControls", getPref("touchController.mode") === "all" && "exposeTouchLayoutManager", - (getPref("touchController.mode") === "off" || getPref("touchController.autoOff") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", + (getPref("touchController.mode") === "off" || getPref("touchController.autoOff")) && "disableTakRenderer", getPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity", "patchBabylonRendererClass" ] : [], @@ -4577,9 +4593,21 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "patchMouseAndKeyboardEnabled", "disableNativeRequestPointerLock" ] : [] -].filter((item2) => !!item2), ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; +]), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ + AppInterface && "detectProductDetailPage" +]), ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS]; class Patcher { - static #patchFunctionBind() { + static remainingPatches = { + home: HOME_PAGE_PATCH_ORDERS, + stream: STREAM_PAGE_PATCH_ORDERS, + "product-detail": PRODUCT_DETAIL_PAGE_PATCH_ORDERS + }; + static patchPage(page) { + let remaining = Patcher.remainingPatches[page]; + if (!remaining) return; + PATCH_ORDERS = PATCH_ORDERS.concat(remaining), delete Patcher.remainingPatches[page]; + } + static patchNativeBind() { let nativeBind = Function.prototype.bind; Function.prototype.bind = function() { let valid = !1; @@ -4587,7 +4615,7 @@ class Patcher { if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0; } if (!valid) return nativeBind.apply(this, arguments); - if (PatcherCache.getInstance().init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; + if (typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; let orgFunc = this, newFunc = (a, item2) => { Patcher.checkChunks(item2), orgFunc(a, item2); }; @@ -4609,7 +4637,7 @@ class Patcher { if (!PATCHES[patchName]) continue; let tmpStr = PATCHES[patchName].call(null, patchedFuncStr); if (!tmpStr) continue; - modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); + modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName), BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS); } if (modified) try { chunkData[chunkId] = eval(patchedFuncStr); @@ -4621,7 +4649,7 @@ class Patcher { if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap); } static init() { - Patcher.#patchFunctionBind(); + Patcher.patchNativeBind(); } } class PatcherCache { @@ -4630,7 +4658,14 @@ class PatcherCache { KEY_CACHE = "BetterXcloud.Patches.Cache"; KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature"; CACHE; - isInitialized = !1; + constructor() { + this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE); + let pathName = window.location.pathname; + if (pathName.includes("/play/launch/")) Patcher.patchPage("stream"); + else if (pathName.includes("/play/games/")) Patcher.patchPage("product-detail"); + else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home"); + PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0)); + } getSignature() { let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]'); if ($link) { @@ -4666,12 +4701,6 @@ class PatcherCache { } window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE)); } - init() { - if (this.isInitialized) return; - if (this.isInitialized = !0, this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, this.CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); - else PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME); - PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = this.cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG2, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG2, PLAYING_PATCH_ORDERS.slice(0)); - } } class BxNumberStepper extends HTMLInputElement { intervalId = null; @@ -6936,7 +6965,10 @@ var BxExposed = { / {2,}/g, / /g ], - toggleLocalCoOp: (enable) => {} + toggleLocalCoOp(enable) {}, + beforePageLoad: (page) => { + BxLogger.info("beforePageLoad", page), Patcher.patchPage(page); + } }; function localRedirect(path) { let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); @@ -9214,7 +9246,7 @@ window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { - if (e.component === "product-details") ProductDetailsPage.injectButtons(); + if (e.component === "product-detail") ProductDetailsPage.injectButtons(); }); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { let dataChannel = e.dataChannel; diff --git a/src/index.ts b/src/index.ts index 6592d76..f35bbe1 100755 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { LoadingScreen } from "@modules/loading-screen"; import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider"; import { TouchController } from "@modules/touch-controller"; import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils"; -import { Patcher } from "@modules/patcher"; +import { Patcher } from "@/modules/patcher/patcher"; import { RemotePlayManager } from "@/modules/remote-play-manager"; import { onHistoryChanged, patchHistoryMethod } from "@utils/history"; import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, patchMeControl, patchPointerLockApi, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches"; @@ -266,9 +266,9 @@ window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); -window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => { +isFullVersion() && window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => { const component = (e as any).component; - if (component === 'product-details') { + if (component === 'product-detail') { ProductDetailsPage.injectButtons(); } }); diff --git a/src/modules/patcher/patcher-utils.ts b/src/modules/patcher/patcher-utils.ts new file mode 100644 index 0000000..362f7e3 --- /dev/null +++ b/src/modules/patcher/patcher-utils.ts @@ -0,0 +1,45 @@ +import type { PatchArray, PatchName, PatchPage } from "./patcher"; + +export class PatcherUtils { + static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { + const index = txt.indexOf(searchString, startIndex); + if (index < 0 || (maxRange && index - startIndex > maxRange)) { + return -1; + } + + return index; + } + + static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { + const index = txt.lastIndexOf(searchString, startIndex); + if (index < 0 || (maxRange && startIndex - index > maxRange)) { + return -1; + } + + return index; + } + + static insertAt(txt: string, index: number, insertString: string): string { + return txt.substring(0, index) + insertString + txt.substring(index); + } + + static replaceWith(txt: string, index: number, fromString: string, toString: string): string { + return txt.substring(0, index) + toString + txt.substring(index + fromString.length); + } + + static filterPatches(patches: Array): PatchArray { + return patches.filter((item): item is PatchName => !!item); + } + + static patchBeforePageLoad(str: string, page: PatchPage): string | false { + let text = `chunkName:()=>"${page}-page",`; + if (!str.includes(text)) { + return false; + } + + str = str.replace('requireAsync(e){', `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`); + str = str.replace('requireSync(e){', `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`); + + return str; + } +} diff --git a/src/modules/patcher.ts b/src/modules/patcher/patcher.ts similarity index 91% rename from src/modules/patcher.ts rename to src/modules/patcher/patcher.ts index ea740cd..b553453 100755 --- a/src/modules/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -1,4 +1,4 @@ -import { SCRIPT_VERSION, STATES } from "@utils/global"; +import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global"; import { BX_FLAGS } from "@utils/bx-flags"; import { BxLogger } from "@utils/bx-logger"; import { hashCode, renderString } from "@utils/utils"; @@ -17,39 +17,12 @@ import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { GamePassCloudGallery } from "@/enums/game-pass-gallery"; import { t } from "@/utils/translation"; import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values"; +import { PatcherUtils } from "./patcher-utils.js"; -type PathName = keyof typeof PATCHES; -type PatchArray = PathName[]; +export type PatchName = keyof typeof PATCHES; +export type PatchArray = PatchName[]; +export type PatchPage = 'home' | 'stream' | 'product-detail'; -class PatcherUtils { - static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { - const index = txt.indexOf(searchString, startIndex); - if (index < 0 || (maxRange && index - startIndex > maxRange)) { - return -1; - } - - return index; - } - - static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { - const index = txt.lastIndexOf(searchString, startIndex); - if (index < 0 || (maxRange && startIndex - index > maxRange)) { - return -1; - } - - return index; - } - - static insertAt(txt: string, index: number, insertString: string): string { - return txt.substring(0, index) + insertString + txt.substring(index); - } - - static replaceWith(txt: string, index: number, fromString: string, toString: string): string { - return txt.substring(0, index) + toString + txt.substring(index + fromString.length); - } -} - -const ENDING_CHUNKS_PATCH_NAME = 'loadingEndingChunks'; const LOG_TAG = 'Patcher'; const PATCHES = { @@ -314,19 +287,6 @@ logFunc(logTag, '//', logMessage); return str; }, - // Add patches that are only needed when start playing - loadingEndingChunks(str: string) { - let text = '"FamilySagaManager"'; - if (!str.includes(text)) { - return false; - } - - BxLogger.info(LOG_TAG, 'Remaining patches:', PATCH_ORDERS); - PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS); - - return str; - }, - // Disable StreamGate disableStreamGate(str: string) { const index = str.indexOf('case"partially-ready":'); @@ -719,30 +679,6 @@ true` + text; return str; }, - /* - (x.AW, { - path: V.LoginDeviceCode.path, - exact: !0, - render: () => (0, n.jsx)(qe, { - children: (0, n.jsx)(Et.R, {}) - }) - }, V.LoginDeviceCode.name), - - const qe = e => { - let { - children: t - } = e; - const { - isTV: a, - isSupportedTVBrowser: r - } = (0, T.d)(); - return a && r ? (0, n.jsx)(n.Fragment, { - children: t - }) : (0, n.jsx)(x.l_, { - to: V.Home.getLink() - }) - }; - */ enableTvRoutes(str: string) { let index = str.indexOf('.LoginDeviceCode.path,'); if (index < 0) { @@ -912,16 +848,15 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { return str; }, - // product-details-page.js#2388, 24.17.20 - detectProductDetailsPage(str: string) { + detectProductDetailPage(str: string) { let index = str.indexOf('{location:"ProductDetailPage",'); - index >= 0 && (index = PatcherUtils.lastIndexOf('return', str, index, 200)); + index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200)); if (index < 0) { return false; } - str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-details" });' + str.substring(index); + str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index); return str; }, @@ -992,9 +927,21 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { str = str.replace(text, '=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);'); return str; }, + + homePageBeforeLoad(str: string) { + return PatcherUtils.patchBeforePageLoad(str, 'home'); + }, + + productDetailPageBeforeLoad(str: string) { + return PatcherUtils.patchBeforePageLoad(str, 'product-detail'); + }, + + streamPageBeforeLoad(str: string) { + return PatcherUtils.patchBeforePageLoad(str, 'stream'); + }, }; -let PATCH_ORDERS: PatchArray = [ +let PATCH_ORDERS = PatcherUtils.filterPatches([ ...(getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ 'enableNativeMkb', 'exposeInputSink', @@ -1015,10 +962,13 @@ let PATCH_ORDERS: PatchArray = [ 'exposeStreamSession', 'exposeDialogRoutes', + 'homePageBeforeLoad', + 'productDetailPageBeforeLoad', + 'streamPageBeforeLoad', + 'guideAchievementsDefaultLocked', 'enableTvRoutes', - // AppInterface && 'detectProductDetailsPage', 'supportLocalCoOp', 'overrideStorageGetSettings', @@ -1027,11 +977,6 @@ let PATCH_ORDERS: PatchArray = [ getPref(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout', getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole', - getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection', - getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection', - getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection', - (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections', - ...(STATES.userAgent.capabilities.touch ? [ 'disableTouchContextMenu', ] : []), @@ -1059,12 +1004,19 @@ let PATCH_ORDERS: PatchArray = [ 'enableConsoleLogging', 'enableXcloudLogger', ] : []), -].filter((item): item is string => !!item) as PatchArray; +]); + +let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ + getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection', + getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection', + STATES.browser.capabilities.touch && getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection', + (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections', +]); // Only when playing // TODO: check this // @ts-ignore -let PLAYING_PATCH_ORDERS: PatchArray = [ +let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ 'patchXcloudTitleInfo', 'disableGamepadDisconnectedScreen', 'patchStreamHud', @@ -1085,7 +1037,7 @@ let PLAYING_PATCH_ORDERS: PatchArray = [ ...(STATES.userAgent.capabilities.touch ? [ getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls', getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager', - (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer', + (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer', getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity', 'patchBabylonRendererClass', ] : []), @@ -1106,12 +1058,32 @@ let PLAYING_PATCH_ORDERS: PatchArray = [ 'patchMouseAndKeyboardEnabled', 'disableNativeRequestPointerLock', ] : []), -].filter((item): item is string => !!item); +]); -const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; +let PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ + AppInterface && 'detectProductDetailPage', +]); + +const ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS]; export class Patcher { - static #patchFunctionBind() { + private static remainingPatches: { [key in PatchPage]: PatchArray } = { + home: HOME_PAGE_PATCH_ORDERS, + stream: STREAM_PAGE_PATCH_ORDERS, + 'product-detail': PRODUCT_DETAIL_PAGE_PATCH_ORDERS, + }; + + static patchPage(page: PatchPage) { + const remaining = Patcher.remainingPatches[page]; + if (!remaining) { + return; + } + + PATCH_ORDERS = PATCH_ORDERS.concat(remaining); + delete Patcher.remainingPatches[page]; + } + + private static patchNativeBind() { const nativeBind = Function.prototype.bind; Function.prototype.bind = function() { let valid = false; @@ -1132,8 +1104,6 @@ export class Patcher { return nativeBind.apply(this, arguments); } - PatcherCache.getInstance().init(); - if (typeof arguments[1] === 'function') { BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()'); Function.prototype.bind = nativeBind; @@ -1211,6 +1181,7 @@ export class Patcher { patchesToCheck.splice(patchIndex, 1); patchIndex--; PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName); + BxLogger.info(LOG_TAG, 'Remaining patches', PATCH_ORDERS); } // Apply patched functions @@ -1236,7 +1207,7 @@ export class Patcher { } static init() { - Patcher.#patchFunctionBind(); + Patcher.patchNativeBind(); } } @@ -1249,7 +1220,29 @@ export class PatcherCache { private CACHE!: { [key: string]: PatchArray }; - private isInitialized = false; + private constructor() { + this.checkSignature(); + + // Read cache from storage + this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || '{}'); + BxLogger.info(LOG_TAG, 'Cache', this.CACHE); + + const pathName = window.location.pathname; + if (pathName.includes('/play/launch/')) { + Patcher.patchPage('stream'); + } else if (pathName.includes('/play/games/')) { + Patcher.patchPage('product-detail'); + } else if (pathName.endsWith('/play') || pathName.endsWith('/play/')) { + Patcher.patchPage('home'); + } + + // Remove cached patches from PATCH_ORDERS & PLAYING_PATCH_ORDERS + PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS); + STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS); + PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS); + + BxLogger.info(LOG_TAG, 'PATCH_ORDERS', PATCH_ORDERS.slice(0)); + } /** * Get patch's signature @@ -1333,30 +1326,4 @@ export class PatcherCache { // Save to storage window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE)); } - - init() { - if (this.isInitialized) { - return; - } - this.isInitialized = true; - - this.checkSignature(); - - // Read cache from storage - this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || '{}'); - BxLogger.info(LOG_TAG, this.CACHE); - - if (window.location.pathname.includes('/play/')) { - PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); - } else { - PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME); - } - - // Remove cached patches from PATCH_ORDERS & PLAYING_PATCH_ORDERS - PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS); - PLAYING_PATCH_ORDERS = this.cleanupPatches(PLAYING_PATCH_ORDERS); - - BxLogger.info(LOG_TAG, PATCH_ORDERS.slice(0)); - BxLogger.info(LOG_TAG, PLAYING_PATCH_ORDERS.slice(0)); - } } diff --git a/src/modules/patches/controller-shortcuts.js b/src/modules/patcher/patches/controller-shortcuts.js similarity index 100% rename from src/modules/patches/controller-shortcuts.js rename to src/modules/patcher/patches/controller-shortcuts.js diff --git a/src/modules/patches/expose-stream-session.js b/src/modules/patcher/patches/expose-stream-session.js similarity index 100% rename from src/modules/patches/expose-stream-session.js rename to src/modules/patcher/patches/expose-stream-session.js diff --git a/src/modules/patches/local-co-op-enable.js b/src/modules/patcher/patches/local-co-op-enable.js similarity index 100% rename from src/modules/patches/local-co-op-enable.js rename to src/modules/patcher/patches/local-co-op-enable.js diff --git a/src/modules/patches/remote-play-enable.js b/src/modules/patcher/patches/remote-play-enable.js similarity index 100% rename from src/modules/patches/remote-play-enable.js rename to src/modules/patcher/patches/remote-play-enable.js diff --git a/src/modules/patches/remote-play-keep-alive.js b/src/modules/patcher/patches/remote-play-keep-alive.js similarity index 100% rename from src/modules/patches/remote-play-keep-alive.js rename to src/modules/patcher/patches/remote-play-keep-alive.js diff --git a/src/modules/patches/set-currently-focused-interactable.js b/src/modules/patcher/patches/set-currently-focused-interactable.js similarity index 100% rename from src/modules/patches/set-currently-focused-interactable.js rename to src/modules/patcher/patches/set-currently-focused-interactable.js diff --git a/src/modules/patches/vibration-adjust.js b/src/modules/patcher/patches/vibration-adjust.js similarity index 100% rename from src/modules/patches/vibration-adjust.js rename to src/modules/patcher/patches/vibration-adjust.js diff --git a/src/modules/ui/dialog/settings-dialog.ts b/src/modules/ui/dialog/settings-dialog.ts index 39712d6..6c6942e 100755 --- a/src/modules/ui/dialog/settings-dialog.ts +++ b/src/modules/ui/dialog/settings-dialog.ts @@ -12,7 +12,7 @@ import { STATES, AppInterface, deepClone, SCRIPT_VERSION, STORAGE, SCRIPT_VARIAN import { t, Translations } from "@/utils/translation"; import { BxSelectElement } from "@/web-components/bx-select"; import { setNearby } from "@/utils/navigation-utils"; -import { PatcherCache } from "@/modules/patcher"; +import { PatcherCache } from "@/modules/patcher/patcher"; import { UserAgentProfile } from "@/enums/user-agent"; import { UserAgent } from "@/utils/user-agent"; import { BX_FLAGS } from "@/utils/bx-flags"; diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index 21daa65..7efe969 100755 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -11,6 +11,7 @@ import { getPref } from "./settings-storages/global-settings-storage"; import { GamePassCloudGallery } from "@/enums/game-pass-gallery"; import { TouchController } from "@/modules/touch-controller"; import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values"; +import { Patcher, type PatchPage } from "@/modules/patcher/patcher"; export enum SupportedInputType { CONTROLLER = 'Controller', @@ -208,5 +209,10 @@ export const BxExposed = { / /g, ], - toggleLocalCoOp: (enable: boolean) => {}, + toggleLocalCoOp(enable: boolean) {}, + + beforePageLoad: isFullVersion() ? (page: PatchPage) => { + BxLogger.info('beforePageLoad', page); + Patcher.patchPage(page); + } : () => {}, };