mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 07:37:19 +02:00
1345 lines
40 KiB
TypeScript
1345 lines
40 KiB
TypeScript
import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global";
|
|
import { BX_FLAGS } from "@utils/bx-flags";
|
|
import { VibrationManager } from "@modules/vibration-manager";
|
|
import { BxLogger } from "@utils/bx-logger";
|
|
import { hashCode, renderString } from "@utils/utils";
|
|
import { BxEvent } from "@/utils/bx-event";
|
|
|
|
import codeControllerShortcuts from "./patches/controller-shortcuts.js" with { type: "text" };
|
|
import codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" };
|
|
import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
|
|
import codeSetCurrentlyFocusedInteractable from "./patches/set-currently-focused-interactable.js" with { type: "text" };
|
|
import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type: "text" };
|
|
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
|
|
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
|
|
import { FeatureGates } from "@/utils/feature-gates.js";
|
|
import { UiSection } from "@/enums/ui-sections.js";
|
|
import { PrefKey } from "@/enums/pref-keys.js";
|
|
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
|
|
import { GamePassCloudGallery } from "@/enums/game-pass-gallery.js";
|
|
|
|
type PatchArray = (keyof typeof PATCHES)[];
|
|
|
|
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 PATCHES = {
|
|
// Disable ApplicationInsights.track() function
|
|
disableAiTrack(str: string) {
|
|
let text = '.track=function(';
|
|
const index = str.indexOf(text);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (PatcherUtils.indexOf(str, '"AppInsightsCore', index, 200) < 0) {
|
|
return false;
|
|
}
|
|
|
|
return PatcherUtils.replaceWith(str, index, text, '.track=function(e){},!!function(');
|
|
},
|
|
|
|
// Set disableTelemetry() to true
|
|
disableTelemetry(str: string) {
|
|
let text = '.disableTelemetry=function(){return!1}';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
return str.replace(text, '.disableTelemetry=function(){return!0}');
|
|
},
|
|
|
|
disableTelemetryProvider(str: string) {
|
|
let 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) {
|
|
let 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) {
|
|
let 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 < 0) {
|
|
return false;
|
|
}
|
|
|
|
return str.replace(str.substring(index - 9, index + 15), 'https://www.xbox.com/play');
|
|
},
|
|
|
|
remotePlayKeepAlive(str: string) {
|
|
let text = 'onServerDisconnectMessage(e){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, text + codeRemotePlayKeepAlive);
|
|
|
|
return str;
|
|
},
|
|
|
|
// Enable Remote Play feature
|
|
remotePlayConnectMode(str: string) {
|
|
let text = 'connectMode:"cloud-connect",';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
return str.replace(text, codeRemotePlayEnable);
|
|
},
|
|
|
|
// Remote Play: Disable achievement toast
|
|
remotePlayDisableAchievementToast(str: string) {
|
|
let text = '.AchievementUnlock:{';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `if (!!window.BX_REMOTE_PLAY_CONFIG) return;`;
|
|
return str.replace(text, text + newCode);
|
|
},
|
|
|
|
// Remote Play: Prevent adding "Fortnite" to the "Jump back in" list
|
|
remotePlayRecentlyUsedTitleIds(str: string) {
|
|
let text = '(e.data.recentlyUsedTitleIds)){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `if (window.BX_REMOTE_PLAY_CONFIG) return;`;
|
|
return str.replace(text, text + newCode);
|
|
},
|
|
|
|
// Remote Play: change web page's title
|
|
/*
|
|
remotePlayWebTitle(str: string) {
|
|
let text = '"undefined"!==typeof e&&document.title!==e';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `if (window.BX_REMOTE_PLAY_CONFIG) { e = "${t('remote-play')} - ${t('better-xcloud')}"; }`;
|
|
return str.replace(text, newCode + text);
|
|
},
|
|
*/
|
|
|
|
// Block WebRTC stats collector
|
|
blockWebRtcStatsCollector(str: string) {
|
|
let text = 'this.shouldCollectStats=!0';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
return str.replace(text, 'this.shouldCollectStats=!1');
|
|
},
|
|
|
|
patchPollGamepads(str: string) {
|
|
const index = str.indexOf('},this.pollGamepads=()=>{');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
const setTimeoutIndex = str.indexOf('setTimeout(this.pollGamepads', index);
|
|
if (setTimeoutIndex < 0) {
|
|
return false;
|
|
}
|
|
|
|
let codeBlock = str.substring(index, setTimeoutIndex);
|
|
|
|
// Patch polling rate
|
|
const tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150);
|
|
const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_CONTROLLER_POLLING_RATE-');
|
|
str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched);
|
|
|
|
// Block gamepad stats collecting
|
|
if (getPref(PrefKey.BLOCK_TRACKING)) {
|
|
codeBlock = codeBlock.replace('this.inputPollingIntervalStats.addValue', '');
|
|
codeBlock = codeBlock.replace('this.inputPollingDurationStats.addValue', '');
|
|
}
|
|
|
|
// Map the Share button on Xbox Series controller with the capturing screenshot feature
|
|
const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/);
|
|
if (match) {
|
|
const gamepadVar = match[1];
|
|
const newCode = renderString(codeControllerShortcuts, {
|
|
gamepadVar,
|
|
});
|
|
|
|
codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set');
|
|
}
|
|
|
|
str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex);
|
|
return str;
|
|
},
|
|
|
|
enableXcloudLogger(str: string) {
|
|
let text = 'this.telemetryProvider=e}log(e,t,r){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `
|
|
const [logTag, logLevel, logMessage] = Array.from(arguments);
|
|
const logFunc = [console.debug, console.log, console.warn, console.error][logLevel];
|
|
logFunc(logTag, '//', logMessage);
|
|
`;
|
|
|
|
str = str.replaceAll(text, text + newCode);
|
|
return str;
|
|
},
|
|
|
|
enableConsoleLogging(str: string) {
|
|
let text = 'static isConsoleLoggingAllowed(){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replaceAll(text, text + 'return true;');
|
|
return str;
|
|
},
|
|
|
|
// Control controller vibration
|
|
playVibration(str: string) {
|
|
let text = '}playVibration(e){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
VibrationManager.updateGlobalVars();
|
|
str = str.replaceAll(text, text + codeVibrationAdjust);
|
|
return str;
|
|
},
|
|
|
|
// Override website's settings
|
|
overrideSettings(str: string) {
|
|
const index = str.indexOf(',EnableStreamGate:');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
// Find the next "},"
|
|
const endIndex = str.indexOf('},', index);
|
|
|
|
let newSettings = JSON.stringify(FeatureGates);
|
|
newSettings = newSettings.substring(1, newSettings.length - 1);
|
|
|
|
const newCode = newSettings;
|
|
|
|
str = str.substring(0, endIndex) + ',' + newCode + str.substring(endIndex);
|
|
return str;
|
|
},
|
|
|
|
disableGamepadDisconnectedScreen(str: string) {
|
|
const index = str.indexOf('"GamepadDisconnected_Title",');
|
|
if (index < 0) {
|
|
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) {
|
|
let 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) {
|
|
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
|
|
disableStreamGate(str: string) {
|
|
const index = str.indexOf('case"partially-ready":');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
const bracketIndex = str.indexOf('=>{', index - 150) + 3;
|
|
|
|
str = str.substring(0, bracketIndex) + 'return 0;' + str.substring(bracketIndex);
|
|
return str;
|
|
},
|
|
|
|
exposeTouchLayoutManager(str: string) {
|
|
let text = 'this._perScopeLayoutsStream=new';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `
|
|
true;
|
|
window.BX_EXPOSED["touchLayoutManager"] = this;
|
|
window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}"));
|
|
`;
|
|
|
|
str = str.replace(text, newCode + text);
|
|
return str;
|
|
},
|
|
|
|
|
|
patchBabylonRendererClass(str: string) {
|
|
// ()=>{a.current.render(),h.current=window.requestAnimationFrame(l)
|
|
let index = str.indexOf('.current.render(),');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
// Move back a character
|
|
index -= 1;
|
|
|
|
// Get variable of the "BabylonRendererClass" object
|
|
const rendererVar = str[index];
|
|
|
|
const newCode = `
|
|
if (window.BX_EXPOSED.stopTakRendering) {
|
|
try {
|
|
document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen');
|
|
|
|
${rendererVar}.current.dispose();
|
|
} catch (e) {}
|
|
|
|
window.BX_EXPOSED.stopTakRendering = false;
|
|
return;
|
|
}
|
|
`;
|
|
|
|
str = str.substring(0, index) + newCode + str.substring(index);
|
|
return str;
|
|
},
|
|
|
|
supportLocalCoOp(str: string) {
|
|
let text = 'this.gamepadMappingsToSend=[],';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `true; ${codeLocalCoOpEnable}; true,`;
|
|
|
|
str = str.replace(text, text + newCode);
|
|
return str;
|
|
},
|
|
|
|
forceFortniteConsole(str: string) {
|
|
let 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) {
|
|
let text = 'const{TakRenderer:';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
let autoOffCode = '';
|
|
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) {
|
|
autoOffCode = 'return;';
|
|
} else if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
|
|
autoOffCode = `
|
|
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 = `
|
|
${autoOffCode}
|
|
|
|
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) {
|
|
let text = 'this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, 'this.useCombinedAudioVideoStream=true');
|
|
return str;
|
|
},
|
|
|
|
patchStreamHud(str: string) {
|
|
let text = 'let{onCollapse';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
let newCode = `
|
|
// Expose onShowStreamMenu
|
|
window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu;
|
|
// Restore the "..." button
|
|
e.guideUI = null;
|
|
`;
|
|
|
|
// Remove the TAK Edit button when the touch controller is disabled
|
|
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) {
|
|
newCode += 'e.canShowTakHUD = false;';
|
|
}
|
|
|
|
str = str.replace(text, newCode + text);
|
|
return str;
|
|
},
|
|
|
|
broadcastPollingMode(str: string) {
|
|
let text = '.setPollingMode=e=>{';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `
|
|
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()});
|
|
`;
|
|
str = str.replace(text, text + newCode);
|
|
return str;
|
|
},
|
|
|
|
patchGamepadPolling(str: string) {
|
|
let index = str.indexOf('.shouldHandleGamepadInput)())return void');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = str.indexOf('{', index - 20) + 1;
|
|
str = str.substring(0, index) + 'if (window.BX_EXPOSED.disableGamepadPolling) return;' + str.substring(index);
|
|
return str;
|
|
},
|
|
|
|
patchXcloudTitleInfo(str: string) {
|
|
let text = 'async cloudConnect';
|
|
let index = str.indexOf(text);
|
|
if (index < 0) {
|
|
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) {
|
|
let text = 'async homeConsoleConnect';
|
|
let index = str.indexOf(text);
|
|
if (index < 0) {
|
|
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) {
|
|
let 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) {
|
|
let 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) {
|
|
let 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;
|
|
},
|
|
|
|
patchShowSensorControls(str: string) {
|
|
let text = '{shouldShowSensorControls:';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||`;
|
|
|
|
str = str.replace(text, newCode);
|
|
return str;
|
|
},
|
|
|
|
/*
|
|
exposeEventTarget(str: string) {
|
|
let 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;
|
|
},
|
|
//*/
|
|
|
|
// Class with: connectAsync(), doConnectAsync(), setPlayClient()
|
|
exposeStreamSession(str: string) {
|
|
let text =',this._connectionType=';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `;
|
|
${codeExposeStreamSession}
|
|
true` + text;
|
|
|
|
str = str.replace(text, newCode);
|
|
return str;
|
|
},
|
|
|
|
skipFeedbackDialog(str: string) {
|
|
let text = 'shouldTransitionToFeedback(e){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, text + 'return !1;');
|
|
return str;
|
|
},
|
|
|
|
enableNativeMkb(str: string) {
|
|
let text = 'e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;';
|
|
if ((!str.includes(text))) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, text + 'return true;');
|
|
return str;
|
|
},
|
|
|
|
patchMouseAndKeyboardEnabled(str: string) {
|
|
let text = 'get mouseAndKeyboardEnabled(){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, text + 'return true;');
|
|
return str;
|
|
},
|
|
|
|
exposeInputSink(str: string) {
|
|
let text = 'this.controlChannel=null,this.inputChannel=null';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = 'window.BX_EXPOSED.inputSink = this;';
|
|
|
|
str = str.replace(text, newCode + text);
|
|
return str;
|
|
},
|
|
|
|
disableNativeRequestPointerLock(str: string) {
|
|
let text = 'async requestPointerLock(){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, text + 'return;');
|
|
return str;
|
|
},
|
|
|
|
// Fix crashing when RequestInfo.origin is empty
|
|
patchRequestInfoCrash(str: string) {
|
|
let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, 'if (!e) e = "https://www.xbox.com";');
|
|
return str;
|
|
},
|
|
|
|
exposeDialogRoutes(str: string) {
|
|
let text = 'return{goBack:function(){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, 'return window.BX_EXPOSED.dialogRoutes = {goBack:function(){');
|
|
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) {
|
|
let index = str.indexOf('.LoginDeviceCode.path,');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
// Find *qe* name
|
|
const match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100));
|
|
if (!match) {
|
|
return false;
|
|
}
|
|
|
|
const funcName = match[1];
|
|
|
|
// Replace *qe*'s return value
|
|
// `return a && r ?` => `return a && r || true ?`
|
|
index = str.indexOf(`const ${funcName}=e=>{`);
|
|
index > -1 && (index = str.indexOf('return ', index));
|
|
index > -1 && (index = str.indexOf('?', index));
|
|
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = str.substring(0, index) + '|| true' + str.substring(index);
|
|
return str;
|
|
},
|
|
|
|
// Don't render "Play With Friends" sections
|
|
ignorePlayWithFriendsSection(str: string) {
|
|
let index = str.indexOf('location:"PlayWithFriendsRow",');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = PatcherUtils.lastIndexOf(str, 'return', index, 50);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.replaceWith(str, index, 'return', 'return null;');
|
|
return str;
|
|
},
|
|
|
|
// Don't render "All Games" sections
|
|
ignoreAllGamesSection(str: string) {
|
|
let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = PatcherUtils.indexOf(str, 'grid:!0,', index, 1500);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = PatcherUtils.lastIndexOf(str, '(0,', index, 70);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.insertAt(str, index, 'true ? null :');
|
|
return str;
|
|
},
|
|
|
|
// home-page.js
|
|
ignorePlayWithTouchSection(str: string) {
|
|
let index = str.indexOf('("Play_With_Touch"),');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = PatcherUtils.lastIndexOf(str, 'const ', index, 30);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.insertAt(str, index, 'return null;');
|
|
return str;
|
|
},
|
|
|
|
// home-page.js
|
|
ignoreSiglSections(str: string) {
|
|
let index = str.indexOf('SiglRow-module__heroCard___');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = PatcherUtils.lastIndexOf(str, 'const[', index, 300);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS) as UiSection[];
|
|
const siglIds: GamePassCloudGallery[] = [];
|
|
|
|
const sections: PartialRecord<UiSection, GamePassCloudGallery> = {
|
|
[UiSection.NATIVE_MKB]: GamePassCloudGallery.NATIVE_MKB,
|
|
[UiSection.MOST_POPULAR]: GamePassCloudGallery.MOST_POPULAR,
|
|
};
|
|
|
|
for (const section of PREF_HIDE_SECTIONS) {
|
|
const galleryId = sections[section];
|
|
galleryId && siglIds.push(galleryId);
|
|
};
|
|
|
|
const checkSyntax = siglIds.map(item => `siglId === "${item}"`).join(' || ');
|
|
|
|
const newCode = `
|
|
if (e && e.id) {
|
|
const siglId = e.id;
|
|
if (${checkSyntax}) {
|
|
return null;
|
|
}
|
|
}
|
|
`;
|
|
str = PatcherUtils.insertAt(str, index, newCode);
|
|
return str;
|
|
},
|
|
|
|
// Override Storage.getSettings()
|
|
overrideStorageGetSettings(str: string) {
|
|
let text = '}getSetting(e){';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
const newCode = `
|
|
// console.log('setting', this.baseStorageKey, e);
|
|
if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
|
const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey];
|
|
if (e in settings) {
|
|
return settings[e];
|
|
}
|
|
}
|
|
`;
|
|
str = str.replace(text, text + newCode);
|
|
return str;
|
|
},
|
|
|
|
// game-stream.js 24.16.4
|
|
alwaysShowStreamHud(str: string) {
|
|
let index = str.indexOf(',{onShowStreamMenu:');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = str.indexOf('&&(0,', index - 100);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
const commaIndex = str.indexOf(',', index - 10);
|
|
str = str.substring(0, commaIndex) + ',true' + str.substring(index);
|
|
return str;
|
|
},
|
|
|
|
// 24225.js#4127, 24.17.11
|
|
patchSetCurrentlyFocusedInteractable(str: string) {
|
|
let index = str.indexOf('.setCurrentlyFocusedInteractable=(');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = str.indexOf('{', index) + 1;
|
|
str = str.substring(0, index) + codeSetCurrentlyFocusedInteractable + str.substring(index);
|
|
return str;
|
|
},
|
|
|
|
// product-details-page.js#2388, 24.17.20
|
|
detectProductDetailsPage(str: string) {
|
|
let index = str.indexOf('{location:"ProductDetailPage",');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = str.indexOf('return', index - 40);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index);
|
|
return str;
|
|
},
|
|
|
|
detectBrowserRouterReady(str: string) {
|
|
let text = 'BrowserRouter:()=>';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
let index = str.indexOf('{history:this.history,');
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
index = PatcherUtils.lastIndexOf(str, 'return', index, 100);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.insertAt(str, index, 'window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});');
|
|
return str;
|
|
},
|
|
|
|
// Set Achievements list's filter default to "Locked"
|
|
guideAchievementsDefaultLocked(str: string) {
|
|
let index = str.indexOf('FilterButton-module__container');
|
|
index >= 0 && (index = PatcherUtils.lastIndexOf(str, '.All', index, 150));
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.replaceWith(str, index, '.All', '.Locked');
|
|
|
|
index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"');
|
|
index >= 0 && (index = PatcherUtils.indexOf(str, '.All', index, 250));
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.replaceWith(str, index, '.All', '.Locked');
|
|
return str;
|
|
},
|
|
|
|
// Disable long touch activating context menu
|
|
disableTouchContextMenu(str: string) {
|
|
let index = str.indexOf('"ContextualCardActions-module__container');
|
|
index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index));
|
|
index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return ', index, 50));
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
str = PatcherUtils.replaceWith(str, index, 'return', 'return () => {};');
|
|
return str;
|
|
},
|
|
|
|
// Optimize Game slug generator by using cached RegEx
|
|
optimizeGameSlugGenerator(str: string) {
|
|
let text = '/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g';
|
|
if (!str.includes(text)) {
|
|
return false;
|
|
}
|
|
|
|
str = str.replace(text, 'window.BX_EXPOSED.GameSlugRegexes[0]');
|
|
str = str.replace('/ {2,}/g', 'window.BX_EXPOSED.GameSlugRegexes[1]');
|
|
str = str.replace('/ /g', 'window.BX_EXPOSED.GameSlugRegexes[2]');
|
|
|
|
return str;
|
|
},
|
|
};
|
|
|
|
let PATCH_ORDERS: PatchArray = [
|
|
...(getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' ? [
|
|
'enableNativeMkb',
|
|
'patchMouseAndKeyboardEnabled',
|
|
'disableNativeRequestPointerLock',
|
|
'exposeInputSink',
|
|
] : []),
|
|
|
|
'optimizeGameSlugGenerator',
|
|
|
|
'detectBrowserRouterReady',
|
|
'patchRequestInfoCrash',
|
|
|
|
'disableStreamGate',
|
|
'overrideSettings',
|
|
'broadcastPollingMode',
|
|
'patchGamepadPolling',
|
|
|
|
'exposeStreamSession',
|
|
'exposeDialogRoutes',
|
|
|
|
'guideAchievementsDefaultLocked',
|
|
|
|
'enableTvRoutes',
|
|
AppInterface && 'detectProductDetailsPage',
|
|
|
|
'overrideStorageGetSettings',
|
|
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
|
|
|
|
getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout',
|
|
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp',
|
|
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
|
|
|
|
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
|
|
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
|
|
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
|
|
(getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
|
|
|
|
...(STATES.userAgent.capabilities.touch ? [
|
|
'disableTouchContextMenu',
|
|
] : []),
|
|
|
|
...(getPref(PrefKey.BLOCK_TRACKING) ? [
|
|
'disableAiTrack',
|
|
'disableTelemetry',
|
|
|
|
'blockWebRtcStatsCollector',
|
|
'disableIndexDbLogging',
|
|
|
|
'disableTelemetryProvider',
|
|
] : []),
|
|
|
|
...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
|
|
'remotePlayKeepAlive',
|
|
'remotePlayDirectConnectUrl',
|
|
'remotePlayDisableAchievementToast',
|
|
'remotePlayRecentlyUsedTitleIds',
|
|
STATES.userAgent.capabilities.touch && 'patchUpdateInputConfigurationAsync',
|
|
] : []),
|
|
|
|
...(BX_FLAGS.EnableXcloudLogging ? [
|
|
'enableConsoleLogging',
|
|
'enableXcloudLogger',
|
|
] : []),
|
|
].filter(item => !!item);
|
|
|
|
// Only when playing
|
|
let PLAYING_PATCH_ORDERS: PatchArray = [
|
|
'patchXcloudTitleInfo',
|
|
'disableGamepadDisconnectedScreen',
|
|
'patchStreamHud',
|
|
'playVibration',
|
|
|
|
'alwaysShowStreamHud',
|
|
|
|
// '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',
|
|
|
|
// Skip feedback dialog
|
|
getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
|
|
|
|
...(STATES.userAgent.capabilities.touch ? [
|
|
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'patchShowSensorControls',
|
|
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'exposeTouchLayoutManager',
|
|
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
|
|
getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
|
'patchBabylonRendererClass',
|
|
] : []),
|
|
|
|
BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging',
|
|
|
|
'patchPollGamepads',
|
|
|
|
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.getInstance().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> = {};
|
|
const patcherCache = PatcherCache.getInstance();
|
|
|
|
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];
|
|
const funcStr = func.toString();
|
|
let patchedFuncStr = funcStr;
|
|
|
|
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 tmpStr = PATCHES[patchName].call(null, patchedFuncStr);
|
|
|
|
// Not patched
|
|
if (!tmpStr) {
|
|
continue;
|
|
}
|
|
|
|
modified = true;
|
|
patchedFuncStr = tmpStr;
|
|
|
|
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) {
|
|
try {
|
|
item[1][id] = eval(patchedFuncStr);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save to cache
|
|
if (appliedPatches.length) {
|
|
patchesMap[id] = appliedPatches;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(patchesMap).length) {
|
|
patcherCache.saveToCache(patchesMap);
|
|
}
|
|
}
|
|
|
|
static init() {
|
|
Patcher.#patchFunctionBind();
|
|
}
|
|
}
|
|
|
|
export class PatcherCache {
|
|
private static instance: PatcherCache;
|
|
public static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache());
|
|
|
|
private readonly KEY_CACHE = 'better_xcloud_patches_cache';
|
|
private readonly KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
|
|
|
|
private CACHE: any;
|
|
|
|
private isInitialized = false;
|
|
|
|
/**
|
|
* Get patch's signature
|
|
*/
|
|
private getSignature(): number {
|
|
const scriptVersion = SCRIPT_VERSION;
|
|
const patches = JSON.stringify(ALL_PATCHES);
|
|
|
|
// Get client.js's hash
|
|
let webVersion = '';
|
|
const $link = document.querySelector<HTMLLinkElement>('link[data-chunk="client"][href*="/client."]');
|
|
if ($link) {
|
|
const match = /\/client\.([^\.]+)\.js/.exec($link.href);
|
|
match && (webVersion = match[1]);
|
|
} else {
|
|
// Get version from <meta>
|
|
// Sometimes this value is missing
|
|
webVersion = (document.querySelector<HTMLMetaElement>('meta[name=gamepass-app-version]'))?.content ?? '';
|
|
}
|
|
|
|
// Calculate signature
|
|
const sig = hashCode(scriptVersion + webVersion + patches)
|
|
return sig;
|
|
}
|
|
|
|
clear() {
|
|
// Clear cache
|
|
window.localStorage.removeItem(this.KEY_CACHE);
|
|
this.CACHE = {};
|
|
}
|
|
|
|
private checkSignature() {
|
|
const storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0;
|
|
const currentSig = this.getSignature();
|
|
|
|
if (currentSig !== parseInt(storedSig as string)) {
|
|
// Save new signature
|
|
BxLogger.warning(LOG_TAG, 'Signature changed');
|
|
window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString());
|
|
|
|
this.clear();
|
|
} else {
|
|
BxLogger.info(LOG_TAG, 'Signature unchanged');
|
|
}
|
|
}
|
|
|
|
private cleanupPatches(patches: PatchArray): PatchArray {
|
|
return patches.filter(item => {
|
|
for (const id in this.CACHE) {
|
|
const cached = this.CACHE[id];
|
|
|
|
if (cached.includes(item)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
getPatches(id: string): PatchArray {
|
|
return this.CACHE[id];
|
|
}
|
|
|
|
saveToCache(subCache: Record<string, PatchArray>) {
|
|
for (const id in subCache) {
|
|
const patchNames = subCache[id];
|
|
|
|
let data = this.CACHE[id];
|
|
if (!data) {
|
|
this.CACHE[id] = patchNames;
|
|
} else {
|
|
for (const patchName of patchNames) {
|
|
if (!data.includes(patchName)) {
|
|
data.push(patchName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save to storage
|
|
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));
|
|
}
|
|
}
|