mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 19:01:43 +02:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
5d177bd76c | |||
f18c5c14ed | |||
d0ceed00f8 | |||
fce8af4b3b | |||
57686f9d8e | |||
f0e7272a82 | |||
b0ecc7171b | |||
17c08792e1 | |||
e8376b52fe | |||
f6581abe34 | |||
b090d325ae | |||
ec3daa09fd | |||
b2a2e4d27e | |||
4f3430c43c | |||
15c6d3c74b | |||
b170b95145 | |||
4217b89194 | |||
38211168e9 | |||
392dc2cf86 | |||
67de264aa9 | |||
3e2c1bb2a4 | |||
5653914d19 | |||
4a8f66f2a1 | |||
70f43ba8f2 |
198
dist/better-xcloud.lite.user.js
vendored
198
dist/better-xcloud.lite.user.js
vendored
@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud (Lite)
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.9.2
|
||||
// @version 5.9.6-beta
|
||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||
// @author redphx
|
||||
// @license MIT
|
||||
@ -105,7 +105,7 @@ class UserAgent {
|
||||
});
|
||||
}
|
||||
}
|
||||
var SCRIPT_VERSION = "5.9.2", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
|
||||
var SCRIPT_VERSION = "5.9.6-beta", 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,
|
||||
@ -300,6 +299,11 @@ var SUPPORTED_LANGUAGES = {
|
||||
"confirm-reload-stream": "Do you want to refresh the stream?",
|
||||
connected: "Connected",
|
||||
"console-connect": "Connect",
|
||||
"continent-asia": "Asia",
|
||||
"continent-australia": "Australia",
|
||||
"continent-europe": "Europe",
|
||||
"continent-north-america": "North America",
|
||||
"continent-south-america": "South America",
|
||||
contrast: "Contrast",
|
||||
controller: "Controller",
|
||||
"controller-friendly-ui": "Controller-friendly UI",
|
||||
@ -500,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",
|
||||
@ -752,14 +757,16 @@ class SettingElement {
|
||||
}
|
||||
static #renderNumberStepper(key, setting, value, onChange, options = {}) {
|
||||
options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
|
||||
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => {
|
||||
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), intervalId, isHolding = !1, clearIntervalId = () => {
|
||||
intervalId && clearInterval(intervalId), intervalId = null;
|
||||
}, renderTextValue = (value2) => {
|
||||
value2 = parseInt(value2);
|
||||
let textContent = null;
|
||||
if (options.customTextValue) textContent = options.customTextValue(value2);
|
||||
if (textContent === null) textContent = value2.toString() + options.suffix;
|
||||
return textContent;
|
||||
}, updateButtonsVisibility = () => {
|
||||
$btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX);
|
||||
if ($btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX), controlValue === MIN || controlValue === MAX) clearIntervalId();
|
||||
}, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", {
|
||||
"data-type": "dec",
|
||||
type: "button",
|
||||
@ -773,7 +780,7 @@ class SettingElement {
|
||||
}, "+"));
|
||||
if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper;
|
||||
if ($range = CE("input", {
|
||||
id: `bx_setting_${key}`,
|
||||
id: `bx_inp_setting_${key}`,
|
||||
type: "range",
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
@ -800,30 +807,29 @@ class SettingElement {
|
||||
$wrapper.appendChild($markers);
|
||||
}
|
||||
updateButtonsVisibility();
|
||||
let interval, isHolding = !1, onClick = (e) => {
|
||||
if (isHolding) {
|
||||
e.preventDefault(), isHolding = !1;
|
||||
return;
|
||||
}
|
||||
let $btn = e.target, value2 = parseInt(controlValue);
|
||||
let buttonPressed = (e, $btn) => {
|
||||
let value2 = parseInt(controlValue);
|
||||
if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS);
|
||||
else value2 = Math.min(MAX, value2 + STEPS);
|
||||
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2);
|
||||
}, onMouseDown = (e) => {
|
||||
e.preventDefault(), isHolding = !0;
|
||||
let args = arguments;
|
||||
interval && clearInterval(interval), interval = window.setInterval(() => {
|
||||
e.target && BxEvent.dispatch(e.target, "click", {
|
||||
arguments: args
|
||||
});
|
||||
}, 200);
|
||||
}, onMouseUp = (e) => {
|
||||
e.preventDefault(), interval && clearInterval(interval), isHolding = !1;
|
||||
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), onChange && onChange(e, value2);
|
||||
}, onClick = (e) => {
|
||||
if (e.preventDefault(), isHolding) return;
|
||||
let $btn = e.target.closest("button");
|
||||
$btn && buttonPressed(e, $btn), clearIntervalId(), isHolding = !1;
|
||||
}, onPointerDown = (e) => {
|
||||
clearIntervalId();
|
||||
let $btn = e.target.closest("button");
|
||||
if (!$btn) return;
|
||||
isHolding = !0, e.preventDefault(), intervalId = window.setInterval((e2) => {
|
||||
buttonPressed(e2, $btn);
|
||||
}, 200), window.addEventListener("pointerup", onPointerUp, { once: !0 }), window.addEventListener("pointercancel", onPointerUp, { once: !0 });
|
||||
}, onPointerUp = (e) => {
|
||||
clearIntervalId(), isHolding = !1;
|
||||
}, onContextMenu = (e) => e.preventDefault();
|
||||
return $wrapper.setValue = (value2) => {
|
||||
$text.textContent = renderTextValue(value2), $range.value = options.reverse ? -value2 : value2;
|
||||
}, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
|
||||
focus: $range || $btnInc
|
||||
}, $wrapper.addEventListener("click", onClick), $wrapper.addEventListener("pointerdown", onPointerDown), $wrapper.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
|
||||
focus: options.hideSlider ? $btnInc : $range
|
||||
}), $wrapper;
|
||||
}
|
||||
static #METHOD_MAP = {
|
||||
@ -1143,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: {
|
||||
@ -1160,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",
|
||||
@ -1180,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": "中文 (繁體)"
|
||||
@ -2886,6 +2896,7 @@ class NavigationDialogManager {
|
||||
if (!width) return;
|
||||
if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20;
|
||||
else $label = $parent.querySelector("div");
|
||||
if ($select.querySelector("optgroup")) width -= 15;
|
||||
$label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
|
||||
}
|
||||
}
|
||||
@ -2901,7 +2912,7 @@ class NavigationDialogManager {
|
||||
else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
|
||||
if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
|
||||
} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
|
||||
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click"));
|
||||
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
|
||||
} else if (keyCode === "Escape") handled = !0, this.hide();
|
||||
if (handled) event.preventDefault(), event.stopPropagation();
|
||||
break;
|
||||
@ -2955,7 +2966,7 @@ class NavigationDialogManager {
|
||||
}
|
||||
if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
|
||||
if (releasedButton === 0) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click"));
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
|
||||
return;
|
||||
} else if (releasedButton === 1) {
|
||||
this.hide();
|
||||
@ -3930,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) {
|
||||
@ -3990,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");
|
||||
@ -4099,26 +4112,52 @@ class SettingsNavigationDialog extends NavigationDialog {
|
||||
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, $control = CE("select", {
|
||||
let selectedValue = getPref("server_region"), continents = {
|
||||
"america-north": {
|
||||
label: t("continent-north-america")
|
||||
},
|
||||
"america-south": {
|
||||
label: t("continent-south-america")
|
||||
},
|
||||
asia: {
|
||||
label: t("continent-asia")
|
||||
},
|
||||
australia: {
|
||||
label: t("continent-australia")
|
||||
},
|
||||
europe: {
|
||||
label: t("continent-europe")
|
||||
},
|
||||
other: {
|
||||
label: t("other")
|
||||
}
|
||||
}, $control = CE("select", {
|
||||
id: `bx_setting_${setting.pref}`,
|
||||
title: setting.label,
|
||||
tabindex: 0
|
||||
});
|
||||
$control.name = $control.id, $control.addEventListener("input", (e) => {
|
||||
setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e);
|
||||
}), selectedValue = getPref("server_region"), setting.options = {};
|
||||
}), setting.options = {};
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
|
||||
if (region.isDefault) {
|
||||
if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
|
||||
}
|
||||
setting.options[value] = label;
|
||||
let $option = CE("option", { value }, label), continent = continents[region.contintent];
|
||||
if (!continent.children) continent.children = [];
|
||||
continent.children.push($option);
|
||||
}
|
||||
for (let value in setting.options) {
|
||||
let label = setting.options[value], $option = CE("option", { value }, label);
|
||||
$control.appendChild($option);
|
||||
let fragment = document.createDocumentFragment(), key;
|
||||
for (key in continents) {
|
||||
let continent = continents[key];
|
||||
if (!continent.children) continue;
|
||||
fragment.appendChild(CE("optgroup", {
|
||||
label: continent.label
|
||||
}, ...continent.children));
|
||||
}
|
||||
return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
|
||||
return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
|
||||
}
|
||||
renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
|
||||
if (typeof setting === "string") setting = {
|
||||
@ -4394,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) => {
|
||||
@ -4857,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) {
|
||||
@ -5035,21 +5077,22 @@ class StreamBadges {
|
||||
}
|
||||
}
|
||||
class XcloudInterceptor {
|
||||
static SERVER_EMOJIS = {
|
||||
AustraliaEast: "🇦🇺",
|
||||
AustraliaSouthEast: "🇦🇺",
|
||||
BrazilSouth: "🇧🇷",
|
||||
EastUS: "🇺🇸",
|
||||
EastUS2: "🇺🇸",
|
||||
JapanEast: "🇯🇵",
|
||||
KoreaCentral: "🇰🇷",
|
||||
MexicoCentral: "🇲🇽",
|
||||
NorthCentralUs: "🇺🇸",
|
||||
SouthCentralUS: "🇺🇸",
|
||||
UKSouth: "🇬🇧",
|
||||
WestEurope: "🇪🇺",
|
||||
WestUS: "🇺🇸",
|
||||
WestUS2: "🇺🇸"
|
||||
static SERVER_EXTRA_INFO = {
|
||||
EastUS: ["🇺🇸", "america-north"],
|
||||
EastUS2: ["🇺🇸", "america-north"],
|
||||
NorthCentralUs: ["🇺🇸", "america-north"],
|
||||
SouthCentralUS: ["🇺🇸", "america-north"],
|
||||
WestUS: ["🇺🇸", "america-north"],
|
||||
WestUS2: ["🇺🇸", "america-north"],
|
||||
MexicoCentral: ["🇲🇽", "america-north"],
|
||||
BrazilSouth: ["🇧🇷", "america-south"],
|
||||
JapanEast: ["🇯🇵", "asia"],
|
||||
KoreaCentral: ["🇰🇷", "asia"],
|
||||
AustraliaEast: ["🇦🇺", "australia"],
|
||||
AustraliaSouthEast: ["🇦🇺", "australia"],
|
||||
SwedenCentral: ["🇸🇪", "europe"],
|
||||
UKSouth: ["🇬🇧", "europe"],
|
||||
WestEurope: ["🇪🇺", "europe"]
|
||||
};
|
||||
static async handleLogin(request, init) {
|
||||
let bypassServer = getPref("server_bypass_restriction");
|
||||
@ -5061,14 +5104,13 @@ class XcloudInterceptor {
|
||||
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
|
||||
let obj = await response.clone().json();
|
||||
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
|
||||
let serverRegex = /\/\/(\w+)\./, serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
|
||||
for (let region of obj.offeringSettings.regions) {
|
||||
let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
|
||||
for (region of obj.offeringSettings.regions) {
|
||||
let { name: regionName, name: shortName } = region;
|
||||
if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
|
||||
let match = serverRegex.exec(region.baseUri);
|
||||
if (match) {
|
||||
if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName;
|
||||
}
|
||||
if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
|
||||
else region.contintent = "other";
|
||||
region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
|
||||
}
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
|
||||
@ -5426,28 +5468,32 @@ class WebGL2Player {
|
||||
let gl = this.gl, program = this.program;
|
||||
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
|
||||
}
|
||||
drawFrame(force = !1) {
|
||||
if (!force) {
|
||||
if (this.targetFps === 0) return;
|
||||
if (this.targetFps < 60) {
|
||||
let currentTime = performance.now();
|
||||
if (currentTime - this.lastFrameTime < this.frameInterval) return;
|
||||
this.lastFrameTime = currentTime;
|
||||
}
|
||||
}
|
||||
forceDrawFrame() {
|
||||
let gl = this.gl;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
setupRendering() {
|
||||
let animate;
|
||||
let frameCallback;
|
||||
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
|
||||
let $video = this.$video;
|
||||
animate = () => {
|
||||
if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
}, this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
} else animate = () => {
|
||||
if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate);
|
||||
}, this.animFrameId = requestAnimationFrame(animate);
|
||||
frameCallback = $video.requestVideoFrameCallback.bind($video);
|
||||
} else frameCallback = requestAnimationFrame;
|
||||
let animate = () => {
|
||||
if (this.stopped) return;
|
||||
let draw = !0;
|
||||
if (this.targetFps === 0) draw = !1;
|
||||
else if (this.targetFps < 60) {
|
||||
let currentTime = performance.now();
|
||||
if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
|
||||
else this.lastFrameTime = currentTime;
|
||||
}
|
||||
if (draw) {
|
||||
let gl = this.gl;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
this.animFrameId = frameCallback(animate);
|
||||
};
|
||||
this.animFrameId = frameCallback(animate);
|
||||
}
|
||||
setupShaders() {
|
||||
BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference"));
|
||||
@ -5914,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);
|
||||
|
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.9.2
|
||||
// @version 5.9.5
|
||||
// ==/UserScript==
|
||||
|
341
dist/better-xcloud.user.js
vendored
341
dist/better-xcloud.user.js
vendored
@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.9.2
|
||||
// @version 5.9.6-beta
|
||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||
// @author redphx
|
||||
// @license MIT
|
||||
@ -107,7 +107,7 @@ class UserAgent {
|
||||
});
|
||||
}
|
||||
}
|
||||
var SCRIPT_VERSION = "5.9.2", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
|
||||
var SCRIPT_VERSION = "5.9.6-beta", 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,
|
||||
@ -323,6 +322,11 @@ var SUPPORTED_LANGUAGES = {
|
||||
"confirm-reload-stream": "Do you want to refresh the stream?",
|
||||
connected: "Connected",
|
||||
"console-connect": "Connect",
|
||||
"continent-asia": "Asia",
|
||||
"continent-australia": "Australia",
|
||||
"continent-europe": "Europe",
|
||||
"continent-north-america": "North America",
|
||||
"continent-south-america": "South America",
|
||||
contrast: "Contrast",
|
||||
controller: "Controller",
|
||||
"controller-friendly-ui": "Controller-friendly UI",
|
||||
@ -523,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",
|
||||
@ -775,14 +780,16 @@ class SettingElement {
|
||||
}
|
||||
static #renderNumberStepper(key, setting, value, onChange, options = {}) {
|
||||
options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
|
||||
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => {
|
||||
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), intervalId, isHolding = !1, clearIntervalId = () => {
|
||||
intervalId && clearInterval(intervalId), intervalId = null;
|
||||
}, renderTextValue = (value2) => {
|
||||
value2 = parseInt(value2);
|
||||
let textContent = null;
|
||||
if (options.customTextValue) textContent = options.customTextValue(value2);
|
||||
if (textContent === null) textContent = value2.toString() + options.suffix;
|
||||
return textContent;
|
||||
}, updateButtonsVisibility = () => {
|
||||
$btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX);
|
||||
if ($btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX), controlValue === MIN || controlValue === MAX) clearIntervalId();
|
||||
}, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", {
|
||||
"data-type": "dec",
|
||||
type: "button",
|
||||
@ -796,7 +803,7 @@ class SettingElement {
|
||||
}, "+"));
|
||||
if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper;
|
||||
if ($range = CE("input", {
|
||||
id: `bx_setting_${key}`,
|
||||
id: `bx_inp_setting_${key}`,
|
||||
type: "range",
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
@ -823,30 +830,29 @@ class SettingElement {
|
||||
$wrapper.appendChild($markers);
|
||||
}
|
||||
updateButtonsVisibility();
|
||||
let interval, isHolding = !1, onClick = (e) => {
|
||||
if (isHolding) {
|
||||
e.preventDefault(), isHolding = !1;
|
||||
return;
|
||||
}
|
||||
let $btn = e.target, value2 = parseInt(controlValue);
|
||||
let buttonPressed = (e, $btn) => {
|
||||
let value2 = parseInt(controlValue);
|
||||
if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS);
|
||||
else value2 = Math.min(MAX, value2 + STEPS);
|
||||
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2);
|
||||
}, onMouseDown = (e) => {
|
||||
e.preventDefault(), isHolding = !0;
|
||||
let args = arguments;
|
||||
interval && clearInterval(interval), interval = window.setInterval(() => {
|
||||
e.target && BxEvent.dispatch(e.target, "click", {
|
||||
arguments: args
|
||||
});
|
||||
}, 200);
|
||||
}, onMouseUp = (e) => {
|
||||
e.preventDefault(), interval && clearInterval(interval), isHolding = !1;
|
||||
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), onChange && onChange(e, value2);
|
||||
}, onClick = (e) => {
|
||||
if (e.preventDefault(), isHolding) return;
|
||||
let $btn = e.target.closest("button");
|
||||
$btn && buttonPressed(e, $btn), clearIntervalId(), isHolding = !1;
|
||||
}, onPointerDown = (e) => {
|
||||
clearIntervalId();
|
||||
let $btn = e.target.closest("button");
|
||||
if (!$btn) return;
|
||||
isHolding = !0, e.preventDefault(), intervalId = window.setInterval((e2) => {
|
||||
buttonPressed(e2, $btn);
|
||||
}, 200), window.addEventListener("pointerup", onPointerUp, { once: !0 }), window.addEventListener("pointercancel", onPointerUp, { once: !0 });
|
||||
}, onPointerUp = (e) => {
|
||||
clearIntervalId(), isHolding = !1;
|
||||
}, onContextMenu = (e) => e.preventDefault();
|
||||
return $wrapper.setValue = (value2) => {
|
||||
$text.textContent = renderTextValue(value2), $range.value = options.reverse ? -value2 : value2;
|
||||
}, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
|
||||
focus: $range || $btnInc
|
||||
}, $wrapper.addEventListener("click", onClick), $wrapper.addEventListener("pointerdown", onPointerDown), $wrapper.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
|
||||
focus: options.hideSlider ? $btnInc : $range
|
||||
}), $wrapper;
|
||||
}
|
||||
static #METHOD_MAP = {
|
||||
@ -1166,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: {
|
||||
@ -1183,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",
|
||||
@ -1203,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": "中文 (繁體)"
|
||||
@ -1842,7 +1852,7 @@ class ScreenshotManager {
|
||||
if (!$player || !$player.isConnected) return;
|
||||
$player.parentElement.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot");
|
||||
let canvasContext = this.canvasContext;
|
||||
if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().drawFrame(!0);
|
||||
if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();
|
||||
if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {
|
||||
let data = $canvas.toDataURL("image/png").split(";base64,")[1];
|
||||
AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
|
||||
@ -3167,6 +3177,7 @@ class NavigationDialogManager {
|
||||
if (!width) return;
|
||||
if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20;
|
||||
else $label = $parent.querySelector("div");
|
||||
if ($select.querySelector("optgroup")) width -= 15;
|
||||
$label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
|
||||
}
|
||||
}
|
||||
@ -3182,7 +3193,7 @@ class NavigationDialogManager {
|
||||
else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
|
||||
if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
|
||||
} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
|
||||
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click"));
|
||||
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
|
||||
} else if (keyCode === "Escape") handled = !0, this.hide();
|
||||
if (handled) event.preventDefault(), event.stopPropagation();
|
||||
break;
|
||||
@ -3236,7 +3247,7 @@ class NavigationDialogManager {
|
||||
}
|
||||
if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
|
||||
if (releasedButton === 0) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click"));
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
|
||||
return;
|
||||
} else if (releasedButton === 1) {
|
||||
this.hide();
|
||||
@ -4034,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;
|
||||
@ -4383,9 +4399,9 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
},
|
||||
guideAchievementsDefaultLocked(str) {
|
||||
let index = str.indexOf("FilterButton-module__container");
|
||||
if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, ".All", index, 150)), index < 0) return !1;
|
||||
if (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)), index < 0) return !1;
|
||||
return str = PatcherUtils.replaceWith(str, index, ".All", ".Locked"), str;
|
||||
if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150)), index < 0) return !1;
|
||||
if (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)), index < 0) return !1;
|
||||
return str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), str;
|
||||
},
|
||||
disableTouchContextMenu(str) {
|
||||
let index = str.indexOf('"ContextualCardActions-module__container');
|
||||
@ -4396,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" ? [
|
||||
@ -4404,6 +4425,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
"disableNativeRequestPointerLock",
|
||||
"exposeInputSink"
|
||||
] : [],
|
||||
"modifyPreloadedState",
|
||||
"optimizeGameSlugGenerator",
|
||||
"detectBrowserRouterReady",
|
||||
"patchRequestInfoCrash",
|
||||
@ -4440,6 +4462,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
"remotePlayDirectConnectUrl",
|
||||
"remotePlayDisableAchievementToast",
|
||||
"remotePlayRecentlyUsedTitleIds",
|
||||
"remotePlayWebTitle",
|
||||
STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"
|
||||
] : [],
|
||||
...BX_FLAGS.EnableXcloudLogging ? [
|
||||
@ -4479,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);
|
||||
};
|
||||
@ -4487,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;
|
||||
@ -4510,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 {
|
||||
@ -5074,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) {
|
||||
@ -5134,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");
|
||||
@ -5240,29 +5271,55 @@ 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, $control = CE("select", {
|
||||
let selectedValue = getPref("server_region"), continents = {
|
||||
"america-north": {
|
||||
label: t("continent-north-america")
|
||||
},
|
||||
"america-south": {
|
||||
label: t("continent-south-america")
|
||||
},
|
||||
asia: {
|
||||
label: t("continent-asia")
|
||||
},
|
||||
australia: {
|
||||
label: t("continent-australia")
|
||||
},
|
||||
europe: {
|
||||
label: t("continent-europe")
|
||||
},
|
||||
other: {
|
||||
label: t("other")
|
||||
}
|
||||
}, $control = CE("select", {
|
||||
id: `bx_setting_${setting.pref}`,
|
||||
title: setting.label,
|
||||
tabindex: 0
|
||||
});
|
||||
$control.name = $control.id, $control.addEventListener("input", (e) => {
|
||||
setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e);
|
||||
}), selectedValue = getPref("server_region"), setting.options = {};
|
||||
}), setting.options = {};
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
|
||||
if (region.isDefault) {
|
||||
if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
|
||||
}
|
||||
setting.options[value] = label;
|
||||
let $option = CE("option", { value }, label), continent = continents[region.contintent];
|
||||
if (!continent.children) continent.children = [];
|
||||
continent.children.push($option);
|
||||
}
|
||||
for (let value in setting.options) {
|
||||
let label = setting.options[value], $option = CE("option", { value }, label);
|
||||
$control.appendChild($option);
|
||||
let fragment = document.createDocumentFragment(), key;
|
||||
for (key in continents) {
|
||||
let continent = continents[key];
|
||||
if (!continent.children) continue;
|
||||
fragment.appendChild(CE("optgroup", {
|
||||
label: continent.label
|
||||
}, ...continent.children));
|
||||
}
|
||||
return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
|
||||
return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
|
||||
}
|
||||
renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
|
||||
if (typeof setting === "string") setting = {
|
||||
@ -5728,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;
|
||||
@ -6429,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;
|
||||
@ -6612,21 +6701,22 @@ class StreamBadges {
|
||||
static setupEvents() {}
|
||||
}
|
||||
class XcloudInterceptor {
|
||||
static SERVER_EMOJIS = {
|
||||
AustraliaEast: "🇦🇺",
|
||||
AustraliaSouthEast: "🇦🇺",
|
||||
BrazilSouth: "🇧🇷",
|
||||
EastUS: "🇺🇸",
|
||||
EastUS2: "🇺🇸",
|
||||
JapanEast: "🇯🇵",
|
||||
KoreaCentral: "🇰🇷",
|
||||
MexicoCentral: "🇲🇽",
|
||||
NorthCentralUs: "🇺🇸",
|
||||
SouthCentralUS: "🇺🇸",
|
||||
UKSouth: "🇬🇧",
|
||||
WestEurope: "🇪🇺",
|
||||
WestUS: "🇺🇸",
|
||||
WestUS2: "🇺🇸"
|
||||
static SERVER_EXTRA_INFO = {
|
||||
EastUS: ["🇺🇸", "america-north"],
|
||||
EastUS2: ["🇺🇸", "america-north"],
|
||||
NorthCentralUs: ["🇺🇸", "america-north"],
|
||||
SouthCentralUS: ["🇺🇸", "america-north"],
|
||||
WestUS: ["🇺🇸", "america-north"],
|
||||
WestUS2: ["🇺🇸", "america-north"],
|
||||
MexicoCentral: ["🇲🇽", "america-north"],
|
||||
BrazilSouth: ["🇧🇷", "america-south"],
|
||||
JapanEast: ["🇯🇵", "asia"],
|
||||
KoreaCentral: ["🇰🇷", "asia"],
|
||||
AustraliaEast: ["🇦🇺", "australia"],
|
||||
AustraliaSouthEast: ["🇦🇺", "australia"],
|
||||
SwedenCentral: ["🇸🇪", "europe"],
|
||||
UKSouth: ["🇬🇧", "europe"],
|
||||
WestEurope: ["🇪🇺", "europe"]
|
||||
};
|
||||
static async handleLogin(request, init) {
|
||||
let bypassServer = getPref("server_bypass_restriction");
|
||||
@ -6638,14 +6728,13 @@ class XcloudInterceptor {
|
||||
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
|
||||
let obj = await response.clone().json();
|
||||
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
|
||||
let serverRegex = /\/\/(\w+)\./, serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
|
||||
for (let region of obj.offeringSettings.regions) {
|
||||
let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
|
||||
for (region of obj.offeringSettings.regions) {
|
||||
let { name: regionName, name: shortName } = region;
|
||||
if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
|
||||
let match = serverRegex.exec(region.baseUri);
|
||||
if (match) {
|
||||
if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName;
|
||||
}
|
||||
if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
|
||||
else region.contintent = "other";
|
||||
region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
|
||||
}
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
|
||||
@ -6915,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) {
|
||||
@ -7042,28 +7103,32 @@ class WebGL2Player {
|
||||
let gl = this.gl, program = this.program;
|
||||
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
|
||||
}
|
||||
drawFrame(force = !1) {
|
||||
if (!force) {
|
||||
if (this.targetFps === 0) return;
|
||||
if (this.targetFps < 60) {
|
||||
let currentTime = performance.now();
|
||||
if (currentTime - this.lastFrameTime < this.frameInterval) return;
|
||||
this.lastFrameTime = currentTime;
|
||||
}
|
||||
}
|
||||
forceDrawFrame() {
|
||||
let gl = this.gl;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
setupRendering() {
|
||||
let animate;
|
||||
let frameCallback;
|
||||
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
|
||||
let $video = this.$video;
|
||||
animate = () => {
|
||||
if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
}, this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
} else animate = () => {
|
||||
if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate);
|
||||
}, this.animFrameId = requestAnimationFrame(animate);
|
||||
frameCallback = $video.requestVideoFrameCallback.bind($video);
|
||||
} else frameCallback = requestAnimationFrame;
|
||||
let animate = () => {
|
||||
if (this.stopped) return;
|
||||
let draw = !0;
|
||||
if (this.targetFps === 0) draw = !1;
|
||||
else if (this.targetFps < 60) {
|
||||
let currentTime = performance.now();
|
||||
if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
|
||||
else this.lastFrameTime = currentTime;
|
||||
}
|
||||
if (draw) {
|
||||
let gl = this.gl;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
this.animFrameId = frameCallback(animate);
|
||||
};
|
||||
this.animFrameId = frameCallback(animate);
|
||||
}
|
||||
setupShaders() {
|
||||
BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference"));
|
||||
@ -7863,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);
|
||||
@ -7992,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));
|
||||
|
@ -10,8 +10,8 @@
|
||||
"build": "build.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.11",
|
||||
"@types/node": "^22.7.8",
|
||||
"@types/bun": "^1.1.12",
|
||||
"@types/node": "^22.7.9",
|
||||
"@types/stylus": "^0.48.43",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-plugin-compat": "^6.0.1",
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) {
|
||||
@ -942,20 +942,20 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
// 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));
|
||||
index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150));
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = PatcherUtils.replaceWith(str, index, '.All', '.Locked');
|
||||
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));
|
||||
index >= 0 && (index = PatcherUtils.indexOf(str, '"All"', index, 250));
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = PatcherUtils.replaceWith(str, index, '.All', '.Locked');
|
||||
str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"');
|
||||
return str;
|
||||
},
|
||||
|
||||
@ -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));
|
||||
|
@ -94,52 +94,52 @@ export class WebGL2Player {
|
||||
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
|
||||
}
|
||||
|
||||
drawFrame(force=false) {
|
||||
if (!force) {
|
||||
// Don't draw when FPS is 0
|
||||
if (this.targetFps === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit FPS
|
||||
if (this.targetFps < 60) {
|
||||
const currentTime = performance.now();
|
||||
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
||||
if (timeSinceLastFrame < this.frameInterval) {
|
||||
return;
|
||||
}
|
||||
this.lastFrameTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
forceDrawFrame() {
|
||||
const gl = this.gl!;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
private setupRendering() {
|
||||
let animate: any;
|
||||
|
||||
let frameCallback: any;
|
||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||
const $video = this.$video;
|
||||
animate = () => {
|
||||
if (!this.stopped) {
|
||||
this.drawFrame();
|
||||
this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
}
|
||||
}
|
||||
|
||||
this.animFrameId = $video.requestVideoFrameCallback(animate);
|
||||
frameCallback = $video.requestVideoFrameCallback.bind($video);
|
||||
} else {
|
||||
animate = () => {
|
||||
if (!this.stopped) {
|
||||
this.drawFrame();
|
||||
this.animFrameId = requestAnimationFrame(animate);
|
||||
frameCallback = requestAnimationFrame;
|
||||
}
|
||||
|
||||
let animate = () => {
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
let draw = true;
|
||||
|
||||
// Don't draw when FPS is 0
|
||||
if (this.targetFps === 0) {
|
||||
draw = false;
|
||||
} else if (this.targetFps < 60) {
|
||||
// Limit FPS
|
||||
const currentTime = performance.now();
|
||||
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
||||
if (timeSinceLastFrame < this.frameInterval) {
|
||||
draw = false;
|
||||
} else {
|
||||
this.lastFrameTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
this.animFrameId = requestAnimationFrame(animate);
|
||||
if (draw) {
|
||||
const gl = this.gl!;
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
this.animFrameId = frameCallback(animate);
|
||||
}
|
||||
|
||||
this.animFrameId = frameCallback(animate);
|
||||
}
|
||||
|
||||
private setupShaders() {
|
||||
|
@ -203,6 +203,11 @@ export class NavigationDialogManager {
|
||||
$label = $parent.querySelector<HTMLElement>('div')!;
|
||||
}
|
||||
|
||||
// Reduce width if it has <optgroup>
|
||||
if ($select.querySelector('optgroup')) {
|
||||
width -= 15;
|
||||
}
|
||||
|
||||
// Set min-width
|
||||
$label.style.minWidth = width + 'px';
|
||||
$parent.dataset.calculated = 'true';
|
||||
@ -234,7 +239,7 @@ export class NavigationDialogManager {
|
||||
} else if (keyCode === 'Enter' || keyCode === 'NumpadEnter' || keyCode === 'Space') {
|
||||
if (!($target instanceof HTMLInputElement && $target.type === 'text')) {
|
||||
handled = true;
|
||||
$target.dispatchEvent(new MouseEvent('click'));
|
||||
$target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
}
|
||||
} else if (keyCode === 'Escape') {
|
||||
handled = true;
|
||||
@ -361,7 +366,7 @@ export class NavigationDialogManager {
|
||||
}
|
||||
|
||||
if (releasedButton === GamepadKey.A) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click'));
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
return;
|
||||
} else if (releasedButton === GamepadKey.B) {
|
||||
this.hide();
|
||||
|
@ -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');
|
||||
|
||||
@ -1044,13 +1063,37 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
}
|
||||
|
||||
private renderServerSetting(setting: SettingTabContentItem): HTMLElement {
|
||||
let selectedValue;
|
||||
let selectedValue =getPref(PrefKey.SERVER_REGION);
|
||||
|
||||
const continents: Record<ServerContinent, {
|
||||
label: string,
|
||||
children?: HTMLOptionElement[],
|
||||
}> = {
|
||||
'america-north': {
|
||||
label: t('continent-north-america'),
|
||||
},
|
||||
'america-south': {
|
||||
label: t('continent-south-america'),
|
||||
},
|
||||
'asia': {
|
||||
label: t('continent-asia'),
|
||||
},
|
||||
'australia': {
|
||||
label: t('continent-australia'),
|
||||
},
|
||||
'europe': {
|
||||
label: t('continent-europe'),
|
||||
},
|
||||
'other': {
|
||||
label: t('other'),
|
||||
},
|
||||
};
|
||||
|
||||
const $control = CE<HTMLSelectElement>('select', {
|
||||
id: `bx_setting_${setting.pref}`,
|
||||
title: setting.label,
|
||||
tabindex: 0,
|
||||
});
|
||||
id: `bx_setting_${setting.pref}`,
|
||||
title: setting.label,
|
||||
tabindex: 0,
|
||||
});
|
||||
$control.name = $control.id;
|
||||
|
||||
$control.addEventListener('input', (e: Event) => {
|
||||
@ -1058,8 +1101,6 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
this.onGlobalSettingChanged(e);
|
||||
});
|
||||
|
||||
selectedValue = getPref(PrefKey.SERVER_REGION);
|
||||
|
||||
setting.options = {};
|
||||
for (const regionName in STATES.serverRegions) {
|
||||
const region = STATES.serverRegions[regionName];
|
||||
@ -1076,15 +1117,29 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
}
|
||||
|
||||
setting.options[value] = label;
|
||||
|
||||
const $option = CE<HTMLOptionElement>('option', {value: value}, label);
|
||||
const continent = continents[region.contintent];
|
||||
if (!continent.children) {
|
||||
continent.children = [];
|
||||
}
|
||||
continent.children.push($option);
|
||||
}
|
||||
|
||||
for (const value in setting.options) {
|
||||
const label = setting.options[value];
|
||||
const fragment = document.createDocumentFragment();
|
||||
let key: keyof typeof continents;
|
||||
for (key in continents) {
|
||||
const continent = continents[key];
|
||||
if (!continent.children) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $option = CE('option', {value: value}, label);
|
||||
$control.appendChild($option);
|
||||
fragment.appendChild(CE('optgroup', {
|
||||
label: continent.label,
|
||||
}, ...continent.children));
|
||||
}
|
||||
|
||||
$control.appendChild(fragment);
|
||||
$control.disabled = Object.keys(STATES.serverRegions).length === 0;
|
||||
|
||||
// Select preferred region
|
||||
|
@ -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')) {
|
||||
|
17
src/types/index.d.ts
vendored
17
src/types/index.d.ts
vendored
@ -21,20 +21,29 @@ interface Window {
|
||||
|
||||
interface NavigatorBattery extends Navigator {
|
||||
getBattery: () => Promise<{
|
||||
charging: boolean,
|
||||
level: float,
|
||||
charging: boolean;
|
||||
level: float;
|
||||
}>,
|
||||
}
|
||||
|
||||
type ServerContinent = 'america-north' | 'america-south' | 'asia' | 'australia' | 'europe' | 'other';
|
||||
type ServerRegion = {
|
||||
baseUri: string;
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
shortName: string;
|
||||
|
||||
contintent: ServerContinent;
|
||||
};
|
||||
|
||||
type BxStates = {
|
||||
supportedRegion: boolean;
|
||||
serverRegions: any;
|
||||
serverRegions: Record<string, ServerRegion>;
|
||||
selectedRegion: any;
|
||||
gsToken: string;
|
||||
isSignedIn: boolean;
|
||||
|
||||
isPlaying: boolean;
|
||||
appContext: any | null;
|
||||
|
||||
browser: {
|
||||
capabilities: {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ export const STATES: BxStates = {
|
||||
isSignedIn: false,
|
||||
|
||||
isPlaying: false,
|
||||
appContext: {},
|
||||
|
||||
browser: {
|
||||
capabilities: {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export class ScreenshotManager {
|
||||
const canvasContext = this.canvasContext;
|
||||
|
||||
if ($player instanceof HTMLCanvasElement) {
|
||||
streamPlayer.getWebGL2Player().drawFrame(true);
|
||||
streamPlayer.getWebGL2Player().forceDrawFrame();
|
||||
}
|
||||
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
|
@ -25,9 +25,9 @@ export interface BxSelectSettingElement extends HTMLSelectElement, BxBaseSetting
|
||||
export class SettingElement {
|
||||
static #renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any): BxSelectSettingElement {
|
||||
const $control = CE<BxSelectSettingElement>('select', {
|
||||
// title: setting.label,
|
||||
tabindex: 0,
|
||||
});
|
||||
// title: setting.label,
|
||||
tabindex: 0,
|
||||
});
|
||||
|
||||
let $parent: HTMLElement;
|
||||
if (setting.optionsGroup) {
|
||||
@ -64,10 +64,10 @@ export class SettingElement {
|
||||
|
||||
static #renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}): BxSelectSettingElement {
|
||||
const $control = CE<BxSelectSettingElement>('select', {
|
||||
// title: setting.label,
|
||||
multiple: true,
|
||||
tabindex: 0,
|
||||
});
|
||||
// title: setting.label,
|
||||
multiple: true,
|
||||
tabindex: 0,
|
||||
});
|
||||
|
||||
if (params && params.size) {
|
||||
$control.setAttribute('size', params.size.toString());
|
||||
@ -164,6 +164,14 @@ export class SettingElement {
|
||||
const MAX = options.reverse ? -setting.min! : setting.max!;
|
||||
const STEPS = Math.max(setting.steps || 1, 1);
|
||||
|
||||
let intervalId: number | null;
|
||||
let isHolding = false;
|
||||
|
||||
const clearIntervalId = () => {
|
||||
intervalId && clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
|
||||
const renderTextValue = (value: any) => {
|
||||
value = parseInt(value as string);
|
||||
|
||||
@ -182,6 +190,10 @@ export class SettingElement {
|
||||
const updateButtonsVisibility = () => {
|
||||
$btnDec.classList.toggle('bx-inactive', controlValue === MIN);
|
||||
$btnInc.classList.toggle('bx-inactive', controlValue === MAX);
|
||||
|
||||
if (controlValue === MIN || controlValue === MAX) {
|
||||
clearIntervalId();
|
||||
}
|
||||
}
|
||||
|
||||
const $wrapper = CE<BxHtmlSettingElement>('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
|
||||
@ -212,7 +224,7 @@ export class SettingElement {
|
||||
}
|
||||
|
||||
$range = CE<HTMLInputElement>('input', {
|
||||
id: `bx_setting_${key}`,
|
||||
id: `bx_inp_setting_${key}`,
|
||||
type: 'range',
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
@ -273,18 +285,7 @@ export class SettingElement {
|
||||
|
||||
updateButtonsVisibility();
|
||||
|
||||
let interval: number;
|
||||
let isHolding = false;
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
if (isHolding) {
|
||||
e.preventDefault();
|
||||
isHolding = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const $btn = e.target as HTMLElement;
|
||||
const buttonPressed = (e: Event, $btn: HTMLElement) => {
|
||||
let value = parseInt(controlValue);
|
||||
|
||||
const btnType = $btn.dataset.type;
|
||||
@ -300,27 +301,43 @@ export class SettingElement {
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range && ($range.value = value.toString());
|
||||
|
||||
isHolding = false;
|
||||
!(e as any).ignoreOnChange && onChange && onChange(e, value);
|
||||
}
|
||||
|
||||
const onMouseDown = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
isHolding = true;
|
||||
|
||||
const args = arguments;
|
||||
interval && clearInterval(interval);
|
||||
interval = window.setInterval(() => {
|
||||
e.target && BxEvent.dispatch(e.target as HTMLElement, 'click', {
|
||||
arguments: args,
|
||||
});
|
||||
}, 200);
|
||||
onChange && onChange(e, value);
|
||||
};
|
||||
|
||||
const onMouseUp = (e: PointerEvent) => {
|
||||
const onClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (isHolding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
|
||||
$btn && buttonPressed(e, $btn);
|
||||
|
||||
clearIntervalId();
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
clearIntervalId();
|
||||
|
||||
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
|
||||
if (!$btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHolding = true;
|
||||
e.preventDefault();
|
||||
|
||||
interval && clearInterval(interval);
|
||||
intervalId = window.setInterval((e: Event) => {
|
||||
buttonPressed(e, $btn);
|
||||
}, 200);
|
||||
|
||||
window.addEventListener('pointerup', onPointerUp, {once: true});
|
||||
window.addEventListener('pointercancel', onPointerUp, {once: true});
|
||||
};
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
clearIntervalId();
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
@ -332,18 +349,11 @@ export class SettingElement {
|
||||
$range.value = options.reverse ? -value : value;
|
||||
};
|
||||
|
||||
$btnDec.addEventListener('click', onClick);
|
||||
$btnDec.addEventListener('pointerdown', onMouseDown);
|
||||
$btnDec.addEventListener('pointerup', onMouseUp);
|
||||
$btnDec.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
$btnInc.addEventListener('click', onClick);
|
||||
$btnInc.addEventListener('pointerdown', onMouseDown);
|
||||
$btnInc.addEventListener('pointerup', onMouseUp);
|
||||
$btnInc.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
$wrapper.addEventListener('click', onClick);
|
||||
$wrapper.addEventListener('pointerdown', onPointerDown);
|
||||
$wrapper.addEventListener('contextmenu', onContextMenu);
|
||||
setNearby($wrapper, {
|
||||
focus: $range || $btnInc,
|
||||
focus: options.hideSlider ? $btnInc : $range,
|
||||
})
|
||||
|
||||
return $wrapper;
|
||||
|
@ -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': '中文 (繁體)',
|
||||
|
@ -67,6 +67,11 @@ const Texts = {
|
||||
"confirm-reload-stream": "Do you want to refresh the stream?",
|
||||
"connected": "Connected",
|
||||
"console-connect": "Connect",
|
||||
"continent-asia": "Asia",
|
||||
"continent-australia": "Australia",
|
||||
"continent-europe": "Europe",
|
||||
"continent-north-america": "North America",
|
||||
"continent-south-america": "South America",
|
||||
"contrast": "Contrast",
|
||||
"controller": "Controller",
|
||||
"controller-friendly-ui": "Controller-friendly UI",
|
||||
@ -267,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",
|
||||
|
@ -14,21 +14,31 @@ import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export class XcloudInterceptor {
|
||||
private static readonly SERVER_EMOJIS = {
|
||||
AustraliaEast: '🇦🇺',
|
||||
AustraliaSouthEast: '🇦🇺',
|
||||
BrazilSouth: '🇧🇷',
|
||||
EastUS: '🇺🇸',
|
||||
EastUS2: '🇺🇸',
|
||||
JapanEast: '🇯🇵',
|
||||
KoreaCentral: '🇰🇷',
|
||||
MexicoCentral: '🇲🇽',
|
||||
NorthCentralUs: '🇺🇸',
|
||||
SouthCentralUS: '🇺🇸',
|
||||
UKSouth: '🇬🇧',
|
||||
WestEurope: '🇪🇺',
|
||||
WestUS: '🇺🇸',
|
||||
WestUS2: '🇺🇸',
|
||||
private static readonly SERVER_EXTRA_INFO: Record<string, [string, ServerContinent]> = {
|
||||
// North America
|
||||
EastUS: ['🇺🇸', 'america-north'],
|
||||
EastUS2: ['🇺🇸', 'america-north'],
|
||||
NorthCentralUs: ['🇺🇸', 'america-north'],
|
||||
SouthCentralUS: ['🇺🇸', 'america-north'],
|
||||
WestUS: ['🇺🇸', 'america-north'],
|
||||
WestUS2: ['🇺🇸', 'america-north'],
|
||||
MexicoCentral: ['🇲🇽', 'america-north'],
|
||||
|
||||
// South America
|
||||
BrazilSouth: ['🇧🇷', 'america-south'],
|
||||
|
||||
// Asia
|
||||
JapanEast: ['🇯🇵', 'asia'],
|
||||
KoreaCentral: ['🇰🇷', 'asia'],
|
||||
|
||||
// Australia
|
||||
AustraliaEast: ['🇦🇺', 'australia'],
|
||||
AustraliaSouthEast: ['🇦🇺', 'australia'],
|
||||
|
||||
// Europe
|
||||
SwedenCentral: ['🇸🇪', 'europe'],
|
||||
UKSouth: ['🇬🇧', 'europe'],
|
||||
WestEurope: ['🇪🇺', 'europe'],
|
||||
};
|
||||
|
||||
private static async handleLogin(request: RequestInfo | URL, init?: RequestInit) {
|
||||
@ -52,10 +62,11 @@ export class XcloudInterceptor {
|
||||
|
||||
// Get server list
|
||||
const serverRegex = /\/\/(\w+)\./;
|
||||
const serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
|
||||
const serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO;
|
||||
|
||||
for (let region of obj.offeringSettings.regions) {
|
||||
const regionName = region.name as keyof typeof serverEmojis;
|
||||
let region: ServerRegion;
|
||||
for (region of obj.offeringSettings.regions) {
|
||||
const regionName = region.name as keyof typeof serverExtra;
|
||||
let shortName = region.name;
|
||||
|
||||
if (region.isDefault) {
|
||||
@ -65,8 +76,11 @@ export class XcloudInterceptor {
|
||||
let match = serverRegex.exec(region.baseUri);
|
||||
if (match) {
|
||||
shortName = match[1];
|
||||
if (serverEmojis[regionName]) {
|
||||
shortName = serverEmojis[regionName] + ' ' + shortName;
|
||||
if (serverExtra[regionName]) {
|
||||
shortName = serverExtra[regionName][0] + ' ' + shortName;
|
||||
region.contintent = serverExtra[regionName][1];
|
||||
} else {
|
||||
region.contintent = 'other';
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user