Compare commits

...

12 Commits

15 changed files with 286 additions and 189 deletions

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud (Lite)
// @namespace https://github.com/redphx
// @version 5.9.3
// @version 5.9.5
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -105,7 +105,7 @@ class UserAgent {
});
}
}
var SCRIPT_VERSION = "5.9.3", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
var SCRIPT_VERSION = "5.9.5", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@ -114,7 +114,6 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu
gsToken: "",
isSignedIn: !1,
isPlaying: !1,
appContext: {},
browser: {
capabilities: {
touch: browserHasTouchSupport,
@ -505,6 +504,7 @@ var SUPPORTED_LANGUAGES = {
"separate-touch-controller": "Separate Touch controller & Controller #1",
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
server: "Server",
"server-locations": "Server locations",
settings: "Settings",
"settings-reload": "Reload page to reflect changes",
"settings-reload-note": "Settings in this tab only go into effect on the next page load",
@ -1149,6 +1149,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
},
server_region: {
label: t("region"),
note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),
default: "default"
},
server_bypass_restriction: {
@ -1166,6 +1167,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
options: {
default: t("default"),
"ar-SA": "العربية",
"bg-BG": "Български",
"cs-CZ": "čeština",
"da-DK": "dansk",
"de-DE": "Deutsch",
@ -1186,9 +1188,11 @@ class GlobalSettingsStorage extends BaseSettingsStore {
"pl-PL": "polski",
"pt-BR": "português (Brasil)",
"pt-PT": "português (Portugal)",
"ro-RO": "Română",
"ru-RU": "русский",
"sk-SK": "slovenčina",
"sv-SE": "svenska",
"th-TH": "ไทย",
"tr-TR": "Türkçe",
"zh-CN": "中文(简体)",
"zh-TW": "中文 (繁體)"
@ -3937,9 +3941,14 @@ class SettingsNavigationDialog extends NavigationDialog {
reloadPage() {
this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();
}
async getRecommendedSettings(deviceCode) {
async getRecommendedSettings(androidInfo) {
function normalize(str) {
return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
}
try {
let json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {};
let { brand, board, model } = androidInfo;
brand = normalize(brand), board = normalize(board), model = normalize(model);
let url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${brand}/${board}-${model}.json`, json = await (await NATIVE_FETCH(url)).json(), recommended = {};
if (json.schema_version !== 1) return null;
let scriptSettings = json.settings.script;
if (scriptSettings._base) {
@ -3997,10 +4006,7 @@ class SettingsNavigationDialog extends NavigationDialog {
}
let recommendedDevice = "";
if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {
if (BX_FLAGS.DeviceInfo.androidInfo) {
let deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board;
recommendedDevice = await this.getRecommendedSettings(deviceCode);
}
if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await this.getRecommendedSettings(BX_FLAGS.DeviceInfo.androidInfo);
}
let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;
if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on");
@ -4427,6 +4433,7 @@ class SettingsNavigationDialog extends NavigationDialog {
}
var BxExposed = {
getTitleInfo: () => STATES.currentStream.titleInfo,
modifyPreloadedState: !1,
modifyTitleInfo: !1,
setupGainNode: ($media, audioStream) => {
if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => {
@ -4890,6 +4897,8 @@ class GuideMenu {
}
observe($addedElm) {
let className = $addedElm.className;
if (!className) className = $addedElm.firstElementChild?.className ?? "";
if (!className || className.startsWith("bx-")) return;
if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;
let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
if ($selectedTab) {
@ -5951,7 +5960,7 @@ class RootDialogObserver {
if (mutation.type !== "childList") continue;
if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
let $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) RootDialogObserver.handleAddedElement($root, $addedElm);
if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
}
let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 5.9.3
// @version 5.9.5
// ==/UserScript==

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 5.9.3
// @version 5.9.5
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -107,7 +107,7 @@ class UserAgent {
});
}
}
var SCRIPT_VERSION = "5.9.3", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
var SCRIPT_VERSION = "5.9.5", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@ -116,7 +116,6 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu
gsToken: "",
isSignedIn: !1,
isPlaying: !1,
appContext: {},
browser: {
capabilities: {
touch: browserHasTouchSupport,
@ -528,6 +527,7 @@ var SUPPORTED_LANGUAGES = {
"separate-touch-controller": "Separate Touch controller & Controller #1",
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
server: "Server",
"server-locations": "Server locations",
settings: "Settings",
"settings-reload": "Reload page to reflect changes",
"settings-reload-note": "Settings in this tab only go into effect on the next page load",
@ -1172,6 +1172,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
},
server_region: {
label: t("region"),
note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),
default: "default"
},
server_bypass_restriction: {
@ -1189,6 +1190,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
options: {
default: t("default"),
"ar-SA": "العربية",
"bg-BG": "Български",
"cs-CZ": "čeština",
"da-DK": "dansk",
"de-DE": "Deutsch",
@ -1209,9 +1211,11 @@ class GlobalSettingsStorage extends BaseSettingsStore {
"pl-PL": "polski",
"pt-BR": "português (Brasil)",
"pt-PT": "português (Portugal)",
"ro-RO": "Română",
"ru-RU": "русский",
"sk-SK": "slovenčina",
"sv-SE": "svenska",
"th-TH": "ไทย",
"tr-TR": "Türkçe",
"zh-CN": "中文(简体)",
"zh-TW": "中文 (繁體)"
@ -4041,6 +4045,11 @@ var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG2 = "Patcher", PATC
let newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;";
return str.replace(text, text + newCode);
},
remotePlayWebTitle(str) {
let text = "titleTemplate:void 0,title:", index = str.indexOf(text);
if (index < 0) return !1;
return str = PatcherUtils.insertAt(str, index + text.length, `!!window.BX_REMOTE_PLAY_CONFIG ? "${t("remote-play")} - Better xCloud" :`), str;
},
blockWebRtcStatsCollector(str) {
let text = "this.shouldCollectStats=!0";
if (!str.includes(text)) return !1;
@ -4403,6 +4412,11 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
let text = "/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g";
if (!str.includes(text)) return !1;
return 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]"), str;
},
modifyPreloadedState(str) {
let text = "=window.__PRELOADED_STATE__;";
if (!str.includes(text)) return !1;
return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str;
}
}, PATCH_ORDERS = [
...getPref("native_mkb_enabled") === "on" ? [
@ -4411,6 +4425,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"disableNativeRequestPointerLock",
"exposeInputSink"
] : [],
"modifyPreloadedState",
"optimizeGameSlugGenerator",
"detectBrowserRouterReady",
"patchRequestInfoCrash",
@ -4447,6 +4462,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"remotePlayDirectConnectUrl",
"remotePlayDisableAchievementToast",
"remotePlayRecentlyUsedTitleIds",
"remotePlayWebTitle",
STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"
] : [],
...BX_FLAGS.EnableXcloudLogging ? [
@ -4486,7 +4502,7 @@ class Patcher {
if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0;
}
if (!valid) return nativeBind.apply(this, arguments);
if (PatcherCache.init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;
if (PatcherCache.getInstance().init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;
let orgFunc = this, newFunc = (a, item2) => {
Patcher.patch(item2), orgFunc(a, item2);
};
@ -4494,10 +4510,10 @@ class Patcher {
};
}
static patch(item) {
let patchesToCheck, appliedPatches, patchesMap = {};
let patchesToCheck, appliedPatches, patchesMap = {}, patcherCache = PatcherCache.getInstance();
for (let id in item[1]) {
appliedPatches = [];
let cachedPatches = PatcherCache.getPatches(id);
let cachedPatches = patcherCache.getPatches(id);
if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS);
else patchesToCheck = PATCH_ORDERS.slice(0);
if (!patchesToCheck.length) continue;
@ -4517,53 +4533,59 @@ class Patcher {
}
if (appliedPatches.length) patchesMap[id] = appliedPatches;
}
if (Object.keys(patchesMap).length) PatcherCache.saveToCache(patchesMap);
if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap);
}
static init() {
Patcher.#patchFunctionBind();
}
}
class PatcherCache {
static #KEY_CACHE = "better_xcloud_patches_cache";
static #KEY_SIGNATURE = "better_xcloud_patches_cache_signature";
static #CACHE;
static #isInitialized = !1;
static #getSignature() {
let scriptVersion = SCRIPT_VERSION, webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content, patches = JSON.stringify(ALL_PATCHES);
static instance;
static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache);
KEY_CACHE = "better_xcloud_patches_cache";
KEY_SIGNATURE = "better_xcloud_patches_cache_signature";
CACHE;
isInitialized = !1;
getSignature() {
let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]');
if ($link) {
let match = /\/client\.([^\.]+)\.js/.exec($link.href);
match && (webVersion = match[1]);
} else webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? "";
return hashCode(scriptVersion + webVersion + patches);
}
static clear() {
window.localStorage.removeItem(PatcherCache.#KEY_CACHE), PatcherCache.#CACHE = {};
clear() {
window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {};
}
static checkSignature() {
let storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0, currentSig = PatcherCache.#getSignature();
if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear();
checkSignature() {
let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature();
if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear();
else BxLogger.info(LOG_TAG2, "Signature unchanged");
}
static #cleanupPatches(patches) {
cleanupPatches(patches) {
return patches.filter((item2) => {
for (let id2 in PatcherCache.#CACHE)
if (PatcherCache.#CACHE[id2].includes(item2)) return !1;
for (let id2 in this.CACHE)
if (this.CACHE[id2].includes(item2)) return !1;
return !0;
});
}
static getPatches(id2) {
return PatcherCache.#CACHE[id2];
getPatches(id2) {
return this.CACHE[id2];
}
static saveToCache(subCache) {
saveToCache(subCache) {
for (let id2 in subCache) {
let patchNames = subCache[id2], data = PatcherCache.#CACHE[id2];
if (!data) PatcherCache.#CACHE[id2] = patchNames;
let patchNames = subCache[id2], data = this.CACHE[id2];
if (!data) this.CACHE[id2] = patchNames;
else for (let patchName of patchNames)
if (!data.includes(patchName)) data.push(patchName);
}
window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE));
window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));
}
static init() {
if (PatcherCache.#isInitialized) return;
if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, PatcherCache.#CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS);
init() {
if (this.isInitialized) return;
if (this.isInitialized = !0, this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, this.CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS);
else PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME);
PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG2, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG2, PLAYING_PATCH_ORDERS.slice(0));
PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = this.cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG2, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG2, PLAYING_PATCH_ORDERS.slice(0));
}
}
class FullscreenText {
@ -5081,9 +5103,14 @@ class SettingsNavigationDialog extends NavigationDialog {
reloadPage() {
this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();
}
async getRecommendedSettings(deviceCode) {
async getRecommendedSettings(androidInfo) {
function normalize(str) {
return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
}
try {
let json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {};
let { brand, board, model } = androidInfo;
brand = normalize(brand), board = normalize(board), model = normalize(model);
let url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${brand}/${board}-${model}.json`, json = await (await NATIVE_FETCH(url)).json(), recommended = {};
if (json.schema_version !== 1) return null;
let scriptSettings = json.settings.script;
if (scriptSettings._base) {
@ -5141,10 +5168,7 @@ class SettingsNavigationDialog extends NavigationDialog {
}
let recommendedDevice = "";
if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {
if (BX_FLAGS.DeviceInfo.androidInfo) {
let deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board;
recommendedDevice = await this.getRecommendedSettings(deviceCode);
}
if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await this.getRecommendedSettings(BX_FLAGS.DeviceInfo.androidInfo);
}
let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;
if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on");
@ -5247,7 +5271,7 @@ class SettingsNavigationDialog extends NavigationDialog {
return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked.bind(this)), $svg;
}
onGlobalSettingChanged(e) {
PatcherCache.clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
}
renderServerSetting(setting) {
let selectedValue = getPref("server_region"), continents = {
@ -5761,6 +5785,36 @@ class ControllerShortcut {
}
var BxExposed = {
getTitleInfo: () => STATES.currentStream.titleInfo,
modifyPreloadedState: (state) => {
let LOG_TAG3 = "PreloadState";
try {
state.appContext.requestInfo.userAgent = window.navigator.userAgent;
} catch (e) {
BxLogger.error(LOG_TAG3, e);
}
try {
let sigls = state.xcloud.sigls;
if (STATES.userAgent.capabilities.touch) {
let customList = TouchController.getCustomList(), allGames = sigls["29a81209-df6f-41fd-a528-2ae6b91f719c"].data.products;
customList = customList.filter((id2) => allGames.includes(id2)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);
}
} catch (e) {
BxLogger.error(LOG_TAG3, e);
}
try {
let sigls = state.xcloud.sigls;
if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
} catch (e) {
BxLogger.error(LOG_TAG3, e);
}
try {
let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;
if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play";
} catch (e) {
BxLogger.error(LOG_TAG3, e);
}
return state;
},
modifyTitleInfo: function(titleInfo) {
titleInfo = deepClone(titleInfo);
let supportedInputTypes = titleInfo.details.supportedInputTypes;
@ -6462,6 +6516,8 @@ class GuideMenu {
}
observe($addedElm) {
let className = $addedElm.className;
if (!className) className = $addedElm.firstElementChild?.className ?? "";
if (!className || className.startsWith("bx-")) return;
if (className.includes("AchievementsButton-module__progressBarContainer")) {
TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
return;
@ -6948,34 +7004,6 @@ function onHistoryChanged(e) {
if ($settings) $settings.classList.add("bx-gone");
NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
}
var LOG_TAG3 = "PreloadState";
function overridePreloadState() {
let _state;
Object.defineProperty(window, "__PRELOADED_STATE__", {
configurable: !0,
get: () => {
return _state;
},
set: (state) => {
try {
state.appContext.requestInfo.userAgent = window.navigator.userAgent;
} catch (e) {
BxLogger.error(LOG_TAG3, e);
}
if (STATES.userAgent.capabilities.touch) try {
let sigls = state.xcloud.sigls;
if ("9c86f07a-f3e8-45ad-82a0-a1f759597059" in sigls) {
let customList = TouchController.getCustomList(), allGames = sigls["29a81209-df6f-41fd-a528-2ae6b91f719c"].data.products;
customList = customList.filter((id2) => allGames.includes(id2)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);
}
if (BX_FLAGS.ForceNativeMkbTitles && "8fa264dd-124f-4af3-97e8-596fcdf4b486" in sigls) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
} catch (e) {
BxLogger.error(LOG_TAG3, e);
}
_state = state, STATES.appContext = deepClone(state.appContext);
}
});
}
function setCodecPreferences(sdp, preferredCodec) {
let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];
for (let match of matches) {
@ -7900,7 +7928,7 @@ class RootDialogObserver {
if (mutation.type !== "childList") continue;
if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
let $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) RootDialogObserver.handleAddedElement($root, $addedElm);
if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
}
let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
@ -8029,7 +8057,7 @@ window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {
function main() {
if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9");
if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager();
if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), updatePollingRate(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect();
if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), updatePollingRate(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect();
if (getPref("stream_touch_controller") === "all") TouchController.setup();
if (getPref("mkb_enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
if (getPref("ui_game_card_show_wait_time") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));

View File

@ -20,7 +20,6 @@ import { Patcher } from "@modules/patcher";
import { RemotePlayManager } from "@/modules/remote-play-manager";
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
import { VibrationManager } from "@modules/vibration-manager";
import { overridePreloadState } from "@utils/preload-state";
import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, patchMeControl, patchPointerLockApi, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
import { AppInterface, STATES } from "@utils/global";
import { BxLogger } from "@utils/bx-logger";
@ -362,7 +361,6 @@ function main() {
if (isFullVersion()) {
updatePollingRate();
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
overridePreloadState();
VibrationManager.initialSetup();

View File

@ -17,6 +17,7 @@ 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";
import { t } from "@/utils/translation.js";
type PatchArray = (keyof typeof PATCHES)[];
@ -174,17 +175,16 @@ const PATCHES = {
},
// Remote Play: change web page's title
/*
remotePlayWebTitle(str: string) {
let text = '"undefined"!==typeof e&&document.title!==e';
if (!str.includes(text)) {
let text = 'titleTemplate:void 0,title:';
const index = str.indexOf(text);
if (index < 0) {
return false;
}
const newCode = `if (window.BX_REMOTE_PLAY_CONFIG) { e = "${t('remote-play')} - ${t('better-xcloud')}"; }`;
return str.replace(text, newCode + text);
str = PatcherUtils.insertAt(str, index + text.length, `!!window.BX_REMOTE_PLAY_CONFIG ? "${t('remote-play')} - Better xCloud" :`);
return str;
},
*/
// Block WebRTC stats collector
blockWebRtcStatsCollector(str: string) {
@ -985,6 +985,16 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
return str;
},
modifyPreloadedState(str: string) {
let text = '=window.__PRELOADED_STATE__;';
if (!str.includes(text)) {
return false;
}
str = str.replace(text, '=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);');
return str;
},
};
let PATCH_ORDERS: PatchArray = [
@ -995,6 +1005,8 @@ let PATCH_ORDERS: PatchArray = [
'exposeInputSink',
] : []),
'modifyPreloadedState',
'optimizeGameSlugGenerator',
'detectBrowserRouterReady',
@ -1044,6 +1056,7 @@ let PATCH_ORDERS: PatchArray = [
'remotePlayDirectConnectUrl',
'remotePlayDisableAchievementToast',
'remotePlayRecentlyUsedTitleIds',
'remotePlayWebTitle',
STATES.userAgent.capabilities.touch && 'patchUpdateInputConfigurationAsync',
] : []),
@ -1116,7 +1129,7 @@ export class Patcher {
return nativeBind.apply(this, arguments);
}
PatcherCache.init();
PatcherCache.getInstance().init();
if (typeof arguments[1] === 'function') {
BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()');
@ -1141,11 +1154,12 @@ export class Patcher {
let appliedPatches: PatchArray;
const patchesMap: Record<string, PatchArray> = {};
const patcherCache = PatcherCache.getInstance();
for (let id in item[1]) {
appliedPatches = [];
const cachedPatches = PatcherCache.getPatches(id);
const cachedPatches = patcherCache.getPatches(id);
if (cachedPatches) {
patchesToCheck = cachedPatches.slice(0);
patchesToCheck.push(...PATCH_ORDERS);
@ -1212,7 +1226,7 @@ export class Patcher {
}
if (Object.keys(patchesMap).length) {
PatcherCache.saveToCache(patchesMap);
patcherCache.saveToCache(patchesMap);
}
}
@ -1222,51 +1236,65 @@ export class Patcher {
}
export class PatcherCache {
static #KEY_CACHE = 'better_xcloud_patches_cache';
static #KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
private static instance: PatcherCache;
public static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache());
static #CACHE: any;
private readonly KEY_CACHE = 'better_xcloud_patches_cache';
private readonly KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
static #isInitialized = false;
private CACHE: any;
private isInitialized = false;
/**
* Get patch's signature
*/
static #getSignature(): number {
private getSignature(): number {
const scriptVersion = SCRIPT_VERSION;
const webVersion = (document.querySelector<HTMLMetaElement>('meta[name=gamepass-app-version]'))?.content;
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;
}
static clear() {
clear() {
// Clear cache
window.localStorage.removeItem(PatcherCache.#KEY_CACHE);
PatcherCache.#CACHE = {};
window.localStorage.removeItem(this.KEY_CACHE);
this.CACHE = {};
}
static checkSignature() {
const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0;
const currentSig = PatcherCache.#getSignature();
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(PatcherCache.#KEY_SIGNATURE, currentSig.toString());
window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString());
PatcherCache.clear();
this.clear();
} else {
BxLogger.info(LOG_TAG, 'Signature unchanged');
}
}
static #cleanupPatches(patches: PatchArray): PatchArray {
private cleanupPatches(patches: PatchArray): PatchArray {
return patches.filter(item => {
for (const id in PatcherCache.#CACHE) {
const cached = PatcherCache.#CACHE[id];
for (const id in this.CACHE) {
const cached = this.CACHE[id];
if (cached.includes(item)) {
return false;
@ -1277,17 +1305,17 @@ export class PatcherCache {
});
}
static getPatches(id: string): PatchArray {
return PatcherCache.#CACHE[id];
getPatches(id: string): PatchArray {
return this.CACHE[id];
}
static saveToCache(subCache: Record<string, PatchArray>) {
saveToCache(subCache: Record<string, PatchArray>) {
for (const id in subCache) {
const patchNames = subCache[id];
let data = PatcherCache.#CACHE[id];
let data = this.CACHE[id];
if (!data) {
PatcherCache.#CACHE[id] = patchNames;
this.CACHE[id] = patchNames;
} else {
for (const patchName of patchNames) {
if (!data.includes(patchName)) {
@ -1298,20 +1326,20 @@ export class PatcherCache {
}
// Save to storage
window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE));
window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));
}
static init() {
if (PatcherCache.#isInitialized) {
init() {
if (this.isInitialized) {
return;
}
PatcherCache.#isInitialized = true;
this.isInitialized = true;
PatcherCache.checkSignature();
this.checkSignature();
// Read cache from storage
PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || '{}');
BxLogger.info(LOG_TAG, PatcherCache.#CACHE);
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);
@ -1320,8 +1348,8 @@ export class PatcherCache {
}
// Remove cached patches from PATCH_ORDERS & PLAYING_PATCH_ORDERS
PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS);
PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(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));

View File

@ -19,7 +19,7 @@ import { setNearby } from "@/utils/navigation-utils";
import { PatcherCache } from "@/modules/patcher";
import { UserAgentProfile } from "@/enums/user-agent";
import { UserAgent } from "@/utils/user-agent";
import { BX_FLAGS, NATIVE_FETCH } from "@/utils/bx-flags";
import { BX_FLAGS, NATIVE_FETCH, type BxFlags } from "@/utils/bx-flags";
import { copyToClipboard } from "@/utils/utils";
import { GamepadKey } from "@/enums/mkb";
import { PrefKey, StorageKey } from "@/enums/pref-keys";
@ -683,10 +683,23 @@ export class SettingsNavigationDialog extends NavigationDialog {
window.location.reload();
}
private async getRecommendedSettings(deviceCode: string): Promise<string | null> {
private async getRecommendedSettings(androidInfo: BxFlags['DeviceInfo']['androidInfo']): Promise<string | null> {
function normalize(str: string) {
return str.toLowerCase()
.trim()
.replaceAll(/\s+/g, '-')
.replaceAll(/-+/g, '-');
}
// Get recommended settings from GitHub
try {
const response = await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`);
let {brand, board, model} = androidInfo!;
brand = normalize(brand);
board = normalize(board);
model = normalize(model);
const url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${brand}/${board}-${model}.json`;
const response = await NATIVE_FETCH(url);
const json = (await response.json()) as RecommendedSettings;
const recommended: PartialRecord<PrefKey, any> = {};
@ -804,11 +817,17 @@ export class SettingsNavigationDialog extends NavigationDialog {
if (BX_FLAGS.DeviceInfo.deviceType.includes('android')) {
if (BX_FLAGS.DeviceInfo.androidInfo) {
const deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board;
recommendedDevice = await this.getRecommendedSettings(deviceCode);
recommendedDevice = await this.getRecommendedSettings(BX_FLAGS.DeviceInfo.androidInfo);
}
}
// recommendedDevice = await this.getRecommendedSettings('foster_e');
/*
recommendedDevice = await this.getRecommendedSettings({
manufacturer: 'Lenovo',
board: 'kona',
model: 'Lenovo TB-9707F',
});
*/
const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0;
@ -1034,7 +1053,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
private onGlobalSettingChanged(e: Event) {
// Clear PatcherCache;
isFullVersion() && PatcherCache.clear();
isFullVersion() && PatcherCache.getInstance().clear();
this.$btnReload.classList.add('bx-danger');

View File

@ -178,7 +178,16 @@ export class GuideMenu {
}
observe($addedElm: HTMLElement) {
const className = $addedElm.className;
let className = $addedElm.className;
// Fix custom buttons disappearing in Guide Menu (#551)
if (!className) {
className = $addedElm.firstElementChild?.className ?? '';
}
if (!className || className.startsWith('bx-')) {
return;
}
// TrueAchievements
if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) {

View File

@ -44,7 +44,6 @@ type BxStates = {
isSignedIn: boolean;
isPlaying: boolean;
appContext: any | null;
browser: {
capabilities: {

View File

@ -8,6 +8,8 @@ import { BX_FLAGS } from "./bx-flags";
import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog";
import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamTouchController } from "./settings-storages/global-settings-storage";
import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { TouchController } from "@/modules/touch-controller";
export enum SupportedInputType {
CONTROLLER = 'Controller',
@ -22,6 +24,60 @@ export type SupportedInputTypeValue = (typeof SupportedInputType)[keyof typeof S
export const BxExposed = {
getTitleInfo: () => STATES.currentStream.titleInfo,
modifyPreloadedState: isFullVersion() && ((state: any) => {
let LOG_TAG = 'PreloadState';
// Override User-Agent
try {
state.appContext.requestInfo.userAgent = window.navigator.userAgent;
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
// Add list of games with custom layouts to the official list
try {
const sigls = state.xcloud.sigls;
if (STATES.userAgent.capabilities.touch) {
// The list of custom touch controls
let customList = TouchController.getCustomList();
// Remove non-cloud games from the official list
const allGames = sigls[GamePassCloudGallery.ALL].data.products;
customList = customList.filter(id => allGames.includes(id));
// Add to the official touchlist
sigls[GamePassCloudGallery.TOUCH]?.data.products.push(...customList);
}
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
// Add forced Native MKB titles to the official list
try {
const sigls = state.xcloud.sigls;
if (BX_FLAGS.ForceNativeMkbTitles) {
// Add to the official list
sigls[GamePassCloudGallery.NATIVE_MKB]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
}
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
// Redirect to /en-US/play if visiting from an unsupported region
try {
const xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;
if (xCloud.type === 3 && xCloud.error.type === 'UnsupportedMarketError') {
// Redirect to /en-US/play
window.stop();
window.location.href = 'https://www.xbox.com/en-US/play';
}
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
return state;
}),
modifyTitleInfo: isFullVersion() && function(titleInfo: XcloudTitleInfo): XcloudTitleInfo {
// Clone the object since the original is read-only
titleInfo = deepClone(titleInfo);

View File

@ -1,6 +1,6 @@
import { BxLogger } from "./bx-logger";
type BxFlags = {
export type BxFlags = {
Debug: boolean;
CheckForUpdate: boolean;
@ -15,7 +15,10 @@ type BxFlags = {
userAgent?: string,
androidInfo?: {
manufacturer: string,
brand: string,
board: string,
model: string,
},
}
}

View File

@ -23,7 +23,6 @@ export const STATES: BxStates = {
isSignedIn: false,
isPlaying: false,
appContext: {},
browser: {
capabilities: {

View File

@ -1,56 +0,0 @@
import { deepClone, STATES } from "@utils/global";
import { BxLogger } from "./bx-logger";
import { TouchController } from "@modules/touch-controller";
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
import { BX_FLAGS } from "./bx-flags";
const LOG_TAG = 'PreloadState';
export function overridePreloadState() {
let _state: any;
Object.defineProperty(window, '__PRELOADED_STATE__', {
configurable: true,
get: () => {
return _state;
},
set: state => {
// Override User-Agent
try {
state.appContext.requestInfo.userAgent = window.navigator.userAgent;
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
// Add list of games with custom layouts to the official list
if (STATES.userAgent.capabilities.touch) {
try {
const sigls = state.xcloud.sigls;
if (GamePassCloudGallery.TOUCH in sigls) {
let customList = TouchController.getCustomList();
const allGames = sigls[GamePassCloudGallery.ALL].data.products;
// Remove non-cloud games from the list
customList = customList.filter(id => allGames.includes(id));
// Add to the official list
sigls[GamePassCloudGallery.TOUCH]?.data.products.push(...customList);
}
if (BX_FLAGS.ForceNativeMkbTitles && GamePassCloudGallery.NATIVE_MKB in sigls) {
// Add to the official list
sigls[GamePassCloudGallery.NATIVE_MKB]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
}
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
}
// @ts-ignore
_state = state;
STATES.appContext = deepClone(state.appContext);
}
});
}

View File

@ -79,7 +79,7 @@ export class RootDialogObserver {
BX_FLAGS.Debug && BxLogger.warning('RootDialog', 'added', mutation.addedNodes);
if (mutation.addedNodes.length === 1) {
const $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) {
if ($addedElm instanceof HTMLElement) {
RootDialogObserver.handleAddedElement($root, $addedElm);
}
}

View File

@ -117,6 +117,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
},
[PrefKey.SERVER_REGION]: {
label: t('region'),
note: CE('a', {target: '_blank', href: 'https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022'}, t('server-locations')),
default: 'default',
},
[PrefKey.SERVER_BYPASS_RESTRICTION]: {
@ -135,6 +136,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
options: {
default: t('default'),
'ar-SA': 'العربية',
'bg-BG': 'Български',
'cs-CZ': 'čeština',
'da-DK': 'dansk',
'de-DE': 'Deutsch',
@ -155,9 +157,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
'pl-PL': 'polski',
'pt-BR': 'português (Brasil)',
'pt-PT': 'português (Portugal)',
'ro-RO': 'Română',
'ru-RU': 'русский',
'sk-SK': 'slovenčina',
'sv-SE': 'svenska',
'th-TH': 'ไทย',
'tr-TR': 'Türkçe',
'zh-CN': '中文(简体)',
'zh-TW': '中文 (繁體)',

View File

@ -272,6 +272,7 @@ const Texts = {
"separate-touch-controller": "Separate Touch controller & Controller #1",
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
"server": "Server",
"server-locations": "Server locations",
"settings": "Settings",
"settings-reload": "Reload page to reflect changes",
"settings-reload-note": "Settings in this tab only go into effect on the next page load",