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();
MkbHandler.setupEvents();
Patcher.initialize();
Patcher.init();
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 { 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';
@ -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,72 +421,61 @@ 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'],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'],
['patchStreamHud'],
['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'],
BX_FLAGS.EnableXcloudLogging && ['enableConsoleLogging'],
getPref(PrefKey.BLOCK_TRACKING) && ['blockGamepadStatsCollector'],
[
let PLAYING_PATCH_ORDERS: PatchArray = [
'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen',
],
'patchStreamHud',
'playVibration',
getPref(PrefKey.STREAM_COMBINE_SOURCES) && ['streamCombineSources'],
];
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.BLOCK_TRACKING) && 'blockGamepadStatsCollector',
getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'streamCombineSources',
...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
'patchRemotePlayMkb',
'remotePlayConnectMode',
] : []),
].filter(item => !!item);
const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
export class Patcher {
static #patchFunctionBind() {
@ -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 {
// Check function against patch
const patchedStr = PATCHES[patchName].call(null, str);
// Not patched
if (!patchedStr) {
continue;
}
}
modified = true;
str = patchedstr;
str = patchedStr;
BxLogger.info(LOG_TAG, `Applied "${patchName}" patch`);
appliedPatches.push(patchName);
// Remove patch from group
group.splice(patchIndex, 1);
patchIndex--;
}
// 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;
}
}
// 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);
if (Object.keys(caches).length) {
PatcherCache.saveToCache(caches);
}
}
// 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();

View File

@ -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;
}