Optimize Patcher

This commit is contained in:
redphx 2024-12-06 18:13:24 +07:00
parent fe696043f8
commit 1ea1afe4d4
14 changed files with 207 additions and 188 deletions

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud (Lite) // @name Better xCloud (Lite)
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 6.0.0 // @version 6.0.1-beta-1
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @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(); 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 = { 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, supportedRegion: !0,
@ -4775,7 +4775,8 @@ var BxExposed = {
/ {2,}/g, / {2,}/g,
/ /g / /g
], ],
toggleLocalCoOp: (enable) => {} toggleLocalCoOp(enable) {},
beforePageLoad: () => {}
}; };
function localRedirect(path) { function localRedirect(path) {
let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); 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]); 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 { class StreamUiHandler {
static $btnStreamSettings; static $btnStreamSettings;
static $btnStreamStats; static $btnStreamStats;
@ -6422,9 +6394,6 @@ window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); 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) => { window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => {
let dataChannel = e.dataChannel; let dataChannel = e.dataChannel;
if (!dataChannel || dataChannel.label !== "message") return; if (!dataChannel || dataChannel.label !== "message") return;

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 6.0.0 // @version 6.0.1-beta-1
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @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(); 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 = { 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, supportedRegion: !0,
@ -4063,8 +4063,16 @@ class PatcherUtils {
static replaceWith(txt, index, fromString, toString) { static replaceWith(txt, index, fromString, toString) {
return txt.substring(0, index) + toString + txt.substring(index + fromString.length); 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) { disableAiTrack(str) {
let text = ".track=function(", index = str.indexOf(text); let text = ".track=function(", index = str.indexOf(text);
if (index < 0) return !1; if (index < 0) return !1;
@ -4196,11 +4204,6 @@ logFunc(logTag, '//', logMessage);
let newCode = "e.enableTouchInput = true;"; let newCode = "e.enableTouchInput = true;";
return str = str.replace(text, text + newCode), str; 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) { disableStreamGate(str) {
let index = str.indexOf('case"partially-ready":'); let index = str.indexOf('case"partially-ready":');
if (index < 0) return !1; if (index < 0) return !1;
@ -4471,10 +4474,10 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
if (index < 0) return !1; 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; 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",'); let index = str.indexOf('{location:"ProductDetailPage",');
if (index >= 0 && (index = PatcherUtils.lastIndexOf("return", str, index, 200)), index < 0) return !1; 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-details" });' + str.substring(index), str; return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str;
}, },
detectBrowserRouterReady(str) { detectBrowserRouterReady(str) {
let index = str.indexOf("{history:this.history,"); let index = str.indexOf("{history:this.history,");
@ -4501,8 +4504,17 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
let text = "=window.__PRELOADED_STATE__;"; let text = "=window.__PRELOADED_STATE__;";
if (!str.includes(text)) return !1; if (!str.includes(text)) return !1;
return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str; 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" ? [ ...getPref("nativeMkb.mode") === "on" ? [
"enableNativeMkb", "enableNativeMkb",
"exposeInputSink" "exposeInputSink"
@ -4517,6 +4529,9 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"patchGamepadPolling", "patchGamepadPolling",
"exposeStreamSession", "exposeStreamSession",
"exposeDialogRoutes", "exposeDialogRoutes",
"homePageBeforeLoad",
"productDetailPageBeforeLoad",
"streamPageBeforeLoad",
"guideAchievementsDefaultLocked", "guideAchievementsDefaultLocked",
"enableTvRoutes", "enableTvRoutes",
"supportLocalCoOp", "supportLocalCoOp",
@ -4524,10 +4539,6 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
getPref("ui.gameCard.waitTime.show") && "patchSetCurrentlyFocusedInteractable", getPref("ui.gameCard.waitTime.show") && "patchSetCurrentlyFocusedInteractable",
getPref("ui.layout") !== "default" && "websiteLayout", getPref("ui.layout") !== "default" && "websiteLayout",
getPref("game.fortnite.forceConsole") && "forceFortniteConsole", 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 ? [ ...STATES.userAgent.capabilities.touch ? [
"disableTouchContextMenu" "disableTouchContextMenu"
] : [], ] : [],
@ -4550,7 +4561,12 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"enableConsoleLogging", "enableConsoleLogging",
"enableXcloudLogger" "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", "patchXcloudTitleInfo",
"disableGamepadDisconnectedScreen", "disableGamepadDisconnectedScreen",
"patchStreamHud", "patchStreamHud",
@ -4562,7 +4578,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
...STATES.userAgent.capabilities.touch ? [ ...STATES.userAgent.capabilities.touch ? [
getPref("touchController.mode") === "all" && "patchShowSensorControls", getPref("touchController.mode") === "all" && "patchShowSensorControls",
getPref("touchController.mode") === "all" && "exposeTouchLayoutManager", 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", getPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",
"patchBabylonRendererClass" "patchBabylonRendererClass"
] : [], ] : [],
@ -4577,9 +4593,21 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"patchMouseAndKeyboardEnabled", "patchMouseAndKeyboardEnabled",
"disableNativeRequestPointerLock" "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 { 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; let nativeBind = Function.prototype.bind;
Function.prototype.bind = function() { Function.prototype.bind = function() {
let valid = !1; let valid = !1;
@ -4587,7 +4615,7 @@ class Patcher {
if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0; if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0;
} }
if (!valid) return nativeBind.apply(this, arguments); 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) => { let orgFunc = this, newFunc = (a, item2) => {
Patcher.checkChunks(item2), orgFunc(a, item2); Patcher.checkChunks(item2), orgFunc(a, item2);
}; };
@ -4609,7 +4637,7 @@ class Patcher {
if (!PATCHES[patchName]) continue; if (!PATCHES[patchName]) continue;
let tmpStr = PATCHES[patchName].call(null, patchedFuncStr); let tmpStr = PATCHES[patchName].call(null, patchedFuncStr);
if (!tmpStr) continue; 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 { if (modified) try {
chunkData[chunkId] = eval(patchedFuncStr); chunkData[chunkId] = eval(patchedFuncStr);
@ -4621,7 +4649,7 @@ class Patcher {
if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap); if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap);
} }
static init() { static init() {
Patcher.#patchFunctionBind(); Patcher.patchNativeBind();
} }
} }
class PatcherCache { class PatcherCache {
@ -4630,7 +4658,14 @@ class PatcherCache {
KEY_CACHE = "BetterXcloud.Patches.Cache"; KEY_CACHE = "BetterXcloud.Patches.Cache";
KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature"; KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature";
CACHE; 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() { getSignature() {
let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]'); let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]');
if ($link) { if ($link) {
@ -4666,12 +4701,6 @@ class PatcherCache {
} }
window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE)); 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 { class BxNumberStepper extends HTMLInputElement {
intervalId = null; intervalId = null;
@ -6936,7 +6965,10 @@ var BxExposed = {
/ {2,}/g, / {2,}/g,
/ /g / /g
], ],
toggleLocalCoOp: (enable) => {} toggleLocalCoOp(enable) {},
beforePageLoad: (page) => {
BxLogger.info("beforePageLoad", page), Patcher.patchPage(page);
}
}; };
function localRedirect(path) { function localRedirect(path) {
let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); 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); BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
}); });
window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { 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) => { window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => {
let dataChannel = e.dataChannel; let dataChannel = e.dataChannel;

View File

@ -16,7 +16,7 @@ import { LoadingScreen } from "@modules/loading-screen";
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider"; import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
import { TouchController } from "@modules/touch-controller"; import { TouchController } from "@modules/touch-controller";
import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils"; 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 { RemotePlayManager } from "@/modules/remote-play-manager";
import { onHistoryChanged, patchHistoryMethod } from "@utils/history"; import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, patchMeControl, patchPointerLockApi, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches"; 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); 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; const component = (e as any).component;
if (component === 'product-details') { if (component === 'product-detail') {
ProductDetailsPage.injectButtons(); ProductDetailsPage.injectButtons();
} }
}); });

View File

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

View File

@ -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 { BX_FLAGS } from "@utils/bx-flags";
import { BxLogger } from "@utils/bx-logger"; import { BxLogger } from "@utils/bx-logger";
import { hashCode, renderString } from "@utils/utils"; 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 { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { t } from "@/utils/translation"; import { t } from "@/utils/translation";
import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values"; import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values";
import { PatcherUtils } from "./patcher-utils.js";
type PathName = keyof typeof PATCHES; export type PatchName = keyof typeof PATCHES;
type PatchArray = PathName[]; 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 LOG_TAG = 'Patcher';
const PATCHES = { const PATCHES = {
@ -314,19 +287,6 @@ logFunc(logTag, '//', logMessage);
return str; 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 // Disable StreamGate
disableStreamGate(str: string) { disableStreamGate(str: string) {
const index = str.indexOf('case"partially-ready":'); const index = str.indexOf('case"partially-ready":');
@ -719,30 +679,6 @@ true` + text;
return str; 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) { enableTvRoutes(str: string) {
let index = str.indexOf('.LoginDeviceCode.path,'); let index = str.indexOf('.LoginDeviceCode.path,');
if (index < 0) { if (index < 0) {
@ -912,16 +848,15 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
return str; return str;
}, },
// product-details-page.js#2388, 24.17.20 detectProductDetailPage(str: string) {
detectProductDetailsPage(str: string) {
let index = str.indexOf('{location:"ProductDetailPage",'); 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) { if (index < 0) {
return false; 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; 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__);'); str = str.replace(text, '=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);');
return str; 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<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ ...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'enableNativeMkb', 'enableNativeMkb',
'exposeInputSink', 'exposeInputSink',
@ -1015,10 +962,13 @@ let PATCH_ORDERS: PatchArray = [
'exposeStreamSession', 'exposeStreamSession',
'exposeDialogRoutes', 'exposeDialogRoutes',
'homePageBeforeLoad',
'productDetailPageBeforeLoad',
'streamPageBeforeLoad',
'guideAchievementsDefaultLocked', 'guideAchievementsDefaultLocked',
'enableTvRoutes', 'enableTvRoutes',
// AppInterface && 'detectProductDetailsPage',
'supportLocalCoOp', 'supportLocalCoOp',
'overrideStorageGetSettings', 'overrideStorageGetSettings',
@ -1027,11 +977,6 @@ let PATCH_ORDERS: PatchArray = [
getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout', getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole', getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
(getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
...(STATES.userAgent.capabilities.touch ? [ ...(STATES.userAgent.capabilities.touch ? [
'disableTouchContextMenu', 'disableTouchContextMenu',
] : []), ] : []),
@ -1059,12 +1004,19 @@ let PATCH_ORDERS: PatchArray = [
'enableConsoleLogging', 'enableConsoleLogging',
'enableXcloudLogger', 'enableXcloudLogger',
] : []), ] : []),
].filter((item): item is string => !!item) as PatchArray; ]);
let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
STATES.browser.capabilities.touch && getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
(getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
]);
// Only when playing // Only when playing
// TODO: check this // TODO: check this
// @ts-ignore // @ts-ignore
let PLAYING_PATCH_ORDERS: PatchArray = [ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
'patchXcloudTitleInfo', 'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen', 'disableGamepadDisconnectedScreen',
'patchStreamHud', 'patchStreamHud',
@ -1085,7 +1037,7 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
...(STATES.userAgent.capabilities.touch ? [ ...(STATES.userAgent.capabilities.touch ? [
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls', getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls',
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager', getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager',
(getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer', (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity', getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
'patchBabylonRendererClass', 'patchBabylonRendererClass',
] : []), ] : []),
@ -1106,12 +1058,32 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
'patchMouseAndKeyboardEnabled', 'patchMouseAndKeyboardEnabled',
'disableNativeRequestPointerLock', '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 { 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; const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() { Function.prototype.bind = function() {
let valid = false; let valid = false;
@ -1132,8 +1104,6 @@ export class Patcher {
return nativeBind.apply(this, arguments); return nativeBind.apply(this, arguments);
} }
PatcherCache.getInstance().init();
if (typeof arguments[1] === 'function') { if (typeof arguments[1] === 'function') {
BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()'); BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()');
Function.prototype.bind = nativeBind; Function.prototype.bind = nativeBind;
@ -1211,6 +1181,7 @@ export class Patcher {
patchesToCheck.splice(patchIndex, 1); patchesToCheck.splice(patchIndex, 1);
patchIndex--; patchIndex--;
PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName); PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName);
BxLogger.info(LOG_TAG, 'Remaining patches', PATCH_ORDERS);
} }
// Apply patched functions // Apply patched functions
@ -1236,7 +1207,7 @@ export class Patcher {
} }
static init() { static init() {
Patcher.#patchFunctionBind(); Patcher.patchNativeBind();
} }
} }
@ -1249,7 +1220,29 @@ export class PatcherCache {
private CACHE!: { [key: string]: PatchArray }; 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 * Get patch's signature
@ -1333,30 +1326,4 @@ export class PatcherCache {
// Save to storage // Save to storage
window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE)); 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));
}
} }

View File

@ -12,7 +12,7 @@ import { STATES, AppInterface, deepClone, SCRIPT_VERSION, STORAGE, SCRIPT_VARIAN
import { t, Translations } from "@/utils/translation"; import { t, Translations } from "@/utils/translation";
import { BxSelectElement } from "@/web-components/bx-select"; import { BxSelectElement } from "@/web-components/bx-select";
import { setNearby } from "@/utils/navigation-utils"; import { setNearby } from "@/utils/navigation-utils";
import { PatcherCache } from "@/modules/patcher"; import { PatcherCache } from "@/modules/patcher/patcher";
import { UserAgentProfile } from "@/enums/user-agent"; import { UserAgentProfile } from "@/enums/user-agent";
import { UserAgent } from "@/utils/user-agent"; import { UserAgent } from "@/utils/user-agent";
import { BX_FLAGS } from "@/utils/bx-flags"; import { BX_FLAGS } from "@/utils/bx-flags";

View File

@ -11,6 +11,7 @@ import { getPref } from "./settings-storages/global-settings-storage";
import { GamePassCloudGallery } from "@/enums/game-pass-gallery"; import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { TouchController } from "@/modules/touch-controller"; import { TouchController } from "@/modules/touch-controller";
import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values"; import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
import { Patcher, type PatchPage } from "@/modules/patcher/patcher";
export enum SupportedInputType { export enum SupportedInputType {
CONTROLLER = 'Controller', CONTROLLER = 'Controller',
@ -208,5 +209,10 @@ export const BxExposed = {
/ /g, / /g,
], ],
toggleLocalCoOp: (enable: boolean) => {}, toggleLocalCoOp(enable: boolean) {},
beforePageLoad: isFullVersion() ? (page: PatchPage) => {
BxLogger.info('beforePageLoad', page);
Patcher.patchPage(page);
} : () => {},
}; };