From ea57e04d4f1a637b34e80026e6166649bac8dada Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Fri, 3 May 2024 07:36:03 +0700 Subject: [PATCH] Add PatcherCache class --- src/index.ts | 2 +- src/modules/patcher.ts | 355 ++++++++++++++++++++++++----------------- src/utils/utils.ts | 22 +++ 3 files changed, 234 insertions(+), 145 deletions(-) diff --git a/src/index.ts b/src/index.ts index 70e0ce1..bdfa029 100644 --- a/src/index.ts +++ b/src/index.ts @@ -232,7 +232,7 @@ function main() { StreamStats.setupEvents(); MkbHandler.setupEvents(); - Patcher.initialize(); + Patcher.init(); disablePwa(); diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index 1521024..876af16 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -1,8 +1,13 @@ -import { STATES } from "@utils/global"; +import { SCRIPT_VERSION, STATES } from "@utils/global"; import { BX_FLAGS } from "@utils/bx-flags"; import { getPref, PrefKey } from "@utils/preferences"; import { VibrationManager } from "@modules/vibration-manager"; import { BxLogger } from "@utils/bx-logger"; +import { hashCode } from "@/utils/utils"; + +type PatchArray = (keyof typeof PATCHES)[]; + +const ENDING_CHUNKS_PATCH_NAME = 'loadingEndingChunks'; const LOG_TAG = 'Patcher'; @@ -39,15 +44,15 @@ const PATCHES = { } const newCode = [ - 'this.trackEvent', - 'this.trackPageView', - 'this.trackHttpCompleted', - 'this.trackHttpFailed', - 'this.trackError', - 'this.trackErrorLike', - 'this.onTrackEvent', - '()=>{}', - ].join('='); + 'this.trackEvent', + 'this.trackPageView', + 'this.trackHttpCompleted', + 'this.trackHttpFailed', + 'this.trackError', + 'this.trackErrorLike', + 'this.onTrackEvent', + '()=>{}', + ].join('='); return str.replace(text, newCode + ';' + text); }, @@ -110,16 +115,6 @@ const PATCHES = { return str.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`); }, - // Fix the Guide/Nexus button not working in Remote Play - remotePlayGuideWorkaround(str: string) { - const text = 'nexusButtonHandler:this.featureGates.EnableClientGuideInStream'; - if (!str.includes(text)) { - return false; - } - - return str.replace(text, `nexusButtonHandler: !window.BX_REMOTE_PLAY_CONFIG && this.featureGates.EnableClientGuideInStream`); - }, - // Disable trackEvent() function disableTrackEvent(str: string) { const text = 'this.trackEvent='; @@ -241,14 +236,13 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) { // Add patches that are only needed when start playing loadingEndingChunks(str: string) { - const text = 'Symbol("ChatSocketPlugin")'; + const text = '"FamilySagaManager"'; if (!str.includes(text)) { return false; } BxLogger.info(LOG_TAG, 'Remaining patches:', PATCH_ORDERS); PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS); - Patcher.cleanupPatches(); return str; }, @@ -427,77 +421,66 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar}); }, }; -let PATCH_ORDERS = [ - getPref(PrefKey.BLOCK_TRACKING) && [ +let PATCH_ORDERS: PatchArray = [ + 'disableStreamGate', + 'overrideSettings', + 'broadcastPollingMode', + + getPref(PrefKey.UI_LAYOUT) === 'tv' && 'tvLayout', + getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp', + getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole', + + ...(getPref(PrefKey.BLOCK_TRACKING) ? [ 'disableAiTrack', 'disableTelemetry', - ], - ['disableStreamGate'], - - ['broadcastPollingMode'], - - getPref(PrefKey.UI_LAYOUT) === 'tv' && ['tvLayout'], - - BX_FLAGS.EnableXcloudLogging && [ - 'enableConsoleLogging', - 'enableXcloudLogger', - ], - - getPref(PrefKey.LOCAL_CO_OP_ENABLED) && ['supportLocalCoOp'], - - getPref(PrefKey.BLOCK_TRACKING) && [ 'blockWebRtcStatsCollector', 'disableIndexDbLogging', - ], - getPref(PrefKey.BLOCK_TRACKING) && [ 'disableTelemetryProvider', 'disableTrackEvent', - ], + ] : []), - getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayKeepAlive'], - getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayDirectConnectUrl'], - - [ - 'overrideSettings', - ], - - getPref(PrefKey.REMOTE_PLAY_ENABLED) && STATES.hasTouchSupport && ['patchUpdateInputConfigurationAsync'], - - getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && ['forceFortniteConsole'], -]; + ...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [ + 'remotePlayKeepAlive', + 'remotePlayDirectConnectUrl', + STATES.hasTouchSupport && 'patchUpdateInputConfigurationAsync', + ] : []), + ...(BX_FLAGS.EnableXcloudLogging ? [ + 'enableConsoleLogging', + 'enableXcloudLogger', + ] : []), +].filter(item => !!item); // Only when playing -const PLAYING_PATCH_ORDERS = [ - ['patchXcloudTitleInfo'], - getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['patchRemotePlayMkb'], +let PLAYING_PATCH_ORDERS: PatchArray = [ + 'patchXcloudTitleInfo', + 'disableGamepadDisconnectedScreen', + 'patchStreamHud', + 'playVibration', - getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'], - getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'], + STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager', + STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer', - ['patchStreamHud'], + BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging', - ['playVibration'], - STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && ['exposeTouchLayoutManager'], - STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && ['disableTakRenderer'], + getPref(PrefKey.BLOCK_TRACKING) && 'blockGamepadStatsCollector', - BX_FLAGS.EnableXcloudLogging && ['enableConsoleLogging'], + getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'streamCombineSources', - getPref(PrefKey.BLOCK_TRACKING) && ['blockGamepadStatsCollector'], + ...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [ + 'patchRemotePlayMkb', + 'remotePlayConnectMode', + ] : []), +].filter(item => !!item); - [ - 'disableGamepadDisconnectedScreen', - ], - - getPref(PrefKey.STREAM_COMBINE_SOURCES) && ['streamCombineSources'], -]; +const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; export class Patcher { static #patchFunctionBind() { const nativeBind = Function.prototype.bind; - Function.prototype.bind = function() { + Function.prototype.bind = function () { let valid = false; if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) { if (arguments[1] === 0 || (typeof arguments[1] === 'function')) { @@ -517,11 +500,6 @@ export class Patcher { const orgFunc = this; const newFunc = (a: any, item: any) => { - if (Patcher.length() === 0) { - orgFunc(a, item); - return; - } - Patcher.patch(item); orgFunc(a, item); } @@ -533,96 +511,185 @@ export class Patcher { static length() { return PATCH_ORDERS.length; }; - static patch(item: any) { + static patch(item: [[number], { [key: string]: () => {} }]) { // console.log('patch', '-----'); + let patchesToCheck: PatchArray; let appliedPatches; + const caches: { [key: string]: string[] } = {}; for (let id in item[1]) { - if (PATCH_ORDERS.length <= 0) { - return; + const cachedPatches = PatcherCache.getPatches(id); + if (cachedPatches) { + patchesToCheck = cachedPatches; + patchesToCheck.push(...PATCH_ORDERS); + } else { + patchesToCheck = PATCH_ORDERS; } + // Empty patch list + if (!patchesToCheck.length) { + continue; + } + + // console.log(patchesToCheck); + appliedPatches = []; const func = item[1][id]; let str = func.toString(); - for (let groupIndex = 0; groupIndex < PATCH_ORDERS.length; groupIndex++) { - const group = PATCH_ORDERS[groupIndex]; + // console.log(id, str); + + for (let groupIndex = 0; groupIndex < patchesToCheck.length; groupIndex++) { + const patchName = patchesToCheck[groupIndex]; let modified = false; - for (let patchIndex = 0; patchIndex < group.length; patchIndex++) { - const patchName = group[patchIndex] as keyof typeof PATCHES; - if (appliedPatches.indexOf(patchName) > -1) { - continue; - } - - const patchedstr = PATCHES[patchName].call(null, str); - if (!patchedstr) { - // Only stop if the first patch is failed - if (patchIndex === 0) { - break; - } else { - continue; - } - } - - modified = true; - str = patchedstr; - - BxLogger.info(LOG_TAG, `Applied "${patchName}" patch`); - appliedPatches.push(patchName); - - // Remove patch from group - group.splice(patchIndex, 1); - patchIndex--; + if (appliedPatches.indexOf(patchName) > -1) { + continue; } + // Check function against patch + const patchedStr = PATCHES[patchName].call(null, str); + + // Not patched + if (!patchedStr) { + continue; + } + + modified = true; + str = patchedStr; + + BxLogger.info(LOG_TAG, `Applied "${patchName}" patch`); + appliedPatches.push(patchName); + + // Remove patch + patchesToCheck.splice(groupIndex, 1); + groupIndex--; + PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName); + // Apply patched functions if (modified) { item[1][id] = eval(str); } - - // Remove empty group - if (!group.length) { - PATCH_ORDERS.splice(groupIndex, 1); - groupIndex--; - } } + + // Save to cache + if (appliedPatches.length) { + caches[id] = appliedPatches; + } + } + + if (Object.keys(caches).length) { + PatcherCache.saveToCache(caches); } } - // Remove disabled patches - static cleanupPatches() { - for (let groupIndex = PATCH_ORDERS.length - 1; groupIndex >= 0; groupIndex--) { - const group = PATCH_ORDERS[groupIndex]; - if (group === false) { - PATCH_ORDERS.splice(groupIndex, 1); - continue; - } - - for (let patchIndex = group.length - 1; patchIndex >= 0; patchIndex--) { - const patchName = group[patchIndex] as keyof typeof PATCHES; - if (!PATCHES[patchName]) { - // Remove disabled patch - group.splice(patchIndex, 1); - } - } - - // Remove empty group - if (!group.length) { - PATCH_ORDERS.splice(groupIndex, 1); - } - } - } - - static initialize() { - if (window.location.pathname.includes('/play/')) { - PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS); - } else { - PATCH_ORDERS.push(['loadingEndingChunks']); - } - - Patcher.cleanupPatches(); + static init() { Patcher.#patchFunctionBind(); } } + +class PatcherCache { + static #KEY_CACHE = 'better_xcloud_patches_cache'; + static #KEY_SIGNATURE = 'better_xcloud_patches_cache_signature'; + + static #CACHE: any; + + /** + * Get patch's signature + */ + static #getSignature(): number { + const scriptVersion = SCRIPT_VERSION; + const webVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement)?.content; + const patches = JSON.stringify(ALL_PATCHES); + + // Calculate signature + const sig = hashCode(scriptVersion + webVersion + patches) + return sig; + } + + static checkSignature() { + const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0; + const currentSig = PatcherCache.#getSignature(); + + if (currentSig !== parseInt(storedSig as string)) { + BxLogger.warning(LOG_TAG, 'Signature changed'); + + // Clear cache + window.localStorage.setItem(PatcherCache.#KEY_CACHE, '{}'); + // Save new signature + window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()); + + // Refresh page + // @ts-ignore + window.location.reload(true); + + } else { + BxLogger.info(LOG_TAG, 'Signature unchanged'); + } + } + + static #cleanupPatches(patches: PatchArray): PatchArray { + return patches.filter(item => { + for (const id in PatcherCache.#CACHE) { + const cached = PatcherCache.#CACHE[id]; + + if (cached.includes(item)) { + return false; + } + } + + return true; + }); + } + + static getPatches(id: string): PatchArray { + return PatcherCache.#CACHE[id]; + } + + static saveToCache(subCache: { [key: string]: string[] }) { + for (const id in subCache) { + const patchNames = subCache[id]; + + let data = PatcherCache.#CACHE[id]; + if (!data) { + PatcherCache.#CACHE[id] = patchNames; + } else { + for (const patchName of patchNames) { + if (!data.includes(patchName)) { + data.push(patchName); + } + } + } + } + + // Save to storage + window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE)); + } + + static init() { + // Read cache from storage + PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || '{}'); + BxLogger.info(LOG_TAG, PatcherCache.#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 = PatcherCache.#cleanupPatches(PATCH_ORDERS); + PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS); + + BxLogger.info(LOG_TAG, PATCH_ORDERS.slice(0)); + BxLogger.info(LOG_TAG, PLAYING_PATCH_ORDERS.slice(0)); + } +} + +document.addEventListener('readystatechange', e => { + if (document.readyState === 'interactive') { + PatcherCache.checkSignature(); + } +}); + +PatcherCache.init(); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 70e70e4..ad3902b 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -2,6 +2,9 @@ import { PrefKey, getPref, setPref } from "@utils/preferences"; import { SCRIPT_VERSION } from "@utils/global"; import { UserAgent } from "@utils/user-agent"; +/** + * Check for update + */ export function checkForUpdate() { const CHECK_INTERVAL_SECONDS = 2 * 3600; // check every 2 hours @@ -25,6 +28,9 @@ export function checkForUpdate() { } +/** + * Disable PWA requirement on Safari + */ export function disablePwa() { const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase(); if (!userAgent) { @@ -39,3 +45,19 @@ export function disablePwa() { }); } } + + +/** + * Calculate hash code from a string + * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ + */ +export function hashCode(str: string): number { + let hash = 0; + for (let i = 0, len = str.length; i < len; i++) { + const chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32-bit integer + } + + return hash; +}