2024-05-11 21:15:22 +07:00

835 lines
24 KiB
TypeScript

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";
import { BxEvent } from "@/utils/bx-event";
type PatchArray = (keyof typeof PATCHES)[];
const ENDING_CHUNKS_PATCH_NAME = 'loadingEndingChunks';
const LOG_TAG = 'Patcher';
const PATCHES = {
// Disable ApplicationInsights.track() function
disableAiTrack(str: string) {
const text = '.track=function(';
const index = str.indexOf(text);
if (index === -1) {
return false;
}
if (str.substring(0, index + 200).includes('"AppInsightsCore')) {
return false;
}
return str.substring(0, index) + '.track=function(e){},!!function(' + str.substring(index + text.length);
},
// Set disableTelemetry() to true
disableTelemetry(str: string) {
const text = '.disableTelemetry=function(){return!1}';
if (!str.includes(text)) {
return false;
}
return str.replace(text, '.disableTelemetry=function(){return!0}');
},
disableTelemetryProvider(str: string) {
const text = 'this.enableLightweightTelemetry=!';
if (!str.includes(text)) {
return false;
}
const newCode = [
'this.trackEvent',
'this.trackPageView',
'this.trackHttpCompleted',
'this.trackHttpFailed',
'this.trackError',
'this.trackErrorLike',
'this.onTrackEvent',
'()=>{}',
].join('=');
return str.replace(text, newCode + ';' + text);
},
// Disable IndexDB logging
disableIndexDbLogging(str: string) {
const text = ',this.logsDb=new';
if (!str.includes(text)) {
return false;
}
// Replace log() with an empty function
let newCode = ',this.log=()=>{}';
return str.replace(text, newCode + text);
},
// Set custom website layout
websiteLayout(str: string) {
const text = '?"tv":"default"';
if (!str.includes(text)) {
return false;
}
const layout = getPref(PrefKey.UI_LAYOUT) === 'tv' ? 'tv' : 'default';
return str.replace(text, `?"${layout}":"${layout}"`);
},
// Replace "/direct-connect" with "/play"
remotePlayDirectConnectUrl(str: string) {
const index = str.indexOf('/direct-connect');
if (index === -1) {
return false;
}
return str.replace(str.substring(index - 9, index + 15), 'https://www.xbox.com/play');
},
remotePlayKeepAlive(str: string) {
if (!str.includes('onServerDisconnectMessage(e){')) {
return false;
}
str = str.replace('onServerDisconnectMessage(e){', `onServerDisconnectMessage(e) {
const msg = JSON.parse(e);
if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {
try {
this.sendKeepAlive();
return;
} catch (ex) { console.log(ex); }
}
`);
return str;
},
// Enable Remote Play feature
remotePlayConnectMode(str: string) {
const text = 'connectMode:"cloud-connect"';
if (!str.includes(text)) {
return false;
}
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)||''`);
},
// Disable achievement toast in Remote Play
remotePlayDisableAchievementToast(str: string) {
const text = '.AchievementUnlock:{';
if (!str.includes(text)) {
return false;
}
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
return;
}
`;
return str.replace(text, text + newCode);
},
// Disable trackEvent() function
disableTrackEvent(str: string) {
const text = 'this.trackEvent=';
if (!str.includes(text)) {
return false;
}
return str.replace(text, 'this.trackEvent=e=>{},this.uwuwu=');
},
// Block WebRTC stats collector
blockWebRtcStatsCollector(str: string) {
const text = 'this.shouldCollectStats=!0';
if (!str.includes(text)) {
return false;
}
return str.replace(text, 'this.shouldCollectStats=!1');
},
blockGamepadStatsCollector(str: string) {
const text = 'this.inputPollingIntervalStats.addValue';
if (!str.includes(text)) {
return false;
}
str = str.replace('this.inputPollingIntervalStats.addValue', '');
str = str.replace('this.inputPollingDurationStats.addValue', '');
return str;
},
enableXcloudLogger(str: string) {
const text = 'this.telemetryProvider=e}log(e,t,i){';
if (!str.includes(text)) {
return false;
}
str = str.replaceAll(text, text + 'console.log(Array.from(arguments));');
return str;
},
enableConsoleLogging(str: string) {
const text = 'static isConsoleLoggingAllowed(){';
if (!str.includes(text)) {
return false;
}
str = str.replaceAll(text, text + 'return true;');
return str;
},
// Control controller vibration
playVibration(str: string) {
const text = '}playVibration(e){';
if (!str.includes(text)) {
return false;
}
const newCode = `
if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {
return void(0);
}
if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
}
`;
VibrationManager.updateGlobalVars();
str = str.replaceAll(text, text + newCode);
return str;
},
// Override website's settings
overrideSettings(str: string) {
const index = str.indexOf(',EnableStreamGate:');
if (index === -1) {
return false;
}
// Find the next "},"
const endIndex = str.indexOf('},', index);
const newSettings = [
// 'EnableStreamGate: false',
'PwaPrompt: false',
];
const newCode = newSettings.join(',');
str = str.substring(0, endIndex) + ',' + newCode + str.substring(endIndex);
return str;
},
disableGamepadDisconnectedScreen(str: string) {
const index = str.indexOf('"GamepadDisconnected_Title",');
if (index === -1) {
return false;
}
const constIndex = str.indexOf('const', index - 30);
str = str.substring(0, constIndex) + 'e.onClose();return null;' + str.substring(constIndex);
return str;
},
patchUpdateInputConfigurationAsync(str: string) {
const text = 'async updateInputConfigurationAsync(e){';
if (!str.includes(text)) {
return false;
}
const newCode = 'e.enableTouchInput = true;';
str = str.replace(text, text + newCode);
return str;
},
// Add patches that are only needed when start playing
loadingEndingChunks(str: string) {
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);
return str;
},
// Disable StreamGate
disableStreamGate(str: string) {
const index = str.indexOf('case"partially-ready":');
if (index === -1) {
return false;
}
const bracketIndex = str.indexOf('=>{', index - 150) + 3;
str = str.substring(0, bracketIndex) + 'return 0;' + str.substring(bracketIndex);
return str;
},
exposeTouchLayoutManager(str: string) {
const text = 'this._perScopeLayoutsStream=new';
if (!str.includes(text)) {
return false;
}
str = str.replace(text, 'window.BX_EXPOSED["touchLayoutManager"] = this,' + text);
return str;
},
supportLocalCoOp(str: string) {
const text = 'this.gamepadMappingsToSend=[],';
if (!str.includes(text)) {
return false;
}
let patchstr = `
let match;
let onGamepadChangedStr = this.onGamepadChanged.toString();
onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');
eval(\`this.onGamepadChanged = function \${onGamepadChangedStr}\`);
let onGamepadInputStr = this.onGamepadInput.toString();
match = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);
if (match) {
const gamepadIndexVar = match[0];
onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', \`this.gamepadStates.get(\${gamepadIndexVar},\`);
eval(\`this.onGamepadInput = function \${onGamepadInputStr}\`);
BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');
} else {
BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');
}
`;
const newCode = `true; ${patchstr}; true,`;
str = str.replace(text, text + newCode);
return str;
},
forceFortniteConsole(str: string) {
const text = 'sendTouchInputEnabledMessage(e){';
if (!str.includes(text)) {
return false;
}
const newCode = `window.location.pathname.includes('/launch/fortnite/') && (e = false);`;
str = str.replace(text, text + newCode);
return str;
},
disableTakRenderer(str: string) {
const text = 'const{TakRenderer:';
if (!str.includes(text)) {
return false;
}
let remotePlayCode = '';
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'off' && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
remotePlayCode = `
const gamepads = window.navigator.getGamepads();
let gamepadFound = false;
for (let gamepad of gamepads) {
if (gamepad && gamepad.connected) {
gamepadFound = true;
break;
}
}
if (gamepadFound) {
return;
}
`;
}
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
${remotePlayCode}
} else {
const titleInfo = window.BX_EXPOSED.getTitleInfo();
if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) {
return;
}
}
`;
str = str.replace(text, newCode + text);
return str;
},
streamCombineSources(str: string) {
const text = 'this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen';
if (!str.includes(text)) {
return false;
}
str = str.replace(text, 'this.useCombinedAudioVideoStream=true');
return str;
},
patchStreamHud(str: string) {
const text = 'let{onCollapse';
if (!str.includes(text)) {
return false;
}
// Restore the "..." button
str = str.replace(text, 'e.guideUI = null;' + text);
// Remove the TAK Edit button when the touch controller is disabled
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off') {
str = str.replace(text, 'e.canShowTakHUD = false;' + text);
}
return str;
},
broadcastPollingMode(str: string) {
const text = '.setPollingMode=e=>{';
if (!str.includes(text)) {
return false;
}
const newCode = `
window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged(e);
`;
str = str.replace(text, text + newCode);
return str;
},
patchXcloudTitleInfo(str: string) {
const text = 'async cloudConnect';
let index = str.indexOf(text);
if (index === -1) {
return false;
}
// Find the next "{" backet
let backetIndex = str.indexOf('{', index);
// Get param name
const params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)![1];
const titleInfoVar = params.split(',')[0];
const newCode = `
${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar});
BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
`;
str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1);
return str;
},
patchRemotePlayMkb(str: string) {
const text = 'async homeConsoleConnect';
let index = str.indexOf(text);
if (index === -1) {
return false;
}
// Find the next "{" backet
let backetIndex = str.indexOf('{', index);
// Get param name
const params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)![1];
const configsVar = params.split(',')[1];
const newCode = `
Object.assign(${configsVar}.inputConfiguration, {
enableMouseInput: false,
enableKeyboardInput: false,
enableAbsoluteMouse: false,
});
BxLogger.info('patchRemotePlayMkb', ${configsVar});
`;
str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1);
return str;
},
patchAudioMediaStream(str: string) {
const text = '.srcObject=this.audioMediaStream,';
if (!str.includes(text)) {
return false;
}
const newCode = `window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),`;
str = str.replace(text, text + newCode);
return str;
},
patchCombinedAudioVideoMediaStream(str: string) {
const text = '.srcObject=this.combinedAudioVideoStream';
if (!str.includes(text)) {
return false;
}
const newCode = `,window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)`;
str = str.replace(text, text + newCode);
return str;
},
patchTouchControlDefaultOpacity(str: string) {
const text = 'opacityMultiplier:1';
if (!str.includes(text)) {
return false;
}
const opacity = (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
const newCode = `opacityMultiplier: ${opacity}`;
str = str.replace(text, newCode);
return str;
},
exposeEventTarget(str: string) {
const text ='this._eventTarget=new EventTarget';
if (!str.includes(text)) {
return false;
}
const newCode = `
window.BX_EXPOSED.eventTarget = ${text},
window.dispatchEvent(new Event('${BxEvent.STREAM_EVENT_TARGET_READY}'))
`;
str = str.replace(text, newCode);
return str;
},
exposeStreamSession(str: string) {
const text =',this._connectionType=';
if (!str.includes(text)) {
return false;
}
const newCode = `;
window.BX_EXPOSED.streamSession = this;
const orgSetMicrophoneState = this.setMicrophoneState.bind(this);
this.setMicrophoneState = (e) => {
console.log(e);
orgSetMicrophoneState(e);
};
window.dispatchEvent(new Event('${BxEvent.STREAM_SESSION_READY}'))
true` + text;
str = str.replace(text, newCode);
return str;
},
};
let PATCH_ORDERS: PatchArray = [
'disableStreamGate',
'overrideSettings',
'broadcastPollingMode',
'exposeStreamSession',
getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout',
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp',
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
...(getPref(PrefKey.BLOCK_TRACKING) ? [
'disableAiTrack',
'disableTelemetry',
'blockWebRtcStatsCollector',
'disableIndexDbLogging',
'disableTelemetryProvider',
'disableTrackEvent',
] : []),
...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
'remotePlayKeepAlive',
'remotePlayDirectConnectUrl',
'remotePlayDisableAchievementToast',
STATES.hasTouchSupport && 'patchUpdateInputConfigurationAsync',
] : []),
...(BX_FLAGS.EnableXcloudLogging ? [
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
].filter(item => !!item);
// Only when playing
let PLAYING_PATCH_ORDERS: PatchArray = [
'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen',
'patchStreamHud',
'playVibration',
'exposeEventTarget',
// Patch volume control for normal stream
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream',
// Patch volume control for combined audio+video stream
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
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',
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
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() {
const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() {
let valid = false;
// Looking for these criteria:
// - Variable name <= 2 characters
// - Has 2 params:
// - The first one is null
// - The second one is either 0 or a function
if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
if (arguments[1] === 0 || (typeof arguments[1] === 'function')) {
valid = true;
}
}
if (!valid) {
// @ts-ignore
return nativeBind.apply(this, arguments);
}
PatcherCache.init();
if (typeof arguments[1] === 'function') {
BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()');
Function.prototype.bind = nativeBind;
}
const orgFunc = this;
const newFunc = (a: any, item: any) => {
Patcher.patch(item);
orgFunc(a, item);
}
// @ts-ignore
return nativeBind.apply(newFunc, arguments);
};
}
static patch(item: [[number], { [key: string]: () => {} }]) {
// !!! Use "caches" as variable name will break touch controller???
// console.log('patch', '-----');
let patchesToCheck: PatchArray;
let appliedPatches: PatchArray;
const patchesMap: Record<string, PatchArray> = {};
for (let id in item[1]) {
appliedPatches = [];
const cachedPatches = PatcherCache.getPatches(id);
if (cachedPatches) {
patchesToCheck = cachedPatches.slice(0);
patchesToCheck.push(...PATCH_ORDERS);
} else {
patchesToCheck = PATCH_ORDERS.slice(0);
}
// Empty patch list
if (!patchesToCheck.length) {
continue;
}
const func = item[1][id];
let str = func.toString();
let modified = false;
for (let patchIndex = 0; patchIndex < patchesToCheck.length; patchIndex++) {
const patchName = patchesToCheck[patchIndex];
if (appliedPatches.indexOf(patchName) > -1) {
continue;
}
if (!PATCHES[patchName]) {
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, `${patchName}`);
appliedPatches.push(patchName);
// Remove patch
patchesToCheck.splice(patchIndex, 1);
patchIndex--;
PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName);
}
// Apply patched functions
if (modified) {
item[1][id] = eval(str);
}
// Save to cache
if (appliedPatches.length) {
patchesMap[id] = appliedPatches;
}
}
if (Object.keys(patchesMap).length) {
PatcherCache.saveToCache(patchesMap);
}
}
static init() {
Patcher.#patchFunctionBind();
}
}
export class PatcherCache {
static #KEY_CACHE = 'better_xcloud_patches_cache';
static #KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
static #CACHE: any;
static #isInitialized = false;
/**
* 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 clear() {
// Clear cache
window.localStorage.removeItem(PatcherCache.#KEY_CACHE);
PatcherCache.#CACHE = {};
}
static checkSignature() {
const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0;
const currentSig = PatcherCache.#getSignature();
if (currentSig !== parseInt(storedSig as string)) {
// Save new signature
BxLogger.warning(LOG_TAG, 'Signature changed');
window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString());
PatcherCache.clear();
} 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: Record<string, PatchArray>) {
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() {
if (PatcherCache.#isInitialized) {
return;
}
PatcherCache.#isInitialized = true;
PatcherCache.checkSignature();
// 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));
}
}