Compare commits

..

1 Commits

Author SHA1 Message Date
1d018cc0a3 Test native MKB feature with Remote Play 2024-01-21 18:05:19 +07:00
2 changed files with 82 additions and 160 deletions

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 3.1
// @version 3.0.5
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,15 +13,14 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '3.1';
const SCRIPT_VERSION = '3.0.5';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const ENABLE_XCLOUD_LOGGER = false;
const ENABLE_PRELOAD_BX_UI = false;
const USE_DEV_TOUCH_LAYOUT = false;
const ENABLE_NATIVE_MKB_BETA = false;
window.NATIVE_MKB_TITLES = [
'BT5P2X999VH2',
// Not working anymore
// '9PMQDM08SNK9', // MS Flight Simulator
// '9NP1P1WFS0LB', // Halo Infinite
@ -38,6 +37,16 @@ window.NATIVE_MKB_TITLES = [
// '9P731Z4BBCT3', // Atomic Heart
];
window.REMOTE_PLAY_NATIVE_MKB_TITLES = [
// DOOM 64
'DOOM64',
'BT5P2X999VH2',
// Cyperpunk 2077
'222473492',
'BX3M8L83BBRW',
];
console.log(`[Better xCloud] readyState: ${document.readyState}`);
const BxEvent = {
@ -707,7 +716,6 @@ const Translations = {
"en-US": "Deadzone counterweight",
"es-ES": "Contrapeso de la zona muerta",
"ja-JP": "デッドゾーンのカウンターウエイト",
"pl-PL": "Przeciwwaga martwej strefy",
"pt-BR": "Contador da Zona Morta",
"ru-RU": "Противодействие мертвой зоне игры",
"tr-TR": "Ölü alan denge ağırlığı",
@ -1282,17 +1290,6 @@ const Translations = {
"vi-VN": "Nhấn vào để kích hoạt",
"zh-CN": "单击以启用",
},
"mkb-disclaimer": {
"de-DE": "Das Nutzen dieser Funktion beim Online-Spielen könnte als Betrug angesehen werden",
"en-US": "Using this feature when playing online could be viewed as cheating",
"es-ES": "Usar esta función al jugar en línea podría ser visto como trampas",
"ja-JP": "オンラインプレイでこの機能を使用すると不正行為と判定される可能性があります",
"pl-PL": "Używanie tej funkcji podczas grania online może być postrzegane jako oszukiwanie",
"pt-BR": "Usar esta função em jogos online pode ser considerado como uma forma de trapaça",
"ru-RU": "Использование этой функции при игре онлайн может рассматриваться как читерство",
"uk-UA": "Використання цієї функції під час гри онлайн може розглядатися як шахрайство",
"vi-VN": "Sử dụng chức năng này khi chơi trực tuyến có thể bị xem là gian lận",
},
"mouse-and-keyboard": {
"de-DE": "Maus & Tastatur",
"en-US": "Mouse & Keyboard",
@ -2174,7 +2171,6 @@ const Translations = {
"en-US": "Stick decay minimum",
"es-ES": "Disminuir mínimamente el analógico",
"ja-JP": "スティックの減衰の最小値",
"pl-PL": "Minimalne opóźnienie drążka",
"pt-BR": "Mínimo decaimento do analógico",
"ru-RU": "Минимальная перезарядка стика",
"tr-TR": "Çubuğun ortalanma süresi minimumu",
@ -2187,7 +2183,6 @@ const Translations = {
"en-US": "Stick decay strength",
"es-ES": "Intensidad de decaimiento del analógico",
"ja-JP": "スティックの減衰の強さ",
"pl-PL": "Siła opóźnienia drążka",
"pt-BR": "Força de decaimento do analógico",
"ru-RU": "Скорость перезарядки стика",
"tr-TR": "Çubuğun ortalanma gücü",
@ -2232,7 +2227,6 @@ const Translations = {
"en-US": "Support Better xCloud",
"es-ES": "Apoyar a Better xCloud",
"ja-JP": "Better xCloudをサポート",
"pl-PL": "Wesprzyj Better xCloud",
"pt-BR": "Suporte ao Melhor xCloud",
"ru-RU": "Поддержать Better xCloud",
"tr-TR": "Better xCloud'a destek ver",
@ -2760,7 +2754,6 @@ window.addEventListener('load', e => {
});
const NATIVE_FETCH = window.fetch;
const SERVER_REGIONS = {};
var IS_PLAYING = false;
var STREAM_WEBRTC;
@ -2769,12 +2762,9 @@ var STREAM_AUDIO_GAIN_NODE;
var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID;
var GAME_XBOX_TITLE_ID;
var GAME_PRODUCT_ID;
var APP_CONTEXT;
window.BX_EXPOSED = {};
let IS_REMOTE_PLAYING;
let REMOTE_PLAY_CONFIG;
@ -3123,7 +3113,6 @@ class TitlesInfo {
const details = titleInfo.details;
TitlesInfo.update(details.productId, {
titleId: titleInfo.titleId,
xboxTitleId: details.xboxTitleId,
// Has more than one input type -> must have touch support
hasTouchSupport: (details.supportedInputTypes.length > 1),
});
@ -3340,7 +3329,7 @@ class LoadingScreen {
class TouchController {
static get #EVENT_SHOW_DEFAULT_CONTROLLER() {
static get #EVENT_SHOW_CONTROLLER() {
return new MessageEvent('message', {
data: '{"content":"{\\"layoutId\\":\\"\\"}","target":"/streaming/touchcontrols/showlayoutv2","type":"Message"}',
origin: 'better-xcloud',
@ -3361,8 +3350,6 @@ class TouchController {
static #showing = false;
static #dataChannel;
static #customLayouts = {};
static enable() {
TouchController.#enable = true;
}
@ -3375,17 +3362,8 @@ class TouchController {
return TouchController.#enable;
}
static #showDefault() {
TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);
TouchController.#showing = true;
}
static #show() {
if (GAME_XBOX_TITLE_ID && GAME_XBOX_TITLE_ID in TouchController.#customLayouts) {
TouchController.loadCustomLayout(GAME_XBOX_TITLE_ID);
} else {
TouchController.#showDefault();
}
TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_CONTROLLER);
TouchController.#showing = true;
}
@ -3421,55 +3399,6 @@ class TouchController {
}, 10);
}
static #getCustomLayout(xboxTitleId, callback) {
xboxTitleId = '' + xboxTitleId;
if (xboxTitleId in TouchController.#customLayouts) {
callback(TouchController.#customLayouts[xboxTitleId]);
return;
}
let url;
if (USE_DEV_TOUCH_LAYOUT) {
url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/dev/${xboxTitleId}.json`;
} else {
url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/${xboxTitleId}.json`;
}
window.BX_EXPOSED.touch_layout_manager && NATIVE_FETCH(url)
.then(resp => resp.json())
.then(json => {
TouchController.#customLayouts[xboxTitleId] = json;
callback(json);
})
.reject(() => {
TouchController.#customLayouts[xboxTitleId] = null;
callback(null);
});
}
static loadCustomLayout(xboxTitleId) {
if (!window.BX_EXPOSED.touch_layout_manager) {
return;
}
xboxTitleId = '' + xboxTitleId;
TouchController.#getCustomLayout(xboxTitleId, json => {
json && setTimeout(() => {
window.BX_EXPOSED.touch_layout_manager.changeLayoutForScope({
type: 'showLayout',
scope: '' + xboxTitleId,
subscope: 'base',
layout: {
id: 'System.Standard',
displayName: 'System',
layoutFile: {
content: json.layout,
},
}
});
}, 1000);
});
}
static setup() {
const $style = document.createElement('style');
document.documentElement.appendChild($style);
@ -3534,26 +3463,9 @@ class TouchController {
return;
}
// Load custom touch layout
let showCustom = false;
try {
if (msg.data.includes('/titleinfo')) {
const json = JSON.parse(JSON.parse(msg.data).content);
if (json.focused) {
const xboxTitleId = parseInt(json.titleid, 16);
GAME_XBOX_TITLE_ID = xboxTitleId;
TouchController.loadCustomLayout(xboxTitleId);
showCustom = true;
} else {
GAME_XBOX_TITLE_ID = null;
}
}
} catch (e) { console.log(e) }
// Dispatch a message to display generic touch controller
if (!showCustom && msg.data.includes('touchcontrols/showtitledefault')) {
TouchController.#showDefault();
if (msg.data.includes('touchcontrols/showtitledefault')) {
TouchController.#show();
}
});
@ -6307,8 +6219,7 @@ class Preferences {
'ready': () => {
const options = Preferences.SETTINGS[Preferences.STREAM_CODEC_PROFILE].options;
if (Object.keys(options).length <= 1) {
Preferences.SETTINGS[Preferences.STREAM_CODEC_PROFILE].unsupported = true;
Preferences.SETTINGS[Preferences.STREAM_CODEC_PROFILE].note = '⚠️ ' + __('browser-unsupported-feature');
Preferences.SETTINGS[Preferences.STREAM_CODEC_PROFILE].unsupported = __('browser-unsupported-feature');
}
},
},
@ -6336,7 +6247,7 @@ class Preferences {
'all': __('tc-all-games'),
'off': __('off'),
},
'unsupported': !HAS_TOUCH_SUPPORT,
'unsupported': !HAS_TOUCH_SUPPORT ? __('device-unsupported-touch') : false,
},
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
'default': 'default',
@ -6345,7 +6256,7 @@ class Preferences {
'white': __('tc-all-white'),
'muted': __('tc-muted-colors'),
},
'unsupported': !HAS_TOUCH_SUPPORT,
'unsupported': !HAS_TOUCH_SUPPORT ? __('device-unsupported-touch') : false,
},
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
'default': 'default',
@ -6353,7 +6264,7 @@ class Preferences {
'default': __('default'),
'muted': __('tc-muted-colors'),
},
'unsupported': !HAS_TOUCH_SUPPORT,
'unsupported': !HAS_TOUCH_SUPPORT ? __('device-unsupported-touch') : false,
},
[Preferences.STREAM_SIMPLIFY_MENU]: {
'default': false,
@ -6400,11 +6311,6 @@ class Preferences {
const userAgent = (window.navigator.orgUserAgent || window.navigator.userAgent || '').toLowerCase();
return userAgent.match(/(android|iphone|ipad)/) ? __('browser-unsupported-feature') : false;
})(),
'ready': () => {
const pref = Preferences.SETTINGS[Preferences.MKB_ENABLED];
const note = __(pref.unsupported ? 'browser-unsupported-feature' : 'mkb-disclaimer');
Preferences.SETTINGS[Preferences.MKB_ENABLED].note = '⚠️ ' + note;
},
},
[Preferences.MKB_DEFAULT_PRESET_ID]: {
@ -6811,7 +6717,7 @@ class Patcher {
funcStr = funcStr.replace('onServerDisconnectMessage(e){', `onServerDisconnectMessage(e) {
const msg = JSON.parse(e);
if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {
if (msg.reason === 'WarningForBeingIdle') {
try {
this.sendKeepAlive();
return;
@ -6824,12 +6730,32 @@ class Patcher {
// Enable Remote Play feature
remotePlayConnectMode: function(funcStr) {
const text = 'connectMode:"cloud-connect"';
const text = 'connectMode:"cloud-connect",';
if (!funcStr.includes(text)) {
return false;
}
return funcStr.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`);
const newCode = `
connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect",
remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',
`;
return funcStr.replace(text, newCode);
},
// Remote Play MKB support
remotePlayMkb: function(funcStr) {
const text = 'handleRemotePlayTitleInputConfig(e){';
if (!funcStr.includes(text)) {
return false;
}
const newCode = `
const supportMkb = window.REMOTE_PLAY_NATIVE_MKB_TITLES.includes(e.titleId);
this.gameStream.session.updateInputConfigurationAsync({ enableMouseAndKeyboard: supportMkb });
`;
return funcStr.replace(text, text + newCode);
},
// Disable trackEvent() function
@ -6912,13 +6838,11 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
];
// Enable native Mouse and Keyboard support
if (ENABLE_NATIVE_MKB_BETA) {
newSettings.push('EnableMouseAndKeyboard: true');
newSettings.push('ShowMouseKeyboardSetting: true');
newSettings.push('EnableMouseAndKeyboard: true');
newSettings.push('ShowMouseKeyboardSetting: true');
if (getPref(Preferences.MKB_ABSOLUTE_MOUSE)) {
newSettings.push('EnableAbsoluteMouse: true');
}
if (getPref(Preferences.MKB_ABSOLUTE_MOUSE)) {
newSettings.push('EnableAbsoluteMouse: true');
}
const newCode = newSettings.join(',');
@ -6928,12 +6852,12 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
},
mkbIsMouseAndKeyboardTitle: function(funcStr) {
const text = 'isMouseAndKeyboardTitle:()=>yn';
const text = 'isMouseAndKeyboardTitle:()=>';
if (!funcStr.includes(text)) {
return false;
}
return funcStr.replace(text, `isMouseAndKeyboardTitle:()=>(function(e) { return e && e.details ? window.NATIVE_MKB_TITLES.includes(e.details.productId) : true; })`);
return funcStr.replace(text, text + `(function(e) { return e && e.details ? (window.BX_REMOTE_PLAY_CONFIG ? window.REMOTE_PLAY_NATIVE_MKB_TITLES : window.NATIVE_MKB_TITLES).includes(e.details.productId) : true; }),uwuwu:()=>`);
},
mkbMouseAndKeyboardEnabled: function(funcStr) {
@ -6992,16 +6916,6 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
funcStr = funcStr.substring(0, bracketIndex) + 'return 0;' + funcStr.substring(bracketIndex);
return funcStr;
},
exposeTouchLayoutManager: function(funcStr) {
const text = 'this._perScopeLayoutsStream=new';
if (!funcStr.includes(text)) {
return false;
}
funcStr = funcStr.replace(text, 'window.BX_EXPOSED["touch_layout_manager"] = this,' + text);
return funcStr;
},
};
static #PATCH_ORDERS = [
@ -7031,7 +6945,7 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
[
'overrideSettings',
ENABLE_NATIVE_MKB_BETA && 'mkbIsMouseAndKeyboardTitle',
'mkbIsMouseAndKeyboardTitle',
HAS_TOUCH_SUPPORT && 'patchUpdateInputConfigurationAsync',
],
];
@ -7041,13 +6955,14 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
getPref(Preferences.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'],
['playVibration'],
getPref(Preferences.STREAM_TOUCH_CONTROLLER) === 'all' && ['exposeTouchLayoutManager'],
ENABLE_XCLOUD_LOGGER && ['enableConsoleLogging'],
getPref(Preferences.REMOTE_PLAY_ENABLED) && ['remotePlayMkb'],
[
'disableGamepadDisconnectedScreen',
ENABLE_NATIVE_MKB_BETA && 'mkbMouseAndKeyboardEnabled',
'mkbMouseAndKeyboardEnabled',
],
];
@ -8718,6 +8633,8 @@ function interceptHttpRequests() {
const PREF_STREAM_TOUCH_CONTROLLER = getPref(Preferences.STREAM_TOUCH_CONTROLLER);
const PREF_AUDIO_MIC_ON_PLAYING = getPref(Preferences.AUDIO_MIC_ON_PLAYING);
const orgFetch = window.fetch;
const consoleAddrs = {};
const patchIceCandidates = function(...arg) {
@ -8726,7 +8643,7 @@ function interceptHttpRequests() {
const url = (typeof request === 'string') ? request : request.url;
if (url && url.endsWith('/ice') && url.includes('/sessions/') && request.method === 'GET') {
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().text().then(text => {
@ -8801,7 +8718,7 @@ function interceptHttpRequests() {
// Get console IP
if (url.includes('/configuration')) {
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(obj => {
@ -8824,7 +8741,7 @@ function interceptHttpRequests() {
});
}
return patchIceCandidates(...arg) || NATIVE_FETCH(...arg);
return patchIceCandidates(...arg) || orgFetch(...arg);
}
if (IS_REMOTE_PLAYING && url.includes('/login/user')) {
@ -8848,7 +8765,7 @@ function interceptHttpRequests() {
console.log(e);
}
return NATIVE_FETCH(...arg);
return orgFetch(...arg);
}
if (IS_REMOTE_PLAYING && url.includes('/titles')) {
@ -8868,7 +8785,7 @@ function interceptHttpRequests() {
});
arg[0] = request;
return NATIVE_FETCH(...arg);
return orgFetch(...arg);
}
// ICE server candidates
@ -8879,7 +8796,7 @@ function interceptHttpRequests() {
// Server list
if (!url.includes('xhome.') && url.endsWith('/v2/login/user')) {
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(obj => {
@ -8954,12 +8871,12 @@ function interceptHttpRequests() {
});
arg[0] = newRequest;
return NATIVE_FETCH(...arg);
return orgFetch(...arg);
}
// Get wait time
if (PREF_UI_LOADING_SCREEN_WAIT_TIME && url.includes('xboxlive.com') && url.includes('/waittime/')) {
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(json => {
if (json.estimatedAllocationTimeInSeconds > 0) {
@ -8975,7 +8892,7 @@ function interceptHttpRequests() {
if (url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.hide();
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
// Touch controller for all games
if (PREF_STREAM_TOUCH_CONTROLLER === 'all') {
@ -9002,9 +8919,7 @@ function interceptHttpRequests() {
overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.inputConfiguration.enableVibration = true;
if (ENABLE_NATIVE_MKB_BETA) {
overrides.inputConfiguration.enableMouseAndKeyboard = true;
}
overrides.inputConfiguration.enableMouseAndKeyboard = true;
// Enable touch controller
if (TouchController.isEnabled()) {
@ -9030,7 +8945,7 @@ function interceptHttpRequests() {
// catalog.gamepass
if (url.startsWith('https://catalog.gamepass.com') && url.includes('/products')) {
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(json => {
for (let productId in json.Products) {
@ -9043,7 +8958,7 @@ function interceptHttpRequests() {
}
if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && (url.endsWith('/titles') || url.endsWith('/mru'))) {
const promise = NATIVE_FETCH(...arg);
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(json => {
for (let game of json.results) {
@ -9066,7 +8981,7 @@ function interceptHttpRequests() {
});
}
return NATIVE_FETCH(...arg);
return orgFetch(...arg);
}
}
@ -9182,7 +9097,6 @@ function injectSettingsButton($parent) {
},
*/
[__('touch-controller')]: {
_note: !HAS_TOUCH_SUPPORT ? '⚠️ ' + __('device-unsupported-touch') : null,
[Preferences.STREAM_TOUCH_CONTROLLER]: __('tc-availability'),
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: __('tc-standard-layout-style'),
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: __('tc-custom-layout-style'),
@ -9228,8 +9142,14 @@ function injectSettingsButton($parent) {
const setting = Preferences.SETTINGS[settingId];
const settingLabel = SETTINGS_UI[groupLabel][settingId];
const settingNote = setting.note;
let settingLabel;
let settingNote;
if (Array.isArray(SETTINGS_UI[groupLabel][settingId])) {
[settingLabel, settingNote] = SETTINGS_UI[groupLabel][settingId];
} else {
settingLabel = SETTINGS_UI[groupLabel][settingId];
}
let $control, $inpCustomUserAgent;
let labelAttrs = {};
@ -9300,8 +9220,11 @@ function injectSettingsButton($parent) {
// Disable unsupported settings
if (setting.unsupported) {
$control.disabled = true;
$control.title = setting.unsupported;
}
$control.disabled && ($control.style.cursor = 'help');
const $label = CE('label', labelAttrs, settingLabel);
if (settingNote) {
$label.appendChild(CE('b', {}, settingNote));
@ -10207,11 +10130,10 @@ function onStreamStarted($video) {
GAME_PRODUCT_ID = matches.groups.product_id;
} else {
GAME_TITLE_ID = 'remote-play';
GAME_PRODUCT_ID = null;
}
// Enable MKB
if (getPref(Preferences.MKB_ENABLED) && (!ENABLE_NATIVE_MKB_BETA || !window.NATIVE_MKB_TITLES.includes(GAME_PRODUCT_ID))) {
if (getPref(Preferences.MKB_ENABLED) && (!window.NATIVE_MKB_TITLES.includes(GAME_PRODUCT_ID))) {
console.log('Emulate MKB');
MkbHandler.INSTANCE.init();
}