Compare commits

...

32 Commits

Author SHA1 Message Date
f18c5c14ed Bump version to 5.9.5 2024-11-06 19:22:07 +07:00
d0ceed00f8 Add "Server locations" link 2024-11-06 19:21:38 +07:00
fce8af4b3b Fix custom buttons disappearing in Guide Menu (#551) 2024-11-06 07:36:32 +07:00
57686f9d8e Bump version to 5.9.4 2024-11-01 20:08:15 +07:00
f0e7272a82 Use "brand" instead of "manufacturer" 2024-11-01 19:57:08 +07:00
b0ecc7171b Update device suggestion URL 2024-11-01 19:52:25 +07:00
17c08792e1 Redirect to /en-US/play if visiting from an unsupported region 2024-11-01 17:02:30 +07:00
e8376b52fe Add patch to modify __PRELOADED_STATE__ 2024-11-01 16:55:58 +07:00
f6581abe34 Show "Remote Play" on web's title instead of "Fortnite" 2024-11-01 08:53:24 +07:00
b090d325ae Migrate PatcherCache to singleton class 2024-11-01 07:22:21 +07:00
ec3daa09fd Use hash of client.js file for calculating patch's signature 2024-11-01 07:16:11 +07:00
b2a2e4d27e Add in-game language support for Bulgarian, Romanian and Thai 2024-10-30 08:45:59 +07:00
4f3430c43c Bump version to 5.9.3 2024-10-29 20:35:15 +07:00
15c6d3c74b Update dists 2024-10-29 20:24:04 +07:00
b170b95145 Fix bugs in NumberStepper 2024-10-29 20:20:34 +07:00
4217b89194 Reduce the amount of event listeners in NumberStepper 2024-10-29 20:11:54 +07:00
38211168e9 Reduce width of controller-friendly select box if it has <optgroup> 2024-10-29 16:56:34 +07:00
392dc2cf86 Categorize servers by continents 2024-10-29 16:51:29 +07:00
67de264aa9 Revert "Use gl.texSubImage2D()"
This reverts commit 3e2c1bb2a4.
2024-10-27 10:20:11 +07:00
3e2c1bb2a4 Use gl.texSubImage2D() 2024-10-27 09:36:34 +07:00
5653914d19 Move WebGL2's drawFrame() function to animate() function 2024-10-26 21:53:03 +07:00
4a8f66f2a1 Upgrade bun 2024-10-25 08:59:05 +07:00
70f43ba8f2 Add emoji flag for SwedenCentral server 2024-10-25 07:33:35 +07:00
4d49639622 Bump version to 5.9.2 2024-10-24 20:58:53 +07:00
22f1ebdd08 Fix Remote Play not working when using different network (#538) 2024-10-24 20:38:01 +07:00
bae51eff3d Bump version to 5.9.1 2024-10-23 21:09:26 +07:00
adc9897210 Update translations 2024-10-23 21:08:20 +07:00
53442557e1 Fix Virtual Controller Remapper's bug (contd) 2024-10-23 20:51:04 +07:00
5b67b4c37d Fix Virtual Controller Remapper's bug (contd) 2024-10-23 20:14:10 +07:00
5a06933143 Update dists 2024-10-23 20:03:26 +07:00
6440c91cdf Fix Virtual Controller Remapper's bug 2024-10-23 20:00:27 +07:00
b06dc6e219 Update polling rate's default text 2024-10-22 20:47:45 +07:00
26 changed files with 1010 additions and 744 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud (Lite)
// @namespace https://github.com/redphx
// @version 5.9.0
// @version 5.9.5
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -105,7 +105,7 @@ class UserAgent {
});
}
}
var SCRIPT_VERSION = "5.9.0", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
var SCRIPT_VERSION = "5.9.5", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@ -114,7 +114,6 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu
gsToken: "",
isSignedIn: !1,
isPlaying: !1,
appContext: {},
browser: {
capabilities: {
touch: browserHasTouchSupport,
@ -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",
@ -403,7 +407,7 @@ var SUPPORTED_LANGUAGES = {
e => `${e.version} 버전 사용가능`,
e => `Dostępna jest nowa wersja ${e.version}`,
e => `Versão ${e.version} disponível`,
,
e => `Версия ${e.version} доступна`,
e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
e => `${e.version} sayılı yeni sürüm mevcut`,
e => `Доступна версія ${e.version}`,
@ -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": "中文 (繁體)"
@ -1393,7 +1403,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
customTextValue(value) {
value = parseInt(value);
let text = +(1000 / value).toFixed(2) + " Hz";
if (value === 4) text = `${t("default")} (${text})`;
if (value === 4) text = `${text} (${t("default")})`;
return text;
}
}
@ -2381,7 +2391,7 @@ class MouseDataProvider {
class MkbHandler {}
class LocalDb {
static DB_NAME = "BetterXcloud";
static DB_VERSION = 1;
static DB_VERSION = 2;
db;
open() {
return new Promise((resolve, reject) => {
@ -2390,7 +2400,7 @@ class LocalDb {
return;
}
let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
request.onupgradeneeded = this.onUpgradeNeeded, request.onerror = (e) => {
request.onupgradeneeded = this.onUpgradeNeeded.bind(this), request.onerror = (e) => {
console.log(e), alert(e.target.error.message), reject && reject();
}, request.onsuccess = (e) => {
this.db = e.target.result, resolve();
@ -2438,51 +2448,49 @@ class MkbPresetsDb extends LocalDb {
super();
BxLogger.info(this.LOG_TAG, "constructor()");
}
createTable(db) {
db.createObjectStore(this.TABLE_PRESETS, {
keyPath: "id",
autoIncrement: !0
}).createIndex("name_idx", "name");
}
onUpgradeNeeded(e) {
let db = e.target.result;
switch (e.oldVersion) {
case 0: {
db.createObjectStore(this.TABLE_PRESETS, {
keyPath: "id",
autoIncrement: !0
}).createIndex("name_idx", "name");
break;
}
if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");
if (!db.objectStoreNames.contains(this.TABLE_PRESETS)) this.createTable(db);
}
async presetsTable() {
return await this.open(), await this.table(this.TABLE_PRESETS, "readwrite");
}
async newPreset(name, data) {
let table = await this.presetsTable(), [, id] = await this.add(table, { name, data });
return id;
}
async updatePreset(preset) {
let table = await this.presetsTable(), [, id] = await this.put(table, preset);
return id;
}
async deletePreset(id) {
let table = await this.presetsTable();
return await this.delete(table, id), id;
}
async getPreset(id) {
let table = await this.presetsTable(), [, preset] = await this.get(table, id);
return preset;
}
async getPresets() {
let table = await this.presetsTable(), [, count] = await this.count(table);
if (count > 0) {
let [, items] = await this.getAll(table), presets = {};
return items.forEach((item) => presets[item.id] = item), presets;
}
}
presetsTable() {
return this.open().then(() => this.table(this.TABLE_PRESETS, "readwrite"));
}
newPreset(name, data) {
return this.presetsTable().then((table) => this.add(table, { name, data })).then(([table, id]) => new Promise((resolve) => resolve(id)));
}
updatePreset(preset) {
return this.presetsTable().then((table) => this.put(table, preset)).then(([table, id]) => new Promise((resolve) => resolve(id)));
}
deletePreset(id) {
return this.presetsTable().then((table) => this.delete(table, id)).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
}
getPreset(id) {
return this.presetsTable().then((table) => this.get(table, id)).then(([table, preset]) => new Promise((resolve) => resolve(preset)));
}
getPresets() {
return this.presetsTable().then((table) => this.count(table)).then(([table, count]) => {
if (count > 0) return new Promise((resolve) => {
this.getAll(table).then(([table2, items]) => {
let presets = {};
items.forEach((item) => presets[item.id] = item), resolve(presets);
});
});
let preset = {
name: t("default"),
data: MkbPreset.DEFAULT_PRESET
};
return new Promise((resolve) => {
this.add(table, preset).then(([table2, id]) => {
preset.id = id, setPref("mkb_default_preset_id", id), resolve({ [id]: preset });
});
});
});
let preset = {
name: t("default"),
data: MkbPreset.DEFAULT_PRESET
}, [, id] = await this.add(table, preset);
return preset.id = id, setPref("mkb_default_preset_id", id), {
[id]: preset
};
}
}
var PointerToMouseButton = {
@ -2888,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";
}
}
@ -2903,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;
@ -2957,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();
@ -3176,7 +3185,7 @@ class MkbRemapper {
static instance;
static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper);
LOG_TAG = "MkbRemapper";
STATE = {
states = {
currentPresetId: 0,
presets: {},
editingPresetData: null,
@ -3190,7 +3199,7 @@ class MkbRemapper {
allMouseElements = {};
bindingDialog;
constructor() {
BxLogger.info(this.LOG_TAG, "constructor()"), this.STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({
BxLogger.info(this.LOG_TAG, "constructor()"), this.states.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({
className: "bx-binding-dialog",
content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))),
hideCloseButton: !0
@ -3204,11 +3213,11 @@ class MkbRemapper {
if ($elm.dataset.keyCode === key.code) return;
for (let $otherElm of this.allKeyElements)
if ($otherElm.dataset.keyCode === key.code) this.unbindKey($otherElm);
this.STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code;
this.states.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code;
};
unbindKey = ($elm) => {
let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot);
this.STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode;
this.states.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode;
};
onWheel = (e) => {
e.preventDefault(), this.clearEventListeners(), this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200);
@ -3221,21 +3230,26 @@ class MkbRemapper {
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
onBindingKey = (e) => {
if (!this.STATE.isEditing || e.button !== 0) return;
if (!this.states.isEditing || e.button !== 0) return;
console.log(e), this.$currentBindingKey = e.target, window.addEventListener("keydown", this.onKeyDown), window.addEventListener("mousedown", this.onMouseDown), window.addEventListener("wheel", this.onWheel), this.bindingDialog.show({ title: this.$currentBindingKey.dataset.prompt });
};
onContextMenu = (e) => {
if (e.preventDefault(), !this.STATE.isEditing) return;
if (e.preventDefault(), !this.states.isEditing) return;
this.unbindKey(e.target);
};
getPreset = (presetId) => {
return this.STATE.presets[presetId];
return this.states.presets[presetId];
};
getCurrentPreset = () => {
return this.getPreset(this.STATE.currentPresetId);
let preset = this.getPreset(this.states.currentPresetId);
if (!preset) {
let firstPresetId = parseInt(Object.keys(this.states.presets)[0]);
preset = this.states.presets[firstPresetId], this.states.currentPresetId = firstPresetId, setPref("mkb_default_preset_id", firstPresetId);
}
return preset;
};
switchPreset = (presetId) => {
this.STATE.currentPresetId = presetId;
this.states.currentPresetId = presetId;
let presetData = this.getCurrentPreset().data;
for (let $elm of this.allKeyElements) {
let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot), buttonKeys = presetData.mapping[buttonIndex];
@ -3248,35 +3262,33 @@ class MkbRemapper {
if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default;
"setValue" in $elm && $elm.setValue(value);
}
let activated = getPref("mkb_default_preset_id") === this.STATE.currentPresetId;
let activated = getPref("mkb_default_preset_id") === this.states.currentPresetId;
this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate");
};
refresh() {
while (this.$presetsSelect.firstChild)
this.$presetsSelect.removeChild(this.$presetsSelect.firstChild);
MkbPresetsDb.getInstance().getPresets().then((presets) => {
this.STATE.presets = presets;
let $fragment = document.createDocumentFragment(), defaultPresetId;
if (this.STATE.currentPresetId === 0) this.STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData();
else defaultPresetId = getPref("mkb_default_preset_id");
for (let id in presets) {
let name = presets[id].name;
if (id === defaultPresetId) name = "🎮 " + name;
let $options = CE("option", { value: id }, name);
$options.selected = parseInt(id) === this.STATE.currentPresetId, $fragment.appendChild($options);
}
this.$presetsSelect.appendChild($fragment);
let activated = defaultPresetId === this.STATE.currentPresetId;
this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.STATE.isEditing && this.switchPreset(this.STATE.currentPresetId);
});
async refresh() {
removeChildElements(this.$presetsSelect);
let presets = await MkbPresetsDb.getInstance().getPresets();
this.states.presets = presets;
let fragment = document.createDocumentFragment(), defaultPresetId;
if (this.states.currentPresetId === 0) this.states.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.states.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData();
else defaultPresetId = getPref("mkb_default_preset_id");
for (let id in presets) {
let name = presets[id].name;
if (id === defaultPresetId) name = "🎮 " + name;
let $options = CE("option", { value: id }, name);
$options.selected = parseInt(id) === this.states.currentPresetId, fragment.appendChild($options);
}
this.$presetsSelect.appendChild(fragment);
let activated = defaultPresetId === this.states.currentPresetId;
this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.states.isEditing && this.switchPreset(this.states.currentPresetId);
}
toggleEditing = (force) => {
if (this.STATE.isEditing = typeof force !== "undefined" ? force : !this.STATE.isEditing, this.$wrapper.classList.toggle("bx-editing", this.STATE.isEditing), this.STATE.isEditing) this.STATE.editingPresetData = deepClone(this.getCurrentPreset().data);
else this.STATE.editingPresetData = null;
if (this.states.isEditing = typeof force !== "undefined" ? force : !this.states.isEditing, this.$wrapper.classList.toggle("bx-editing", this.states.isEditing), this.states.isEditing) this.states.editingPresetData = deepClone(this.getCurrentPreset().data);
else this.states.editingPresetData = null;
let childElements = this.$wrapper.querySelectorAll("select, button, input");
for (let $elm of Array.from(childElements)) {
if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue;
let disable = !this.STATE.isEditing;
let disable = !this.states.isEditing;
if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable;
$elm.disabled = disable;
}
@ -3296,10 +3308,10 @@ class MkbRemapper {
title: t("rename"),
icon: BxIcon.CURSOR_TEXT,
tabIndex: -1,
onClick: (e) => {
onClick: async () => {
let preset = this.getCurrentPreset(), newName = promptNewName(preset.name);
if (!newName || newName === preset.name) return;
preset.name = newName, MkbPresetsDb.getInstance().updatePreset(preset).then((id) => this.refresh());
preset.name = newName, await MkbPresetsDb.getInstance().updatePreset(preset), await this.refresh();
}
}), createButton({
icon: BxIcon.NEW,
@ -3309,7 +3321,7 @@ class MkbRemapper {
let newName = promptNewName("");
if (!newName) return;
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id) => {
this.STATE.currentPresetId = id, this.refresh();
this.states.currentPresetId = id, this.refresh();
});
}
}), createButton({
@ -3320,7 +3332,7 @@ class MkbRemapper {
let preset = this.getCurrentPreset(), newName = promptNewName(`${preset.name} (2)`);
if (!newName) return;
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then((id) => {
this.STATE.currentPresetId = id, this.refresh();
this.states.currentPresetId = id, this.refresh();
});
}
}), createButton({
@ -3330,8 +3342,8 @@ class MkbRemapper {
tabIndex: -1,
onClick: (e) => {
if (!confirm(t("confirm-delete-preset"))) return;
MkbPresetsDb.getInstance().deletePreset(this.STATE.currentPresetId).then((id) => {
this.STATE.currentPresetId = 0, this.refresh();
MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then((id) => {
this.states.currentPresetId = 0, this.refresh();
});
}
}));
@ -3353,7 +3365,7 @@ class MkbRemapper {
let $mouseSettings = document.createDocumentFragment();
for (let key in MkbPreset.MOUSE_SETTINGS) {
let setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default, $elm, onChange = (e, value2) => {
this.STATE.editingPresetData.mouse[key] = value2;
this.states.editingPresetData.mouse[key] = value2;
}, $row = CE("label", {
class: "bx-settings-row",
for: `bx_setting_${key}`
@ -3370,14 +3382,14 @@ class MkbRemapper {
style: 1,
tabIndex: -1,
onClick: (e) => {
setPref("mkb_default_preset_id", this.STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh();
setPref("mkb_default_preset_id", this.states.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh();
}
})), CE("div", {}, createButton({
label: t("cancel"),
style: 4,
tabIndex: -1,
onClick: (e) => {
this.switchPreset(this.STATE.currentPresetId), this.toggleEditing(!1);
this.switchPreset(this.states.currentPresetId), this.toggleEditing(!1);
}
}), createButton({
label: t("save"),
@ -3385,7 +3397,7 @@ class MkbRemapper {
tabIndex: -1,
onClick: (e) => {
let updatedPreset = deepClone(this.getCurrentPreset());
updatedPreset.data = this.STATE.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id) => {
updatedPreset.data = this.states.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id) => {
if (id === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData();
this.toggleEditing(!1), this.refresh();
});
@ -3929,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) {
@ -3989,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");
@ -4098,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 = {
@ -4393,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) => {
@ -4856,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) {
@ -5034,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");
@ -5060,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);
@ -5425,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"));
@ -5913,7 +5960,7 @@ class RootDialogObserver {
if (mutation.type !== "childList") continue;
if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
let $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) RootDialogObserver.handleAddedElement($root, $addedElm);
if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
}
let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 5.9.0
// @version 5.9.5
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -107,7 +107,7 @@ class UserAgent {
});
}
}
var SCRIPT_VERSION = "5.9.0", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
var SCRIPT_VERSION = "5.9.5", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@ -116,7 +116,6 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu
gsToken: "",
isSignedIn: !1,
isPlaying: !1,
appContext: {},
browser: {
capabilities: {
touch: browserHasTouchSupport,
@ -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",
@ -426,7 +430,7 @@ var SUPPORTED_LANGUAGES = {
e => `${e.version} 버전 사용가능`,
e => `Dostępna jest nowa wersja ${e.version}`,
e => `Versão ${e.version} disponível`,
,
e => `Версия ${e.version} доступна`,
e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
e => `${e.version} sayılı yeni sürüm mevcut`,
e => `Доступна версія ${e.version}`,
@ -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": "中文 (繁體)"
@ -1416,7 +1426,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
customTextValue(value) {
value = parseInt(value);
let text = +(1000 / value).toFixed(2) + " Hz";
if (value === 4) text = `${t("default")} (${text})`;
if (value === 4) text = `${text} (${t("default")})`;
return text;
}
}
@ -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();
@ -2656,7 +2666,7 @@ class NativeMkbHandler extends MkbHandler {
}
class LocalDb {
static DB_NAME = "BetterXcloud";
static DB_VERSION = 1;
static DB_VERSION = 2;
db;
open() {
return new Promise((resolve, reject) => {
@ -2665,7 +2675,7 @@ class LocalDb {
return;
}
let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
request.onupgradeneeded = this.onUpgradeNeeded, request.onerror = (e) => {
request.onupgradeneeded = this.onUpgradeNeeded.bind(this), request.onerror = (e) => {
console.log(e), alert(e.target.error.message), reject && reject();
}, request.onsuccess = (e) => {
this.db = e.target.result, resolve();
@ -2713,51 +2723,49 @@ class MkbPresetsDb extends LocalDb {
super();
BxLogger.info(this.LOG_TAG, "constructor()");
}
createTable(db) {
db.createObjectStore(this.TABLE_PRESETS, {
keyPath: "id",
autoIncrement: !0
}).createIndex("name_idx", "name");
}
onUpgradeNeeded(e) {
let db = e.target.result;
switch (e.oldVersion) {
case 0: {
db.createObjectStore(this.TABLE_PRESETS, {
keyPath: "id",
autoIncrement: !0
}).createIndex("name_idx", "name");
break;
}
if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");
if (!db.objectStoreNames.contains(this.TABLE_PRESETS)) this.createTable(db);
}
async presetsTable() {
return await this.open(), await this.table(this.TABLE_PRESETS, "readwrite");
}
async newPreset(name, data) {
let table = await this.presetsTable(), [, id2] = await this.add(table, { name, data });
return id2;
}
async updatePreset(preset) {
let table = await this.presetsTable(), [, id2] = await this.put(table, preset);
return id2;
}
async deletePreset(id2) {
let table = await this.presetsTable();
return await this.delete(table, id2), id2;
}
async getPreset(id2) {
let table = await this.presetsTable(), [, preset] = await this.get(table, id2);
return preset;
}
async getPresets() {
let table = await this.presetsTable(), [, count] = await this.count(table);
if (count > 0) {
let [, items] = await this.getAll(table), presets = {};
return items.forEach((item2) => presets[item2.id] = item2), presets;
}
}
presetsTable() {
return this.open().then(() => this.table(this.TABLE_PRESETS, "readwrite"));
}
newPreset(name, data) {
return this.presetsTable().then((table) => this.add(table, { name, data })).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
}
updatePreset(preset) {
return this.presetsTable().then((table) => this.put(table, preset)).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
}
deletePreset(id2) {
return this.presetsTable().then((table) => this.delete(table, id2)).then(([table, id3]) => new Promise((resolve) => resolve(id3)));
}
getPreset(id2) {
return this.presetsTable().then((table) => this.get(table, id2)).then(([table, preset]) => new Promise((resolve) => resolve(preset)));
}
getPresets() {
return this.presetsTable().then((table) => this.count(table)).then(([table, count]) => {
if (count > 0) return new Promise((resolve) => {
this.getAll(table).then(([table2, items]) => {
let presets = {};
items.forEach((item2) => presets[item2.id] = item2), resolve(presets);
});
});
let preset = {
name: t("default"),
data: MkbPreset.DEFAULT_PRESET
};
return new Promise((resolve) => {
this.add(table, preset).then(([table2, id2]) => {
preset.id = id2, setPref("mkb_default_preset_id", id2), resolve({ [id2]: preset });
});
});
});
let preset = {
name: t("default"),
data: MkbPreset.DEFAULT_PRESET
}, [, id2] = await this.add(table, preset);
return preset.id = id2, setPref("mkb_default_preset_id", id2), {
[id2]: preset
};
}
}
var PointerToMouseButton = {
@ -3169,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";
}
}
@ -3184,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;
@ -3238,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();
@ -3457,7 +3466,7 @@ class MkbRemapper {
static instance;
static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper);
LOG_TAG = "MkbRemapper";
STATE = {
states = {
currentPresetId: 0,
presets: {},
editingPresetData: null,
@ -3471,7 +3480,7 @@ class MkbRemapper {
allMouseElements = {};
bindingDialog;
constructor() {
BxLogger.info(this.LOG_TAG, "constructor()"), this.STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({
BxLogger.info(this.LOG_TAG, "constructor()"), this.states.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({
className: "bx-binding-dialog",
content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))),
hideCloseButton: !0
@ -3485,11 +3494,11 @@ class MkbRemapper {
if ($elm.dataset.keyCode === key.code) return;
for (let $otherElm of this.allKeyElements)
if ($otherElm.dataset.keyCode === key.code) this.unbindKey($otherElm);
this.STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code;
this.states.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code;
};
unbindKey = ($elm) => {
let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot);
this.STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode;
this.states.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode;
};
onWheel = (e) => {
e.preventDefault(), this.clearEventListeners(), this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200);
@ -3502,21 +3511,26 @@ class MkbRemapper {
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
onBindingKey = (e) => {
if (!this.STATE.isEditing || e.button !== 0) return;
if (!this.states.isEditing || e.button !== 0) return;
console.log(e), this.$currentBindingKey = e.target, window.addEventListener("keydown", this.onKeyDown), window.addEventListener("mousedown", this.onMouseDown), window.addEventListener("wheel", this.onWheel), this.bindingDialog.show({ title: this.$currentBindingKey.dataset.prompt });
};
onContextMenu = (e) => {
if (e.preventDefault(), !this.STATE.isEditing) return;
if (e.preventDefault(), !this.states.isEditing) return;
this.unbindKey(e.target);
};
getPreset = (presetId) => {
return this.STATE.presets[presetId];
return this.states.presets[presetId];
};
getCurrentPreset = () => {
return this.getPreset(this.STATE.currentPresetId);
let preset = this.getPreset(this.states.currentPresetId);
if (!preset) {
let firstPresetId = parseInt(Object.keys(this.states.presets)[0]);
preset = this.states.presets[firstPresetId], this.states.currentPresetId = firstPresetId, setPref("mkb_default_preset_id", firstPresetId);
}
return preset;
};
switchPreset = (presetId) => {
this.STATE.currentPresetId = presetId;
this.states.currentPresetId = presetId;
let presetData = this.getCurrentPreset().data;
for (let $elm of this.allKeyElements) {
let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot), buttonKeys = presetData.mapping[buttonIndex];
@ -3529,35 +3543,33 @@ class MkbRemapper {
if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default;
"setValue" in $elm && $elm.setValue(value);
}
let activated = getPref("mkb_default_preset_id") === this.STATE.currentPresetId;
let activated = getPref("mkb_default_preset_id") === this.states.currentPresetId;
this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate");
};
refresh() {
while (this.$presetsSelect.firstChild)
this.$presetsSelect.removeChild(this.$presetsSelect.firstChild);
MkbPresetsDb.getInstance().getPresets().then((presets) => {
this.STATE.presets = presets;
let $fragment = document.createDocumentFragment(), defaultPresetId;
if (this.STATE.currentPresetId === 0) this.STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData();
else defaultPresetId = getPref("mkb_default_preset_id");
for (let id2 in presets) {
let name = presets[id2].name;
if (id2 === defaultPresetId) name = "🎮 " + name;
let $options = CE("option", { value: id2 }, name);
$options.selected = parseInt(id2) === this.STATE.currentPresetId, $fragment.appendChild($options);
}
this.$presetsSelect.appendChild($fragment);
let activated = defaultPresetId === this.STATE.currentPresetId;
this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.STATE.isEditing && this.switchPreset(this.STATE.currentPresetId);
});
async refresh() {
removeChildElements(this.$presetsSelect);
let presets = await MkbPresetsDb.getInstance().getPresets();
this.states.presets = presets;
let fragment = document.createDocumentFragment(), defaultPresetId;
if (this.states.currentPresetId === 0) this.states.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.states.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData();
else defaultPresetId = getPref("mkb_default_preset_id");
for (let id2 in presets) {
let name = presets[id2].name;
if (id2 === defaultPresetId) name = "🎮 " + name;
let $options = CE("option", { value: id2 }, name);
$options.selected = parseInt(id2) === this.states.currentPresetId, fragment.appendChild($options);
}
this.$presetsSelect.appendChild(fragment);
let activated = defaultPresetId === this.states.currentPresetId;
this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.states.isEditing && this.switchPreset(this.states.currentPresetId);
}
toggleEditing = (force) => {
if (this.STATE.isEditing = typeof force !== "undefined" ? force : !this.STATE.isEditing, this.$wrapper.classList.toggle("bx-editing", this.STATE.isEditing), this.STATE.isEditing) this.STATE.editingPresetData = deepClone(this.getCurrentPreset().data);
else this.STATE.editingPresetData = null;
if (this.states.isEditing = typeof force !== "undefined" ? force : !this.states.isEditing, this.$wrapper.classList.toggle("bx-editing", this.states.isEditing), this.states.isEditing) this.states.editingPresetData = deepClone(this.getCurrentPreset().data);
else this.states.editingPresetData = null;
let childElements = this.$wrapper.querySelectorAll("select, button, input");
for (let $elm of Array.from(childElements)) {
if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue;
let disable = !this.STATE.isEditing;
let disable = !this.states.isEditing;
if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable;
$elm.disabled = disable;
}
@ -3577,10 +3589,10 @@ class MkbRemapper {
title: t("rename"),
icon: BxIcon.CURSOR_TEXT,
tabIndex: -1,
onClick: (e) => {
onClick: async () => {
let preset = this.getCurrentPreset(), newName = promptNewName(preset.name);
if (!newName || newName === preset.name) return;
preset.name = newName, MkbPresetsDb.getInstance().updatePreset(preset).then((id2) => this.refresh());
preset.name = newName, await MkbPresetsDb.getInstance().updatePreset(preset), await this.refresh();
}
}), createButton({
icon: BxIcon.NEW,
@ -3590,7 +3602,7 @@ class MkbRemapper {
let newName = promptNewName("");
if (!newName) return;
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => {
this.STATE.currentPresetId = id2, this.refresh();
this.states.currentPresetId = id2, this.refresh();
});
}
}), createButton({
@ -3601,7 +3613,7 @@ class MkbRemapper {
let preset = this.getCurrentPreset(), newName = promptNewName(`${preset.name} (2)`);
if (!newName) return;
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then((id2) => {
this.STATE.currentPresetId = id2, this.refresh();
this.states.currentPresetId = id2, this.refresh();
});
}
}), createButton({
@ -3611,8 +3623,8 @@ class MkbRemapper {
tabIndex: -1,
onClick: (e) => {
if (!confirm(t("confirm-delete-preset"))) return;
MkbPresetsDb.getInstance().deletePreset(this.STATE.currentPresetId).then((id2) => {
this.STATE.currentPresetId = 0, this.refresh();
MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then((id2) => {
this.states.currentPresetId = 0, this.refresh();
});
}
}));
@ -3634,7 +3646,7 @@ class MkbRemapper {
let $mouseSettings = document.createDocumentFragment();
for (let key in MkbPreset.MOUSE_SETTINGS) {
let setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default, $elm, onChange = (e, value2) => {
this.STATE.editingPresetData.mouse[key] = value2;
this.states.editingPresetData.mouse[key] = value2;
}, $row = CE("label", {
class: "bx-settings-row",
for: `bx_setting_${key}`
@ -3651,14 +3663,14 @@ class MkbRemapper {
style: 1,
tabIndex: -1,
onClick: (e) => {
setPref("mkb_default_preset_id", this.STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh();
setPref("mkb_default_preset_id", this.states.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh();
}
})), CE("div", {}, createButton({
label: t("cancel"),
style: 4,
tabIndex: -1,
onClick: (e) => {
this.switchPreset(this.STATE.currentPresetId), this.toggleEditing(!1);
this.switchPreset(this.states.currentPresetId), this.toggleEditing(!1);
}
}), createButton({
label: t("save"),
@ -3666,7 +3678,7 @@ class MkbRemapper {
tabIndex: -1,
onClick: (e) => {
let updatedPreset = deepClone(this.getCurrentPreset());
updatedPreset.data = this.STATE.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id2) => {
updatedPreset.data = this.states.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id2) => {
if (id2 === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData();
this.toggleEditing(!1), this.refresh();
});
@ -4033,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;
@ -4395,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" ? [
@ -4403,6 +4425,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"disableNativeRequestPointerLock",
"exposeInputSink"
] : [],
"modifyPreloadedState",
"optimizeGameSlugGenerator",
"detectBrowserRouterReady",
"patchRequestInfoCrash",
@ -4439,6 +4462,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
"remotePlayDirectConnectUrl",
"remotePlayDisableAchievementToast",
"remotePlayRecentlyUsedTitleIds",
"remotePlayWebTitle",
STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"
] : [],
...BX_FLAGS.EnableXcloudLogging ? [
@ -4478,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);
};
@ -4486,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;
@ -4509,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 {
@ -5073,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) {
@ -5133,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");
@ -5239,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 = {
@ -5727,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;
@ -6110,7 +6198,7 @@ class XhomeInterceptor {
["ipV6Address", "ipV6Port"]
];
XhomeInterceptor.consoleAddrs = {};
for (let pair in pairs) {
for (let pair of pairs) {
let [keyAddr, keyPort] = pair;
if (serverDetails[keyAddr]) {
let port = serverDetails[keyPort], ports = new Set;
@ -6428,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;
@ -6611,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");
@ -6637,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);
@ -6914,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) {
@ -7041,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"));
@ -7862,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);
@ -7991,7 +8057,7 @@ window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {
function main() {
if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9");
if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager();
if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), updatePollingRate(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect();
if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), updatePollingRate(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect();
if (getPref("stream_touch_controller") === "all") TouchController.setup();
if (getPref("mkb_enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
if (getPref("ui_game_card_show_wait_time") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));

View File

@ -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",

View File

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

View File

@ -1,4 +1,4 @@
import { CE, createButton, ButtonStyle } from "@utils/html";
import { CE, createButton, ButtonStyle, removeChildElements } from "@utils/html";
import { t } from "@utils/translation";
import { Dialog } from "@modules/dialog";
import { KeyHelper } from "./key-helper";
@ -61,12 +61,10 @@ export class MkbRemapper {
public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper());
private readonly LOG_TAG = 'MkbRemapper';
private STATE: MkbRemapperStates = {
private states: MkbRemapperStates = {
currentPresetId: 0,
presets: {},
editingPresetData: null,
isEditing: false,
};
@ -83,7 +81,7 @@ export class MkbRemapper {
private constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
this.STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
this.states.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
this.bindingDialog = new Dialog({
className: 'bx-binding-dialog',
@ -117,7 +115,7 @@ export class MkbRemapper {
}
}
this.STATE.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
$elm.textContent = key.name;
$elm.dataset.keyCode = key.code;
}
@ -127,7 +125,7 @@ export class MkbRemapper {
const keySlot = parseInt($elm.dataset.keySlot!);
// Remove key from preset
this.STATE.editingPresetData!.mapping[buttonIndex][keySlot] = null;
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = null;
$elm.textContent = '';
delete $elm.dataset.keyCode;
}
@ -161,7 +159,7 @@ export class MkbRemapper {
};
private onBindingKey = (e: MouseEvent) => {
if (!this.STATE.isEditing || e.button !== 0) {
if (!this.states.isEditing || e.button !== 0) {
return;
}
@ -178,7 +176,7 @@ export class MkbRemapper {
private onContextMenu = (e: Event) => {
e.preventDefault();
if (!this.STATE.isEditing) {
if (!this.states.isEditing) {
return;
}
@ -186,15 +184,24 @@ export class MkbRemapper {
};
private getPreset = (presetId: number) => {
return this.STATE.presets[presetId];
return this.states.presets[presetId];
}
private getCurrentPreset = () => {
return this.getPreset(this.STATE.currentPresetId);
let preset = this.getPreset(this.states.currentPresetId);
if (!preset) {
// Get the first preset in the list
const firstPresetId = parseInt(Object.keys(this.states.presets)[0]);
preset = this.states.presets[firstPresetId];
this.states.currentPresetId = firstPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, firstPresetId);
}
return preset;
}
private switchPreset = (presetId: number) => {
this.STATE.currentPresetId = presetId;
this.states.currentPresetId = presetId;
const presetData = this.getCurrentPreset().data;
for (const $elm of this.allKeyElements) {
@ -223,64 +230,62 @@ export class MkbRemapper {
}
// Update state of Activate button
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.STATE.currentPresetId;
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.states.currentPresetId;
this.$activateButton.disabled = activated;
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
}
private refresh() {
private async refresh() {
// Clear presets select
while (this.$presetsSelect.firstChild) {
this.$presetsSelect.removeChild(this.$presetsSelect.firstChild);
removeChildElements(this.$presetsSelect);
const presets = await MkbPresetsDb.getInstance().getPresets();
this.states.presets = presets;
const fragment = document.createDocumentFragment();
let defaultPresetId;
if (this.states.currentPresetId === 0) {
this.states.currentPresetId = parseInt(Object.keys(presets)[0]);
defaultPresetId = this.states.currentPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
} else {
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
}
MkbPresetsDb.getInstance().getPresets().then(presets => {
this.STATE.presets = presets;
const $fragment = document.createDocumentFragment();
let defaultPresetId;
if (this.STATE.currentPresetId === 0) {
this.STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
defaultPresetId = this.STATE.currentPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
} else {
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
for (let id in presets) {
const preset = presets[id];
let name = preset.name;
if (id === defaultPresetId) {
name = `🎮 ` + name;
}
for (let id in presets) {
const preset = presets[id];
let name = preset.name;
if (id === defaultPresetId) {
name = `🎮 ` + name;
}
const $options = CE<HTMLOptionElement>('option', {value: id}, name);
$options.selected = parseInt(id) === this.states.currentPresetId;
const $options = CE<HTMLOptionElement>('option', {value: id}, name);
$options.selected = parseInt(id) === this.STATE.currentPresetId;
fragment.appendChild($options);
};
$fragment.appendChild($options);
};
this.$presetsSelect.appendChild(fragment);
this.$presetsSelect.appendChild($fragment);
// Update state of Activate button
const activated = defaultPresetId === this.states.currentPresetId;
this.$activateButton.disabled = activated;
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
// Update state of Activate button
const activated = defaultPresetId === this.STATE.currentPresetId;
this.$activateButton.disabled = activated;
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
!this.STATE.isEditing && this.switchPreset(this.STATE.currentPresetId);
});
!this.states.isEditing && this.switchPreset(this.states.currentPresetId);
}
private toggleEditing = (force?: boolean) => {
this.STATE.isEditing = typeof force !== 'undefined' ? force : !this.STATE.isEditing;
this.$wrapper.classList.toggle('bx-editing', this.STATE.isEditing);
this.states.isEditing = typeof force !== 'undefined' ? force : !this.states.isEditing;
this.$wrapper.classList.toggle('bx-editing', this.states.isEditing);
if (this.STATE.isEditing) {
this.STATE.editingPresetData = deepClone(this.getCurrentPreset().data);
if (this.states.isEditing) {
this.states.editingPresetData = deepClone(this.getCurrentPreset().data);
} else {
this.STATE.editingPresetData = null;
this.states.editingPresetData = null;
}
@ -290,7 +295,7 @@ export class MkbRemapper {
continue;
}
let disable = !this.STATE.isEditing;
let disable = !this.states.isEditing;
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
disable = !disable;
@ -328,7 +333,7 @@ export class MkbRemapper {
title: t('rename'),
icon: BxIcon.CURSOR_TEXT,
tabIndex: -1,
onClick: e => {
onClick: async () => {
const preset = this.getCurrentPreset();
let newName = promptNewName(preset.name);
@ -338,7 +343,9 @@ export class MkbRemapper {
// Update preset with new name
preset.name = newName;
MkbPresetsDb.getInstance().updatePreset(preset).then(id => this.refresh());
await MkbPresetsDb.getInstance().updatePreset(preset);
await this.refresh();
},
}),
@ -355,7 +362,7 @@ export class MkbRemapper {
// Create new preset selected name
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
this.STATE.currentPresetId = id;
this.states.currentPresetId = id;
this.refresh();
});
},
@ -376,7 +383,7 @@ export class MkbRemapper {
// Create new preset selected name
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => {
this.STATE.currentPresetId = id;
this.states.currentPresetId = id;
this.refresh();
});
},
@ -393,8 +400,8 @@ export class MkbRemapper {
return;
}
MkbPresetsDb.getInstance().deletePreset(this.STATE.currentPresetId).then(id => {
this.STATE.currentPresetId = 0;
MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then(id => {
this.states.currentPresetId = 0;
this.refresh();
});
},
@ -448,7 +455,7 @@ export class MkbRemapper {
let $elm;
const onChange = (e: Event, value: any) => {
(this.STATE.editingPresetData!.mouse as any)[key] = value;
(this.states.editingPresetData!.mouse as any)[key] = value;
};
const $row = CE('label', {
class: 'bx-settings-row',
@ -481,7 +488,7 @@ export class MkbRemapper {
style: ButtonStyle.PRIMARY,
tabIndex: -1,
onClick: e => {
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.STATE.currentPresetId);
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.states.currentPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
this.refresh();
@ -497,7 +504,7 @@ export class MkbRemapper {
tabIndex: -1,
onClick: e => {
// Restore preset
this.switchPreset(this.STATE.currentPresetId);
this.switchPreset(this.states.currentPresetId);
this.toggleEditing(false);
},
}),
@ -509,7 +516,7 @@ export class MkbRemapper {
tabIndex: -1,
onClick: e => {
const updatedPreset = deepClone(this.getCurrentPreset());
updatedPreset.data = this.STATE.editingPresetData as MkbPresetData;
updatedPreset.data = this.states.editingPresetData as MkbPresetData;
MkbPresetsDb.getInstance().updatePreset(updatedPreset).then(id => {
// If this is the default preset => refresh preset data

View File

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

View File

@ -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() {

View File

@ -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();

View File

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

View File

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

17
src/types/index.d.ts vendored
View File

@ -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: {

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
export abstract class LocalDb {
static readonly DB_NAME = 'BetterXcloud';
static readonly DB_VERSION = 1;
static readonly DB_VERSION = 2;
private db: any;
protected db!: IDBDatabase;
protected open() {
return new Promise<void>((resolve, reject) => {
@ -12,7 +12,7 @@ export abstract class LocalDb {
}
const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
request.onupgradeneeded = this.onUpgradeNeeded;
request.onupgradeneeded = this.onUpgradeNeeded.bind(this);
request.onerror = e => {
console.log(e);
@ -29,7 +29,7 @@ export abstract class LocalDb {
protected abstract onUpgradeNeeded(e: IDBVersionChangeEvent): void;
protected table(name: string, type: string): Promise<IDBObjectStore> {
protected table(name: string, type: IDBTransactionMode): Promise<IDBObjectStore> {
const transaction = this.db.transaction(name, type || 'readonly');
const table = transaction.objectStore(name);

View File

@ -18,79 +18,85 @@ export class MkbPresetsDb extends LocalDb {
BxLogger.info(this.LOG_TAG, 'constructor()');
}
private createTable(db: IDBDatabase) {
const presets = db.createObjectStore(this.TABLE_PRESETS, {
keyPath: 'id',
autoIncrement: true,
});
presets.createIndex('name_idx', 'name');
}
protected onUpgradeNeeded(e: IDBVersionChangeEvent): void {
const db = (e.target! as any).result;
switch (e.oldVersion) {
case 0: {
const presets = db.createObjectStore(this.TABLE_PRESETS, {
keyPath: 'id',
autoIncrement: true,
});
presets.createIndex('name_idx', 'name');
break;
}
const db = (e.target! as any).result as IDBDatabase;
if (db.objectStoreNames.contains('undefined')) {
db.deleteObjectStore('undefined');
}
if (!db.objectStoreNames.contains(this.TABLE_PRESETS)) {
this.createTable(db);
}
}
private presetsTable() {
return this.open()
.then(() => this.table(this.TABLE_PRESETS, 'readwrite'))
private async presetsTable() {
await this.open();
return await this.table(this.TABLE_PRESETS, 'readwrite');
}
newPreset(name: string, data: any) {
return this.presetsTable()
.then(table => this.add(table, {name, data}))
.then(([table, id]) => new Promise<number>(resolve => resolve(id)));
async newPreset(name: string, data: any) {
const table = await this.presetsTable();
const [, id] = await this.add(table, { name, data });
return id;
}
updatePreset(preset: MkbStoredPreset) {
return this.presetsTable()
.then(table => this.put(table, preset))
.then(([table, id]) => new Promise(resolve => resolve(id)));
async updatePreset(preset: MkbStoredPreset) {
const table = await this.presetsTable();
const [, id] = await this.put(table, preset);
return id;
}
deletePreset(id: number) {
return this.presetsTable()
.then(table => this.delete(table, id))
.then(([table, id]) => new Promise(resolve => resolve(id)));
async deletePreset(id: number) {
const table = await this.presetsTable();
await this.delete(table, id);
return id;
}
getPreset(id: number): Promise<MkbStoredPreset> {
return this.presetsTable()
.then(table => this.get(table, id))
.then(([table, preset]) => new Promise(resolve => resolve(preset)));
async getPreset(id: number): Promise<MkbStoredPreset> {
const table = await this.presetsTable();
const [, preset] = await this.get(table, id);
return preset;
}
getPresets(): Promise<MkbStoredPresets> {
return this.presetsTable()
.then(table => this.count(table))
.then(([table, count]) => {
if (count > 0) {
return new Promise(resolve => {
this.getAll(table)
.then(([table, items]) => {
const presets: MkbStoredPresets = {};
items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
resolve(presets);
});
});
}
async getPresets(): Promise<MkbStoredPresets> {
const table = await this.presetsTable();
const [, count] = await this.count(table);
// Create "Default" preset when the table is empty
const preset: MkbStoredPreset = {
name: t('default'),
data: MkbPreset.DEFAULT_PRESET,
}
// Return stored presets
if (count > 0) {
const [, items] = await this.getAll(table);
const presets: MkbStoredPresets = {};
items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
return new Promise<MkbStoredPresets>(resolve => {
this.add(table, preset)
.then(([table, id]) => {
preset.id = id;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
return presets;
}
resolve({[id]: preset});
});
});
});
// Create "Default" preset when the table is empty
const preset: MkbStoredPreset = {
name: t('default'),
data: MkbPreset.DEFAULT_PRESET,
};
const [, id] = await this.add(table, preset);
preset.id = id;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
return {
[id]: preset,
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
@ -170,7 +175,7 @@ const Texts = {
(e: any) => `${e.version} 버전 사용가능`,
(e: any) => `Dostępna jest nowa wersja ${e.version}`,
(e: any) => `Versão ${e.version} disponível`,
,
(e: any) => `Версия ${e.version} доступна`,
(e: any) => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
(e: any) => `${e.version} sayılı yeni sürüm mevcut`,
(e: any) => `Доступна версія ${e.version}`,
@ -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",

View File

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

View File

@ -87,7 +87,7 @@ export class XhomeInterceptor {
];
XhomeInterceptor.consoleAddrs = {};
for (const pair in pairs) {
for (const pair of pairs) {
const [keyAddr, keyPort] = pair;
if (serverDetails[keyAddr]) {
const port = serverDetails[keyPort];