Add PatcherCache class

This commit is contained in:
redphx 2024-05-03 07:36:03 +07:00
parent 43e6f3083e
commit ea57e04d4f
3 changed files with 234 additions and 145 deletions

View File

@ -232,7 +232,7 @@ function main() {
StreamStats.setupEvents(); StreamStats.setupEvents();
MkbHandler.setupEvents(); MkbHandler.setupEvents();
Patcher.initialize(); Patcher.init();
disablePwa(); disablePwa();

View File

@ -1,8 +1,13 @@
import { STATES } from "@utils/global"; import { SCRIPT_VERSION, STATES } from "@utils/global";
import { BX_FLAGS } from "@utils/bx-flags"; import { BX_FLAGS } from "@utils/bx-flags";
import { getPref, PrefKey } from "@utils/preferences"; import { getPref, PrefKey } from "@utils/preferences";
import { VibrationManager } from "@modules/vibration-manager"; import { VibrationManager } from "@modules/vibration-manager";
import { BxLogger } from "@utils/bx-logger"; 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'; const LOG_TAG = 'Patcher';
@ -39,15 +44,15 @@ const PATCHES = {
} }
const newCode = [ const newCode = [
'this.trackEvent', 'this.trackEvent',
'this.trackPageView', 'this.trackPageView',
'this.trackHttpCompleted', 'this.trackHttpCompleted',
'this.trackHttpFailed', 'this.trackHttpFailed',
'this.trackError', 'this.trackError',
'this.trackErrorLike', 'this.trackErrorLike',
'this.onTrackEvent', 'this.onTrackEvent',
'()=>{}', '()=>{}',
].join('='); ].join('=');
return str.replace(text, newCode + ';' + text); 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)||''`); 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 // Disable trackEvent() function
disableTrackEvent(str: string) { disableTrackEvent(str: string) {
const text = 'this.trackEvent='; 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 // Add patches that are only needed when start playing
loadingEndingChunks(str: string) { loadingEndingChunks(str: string) {
const text = 'Symbol("ChatSocketPlugin")'; const text = '"FamilySagaManager"';
if (!str.includes(text)) { if (!str.includes(text)) {
return false; return false;
} }
BxLogger.info(LOG_TAG, 'Remaining patches:', PATCH_ORDERS); BxLogger.info(LOG_TAG, 'Remaining patches:', PATCH_ORDERS);
PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS); PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS);
Patcher.cleanupPatches();
return str; return str;
}, },
@ -427,77 +421,66 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
}, },
}; };
let PATCH_ORDERS = [ let PATCH_ORDERS: PatchArray = [
getPref(PrefKey.BLOCK_TRACKING) && [ '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', 'disableAiTrack',
'disableTelemetry', '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', 'blockWebRtcStatsCollector',
'disableIndexDbLogging', 'disableIndexDbLogging',
],
getPref(PrefKey.BLOCK_TRACKING) && [
'disableTelemetryProvider', 'disableTelemetryProvider',
'disableTrackEvent', 'disableTrackEvent',
], ] : []),
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayKeepAlive'], ...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayDirectConnectUrl'], 'remotePlayKeepAlive',
'remotePlayDirectConnectUrl',
[ STATES.hasTouchSupport && 'patchUpdateInputConfigurationAsync',
'overrideSettings', ] : []),
],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && STATES.hasTouchSupport && ['patchUpdateInputConfigurationAsync'],
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && ['forceFortniteConsole'],
];
...(BX_FLAGS.EnableXcloudLogging ? [
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
].filter(item => !!item);
// Only when playing // Only when playing
const PLAYING_PATCH_ORDERS = [ let PLAYING_PATCH_ORDERS: PatchArray = [
['patchXcloudTitleInfo'], 'patchXcloudTitleInfo',
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['patchRemotePlayMkb'], 'disableGamepadDisconnectedScreen',
'patchStreamHud',
'playVibration',
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'], STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager',
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'], STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
['patchStreamHud'], BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging',
['playVibration'], getPref(PrefKey.BLOCK_TRACKING) && 'blockGamepadStatsCollector',
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'],
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);
[ const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
'disableGamepadDisconnectedScreen',
],
getPref(PrefKey.STREAM_COMBINE_SOURCES) && ['streamCombineSources'],
];
export class Patcher { export class Patcher {
static #patchFunctionBind() { static #patchFunctionBind() {
const nativeBind = Function.prototype.bind; const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() { Function.prototype.bind = function () {
let valid = false; let valid = false;
if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) { if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
if (arguments[1] === 0 || (typeof arguments[1] === 'function')) { if (arguments[1] === 0 || (typeof arguments[1] === 'function')) {
@ -517,11 +500,6 @@ export class Patcher {
const orgFunc = this; const orgFunc = this;
const newFunc = (a: any, item: any) => { const newFunc = (a: any, item: any) => {
if (Patcher.length() === 0) {
orgFunc(a, item);
return;
}
Patcher.patch(item); Patcher.patch(item);
orgFunc(a, item); orgFunc(a, item);
} }
@ -533,96 +511,185 @@ export class Patcher {
static length() { return PATCH_ORDERS.length; }; static length() { return PATCH_ORDERS.length; };
static patch(item: any) { static patch(item: [[number], { [key: string]: () => {} }]) {
// console.log('patch', '-----'); // console.log('patch', '-----');
let patchesToCheck: PatchArray;
let appliedPatches; let appliedPatches;
const caches: { [key: string]: string[] } = {};
for (let id in item[1]) { for (let id in item[1]) {
if (PATCH_ORDERS.length <= 0) { const cachedPatches = PatcherCache.getPatches(id);
return; if (cachedPatches) {
patchesToCheck = cachedPatches;
patchesToCheck.push(...PATCH_ORDERS);
} else {
patchesToCheck = PATCH_ORDERS;
} }
// Empty patch list
if (!patchesToCheck.length) {
continue;
}
// console.log(patchesToCheck);
appliedPatches = []; appliedPatches = [];
const func = item[1][id]; const func = item[1][id];
let str = func.toString(); let str = func.toString();
for (let groupIndex = 0; groupIndex < PATCH_ORDERS.length; groupIndex++) { // console.log(id, str);
const group = PATCH_ORDERS[groupIndex];
for (let groupIndex = 0; groupIndex < patchesToCheck.length; groupIndex++) {
const patchName = patchesToCheck[groupIndex];
let modified = false; let modified = false;
for (let patchIndex = 0; patchIndex < group.length; patchIndex++) { if (appliedPatches.indexOf(patchName) > -1) {
const patchName = group[patchIndex] as keyof typeof PATCHES; continue;
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--;
} }
// 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 // Apply patched functions
if (modified) { if (modified) {
item[1][id] = eval(str); 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 init() {
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();
Patcher.#patchFunctionBind(); 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();

View File

@ -2,6 +2,9 @@ import { PrefKey, getPref, setPref } from "@utils/preferences";
import { SCRIPT_VERSION } from "@utils/global"; import { SCRIPT_VERSION } from "@utils/global";
import { UserAgent } from "@utils/user-agent"; import { UserAgent } from "@utils/user-agent";
/**
* Check for update
*/
export function checkForUpdate() { export function checkForUpdate() {
const CHECK_INTERVAL_SECONDS = 2 * 3600; // check every 2 hours 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() { export function disablePwa() {
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase(); const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
if (!userAgent) { 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;
}