// ==UserScript==
// @name Xbox CLoud Gaming Enhancement
// @namespace https://b1ue.me
// @description Xbox CLoud Gaming优化整合 扩展脚本.用以支持原脚本没有覆盖到的部分功能,比如 进入全屏模式时自动横屏/游戏信息汉化/一些游戏的本地多人合作
// @version 1.0.0
// @author b1ue
// @license MIT
// @match https://www.xbox.com/*/*play*
// @run-at document-start
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @connect update.greasyfork.org
// @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.4.1/jquery.min.js
// ==/UserScript==
(function() {
'use strict';
const Nconfig = {
localizeGameInfo: 1,
supportLocalCoOp: 0,
alwaysShowTitle: 2,
no_need_VPN_play: 0,
enableRemotePlay: 0,
};
Object.keys(Nconfig).forEach(key => {
let _val = localStorage.getItem(key + 'GM');
try { _val = JSON.parse(_val) ;} catch (e) {}
if(_val != null) Nconfig[key] = _val;
});
let game_titles = {};
(async () => {
const timestamp = () => Math.floor(new Date().getTime() / 1000);
let resText = localStorage.getItem('game_titles_GM');
const game_titles_gettime = localStorage.getItem("game_titles_gettime_GM") || 0;
if(!resText || timestamp() - game_titles_gettime > 7200){
const r = await GM.xmlHttpRequest({url: "https://update.greasyfork.org/scripts/493376/xbt-title.js", nocache:true});
if(r.status == 200){
resText = r.responseText;
localStorage.setItem("game_titles_GM", resText);
localStorage.setItem("game_titles_gettime_GM", timestamp());
}
}
game_titles = JSON.parse(resText);
})();
let allFullLanguages = [];
let browserFirstLanguage = "zh-CN";
navigator.languages.forEach(language => {
const reg = /^[a-z]{2}-[A-Z]{2}$/;
const isFullLanguage = reg.test(language);
if (isFullLanguage) allFullLanguages.push(language);
});
if (allFullLanguages.length > 0) {
browserFirstLanguage = allFullLanguages[0];
}
const oWindow = self.unsafeWindow || window;
document.addEventListener("fullscreenchange", function (e) {
if (document.fullscreenElement) {
try {
screen?.orientation?.lock("landscape");
} catch (e) {}
}
});
let checkIpsuc = 0;
const originFetch = oWindow.fetch;
oWindow.fetch = async (...arg) => {
let arg0 = arg[0];
let url = "";
let isRequest = false;
switch (typeof arg0) {
case "object":
url = arg0.url;
isRequest = true;
break;
case "string":
url = arg0;
break;
default:
break;
}
if (!url.includes('xhome.') && url.indexOf('/v2/login/user') > -1) {//xgpuweb.gssv-play-prod.xboxlive.com
if(checkIpsuc === 0){
checkIpsuc = 1;
let res = originFetch(...arg).catch(error => {
checkIpsuc = -1;
let remain_count = 5;
const check_state = () => {
let oTitle = $("[class*='UnsupportedMarketPage-module__title']:visible")[0];
if(oTitle){
if(remain_count > 0){
oTitle.innerText = (Nconfig.no_need_VPN_play==1?'免代理失败':'访问错误') + ',页面将在' + (remain_count--) + '秒后刷新';
setTimeout(check_state, 1000);
} else {
location.reload();
}
}
};
setTimeout(check_state, 5000);
});
return res;
}
}
if (url === 'https://greasyfork.org/zh-CN/scripts/455741-xbox-cloud-gaming%E4%BC%98%E5%8C%96%E6%95%B4%E5%90%88/versions') {
let res = originFetch('https://greasyfork.org/scripts/455741/versions.json').then(response => {
response.text = () => response.clone().json().then(json => {
const version = json?.[0]?.version;
const fake_html = `<v><ul class="history_versions"><li><span class="version-number"><a>v${version}</a></span></li></ul></v>`;
return Promise.resolve(fake_html);
});
return response;
});
return res;
}
if (url === 'https://xhome.gssv-play-prod.xboxlive.com/v2/login/user') {
if(Nconfig.enableRemotePlay === 0){
return new Promise(()=>{}); //Promise.reject("未开启串流功能,过滤多余请求");
}
}
if(Nconfig.localizeGameInfo != 1) return originFetch(...arg);
if (url.includes('/v3/products')) {
let ourl = new URL(url);
let json = await arg0.json();
let body = JSON.stringify(json);
ourl.searchParams.set("language",browserFirstLanguage);
let nurl = ourl.toString();
arg[0] = new Request(nurl, {
method: arg0.method,
headers: arg0.headers,
body: body,
});
let res = originFetch(...arg).then(response => {
response.json = () => response.clone().json().then(json => {
for(let gId in json.Products){
let title_zh = "";
if(gId in game_titles && (title_zh = game_titles[gId][0])) json.Products[gId].ProductTitle = title_zh;
}
return Promise.resolve(json);
});
return response;
});
return res;
} else if (url.includes('/search/v2')) {
let ourl = new URL(url);
let json = await arg0.json();
let body = JSON.stringify(json);
ourl.searchParams.set("language",browserFirstLanguage);
let nurl = ourl.toString();
arg[0] = new Request(nurl, {
method: arg0.method,
headers: arg0.headers,
body: body,
});
const query = json.Query;
const Scope = json.Scope;
if(query && Scope === 'EDGEWATER'){
let new_SearchResults = []
for(let gId in game_titles){
if(game_titles[gId][0].includes(query) || game_titles[gId][1].includes(query)){
new_SearchResults.push(gId);
}
}
let res = originFetch(...arg).then(response => {
response.json = () => response.clone().json().then(async json => {
new_SearchResults = new_SearchResults.filter(gId => !(gId in json.SearchResults));
if(new_SearchResults.length > 0){
const response = await originFetch(`https://catalog.gamepass.com/v3/products?market=${ourl.searchParams.get("market")}&language=${browserFirstLanguage}&hydration=${ourl.searchParams.get("hydration")}`, {
method: 'POST',
headers: arg0.headers,
body: JSON.stringify({
Products: new_SearchResults,
}),
});
const data = await response.json();
for(let gId in data.Products){
json.Products[gId] = data.Products[gId];
}
}
for(let gId in json.Products){
let title_zh = "";
if(gId in game_titles && (title_zh = game_titles[gId][0])) json.Products[gId].ProductTitle = title_zh;
}
json.SearchResults = json.SearchResults.concat(new_SearchResults);
return Promise.resolve(json);
});
return response;
});
return res;
}
} else if (url.includes('/v4/api/selection')) {
let res = originFetch(...arg).then(response => {
response.json = () => response.clone().json().then(json => {
let items_array = json?.batchrsp?.items;
if(items_array){
items_array.forEach( _item => {
const item = JSON.parse(_item?.item);
const title = item?.ad?.items?.[0]?.title;
const actionLink = item?.ad?.items?.[0]?.actionLink;
const gId = /msgamepass:\/\/details\?id=([A-Z0-9]+)/.exec(actionLink)?.[1]
if(title && gId){
let title_zh = "";
if(gId in game_titles && (title_zh = game_titles[gId][0])){
item.ad.items[0].title = title_zh;
_item.item = JSON.stringify(item);
}
}
});
}
return Promise.resolve(json);
});
return response;
});
return res;
}
return originFetch(...arg);
}
if(Nconfig.supportLocalCoOp == 1){
const native_includes = String.prototype.includes;
String.prototype.includes = function(){
let funcStr = this;
const text = 'this.gamepadMappingsToSend=[],';
if (native_includes.call(funcStr, text)){
String.prototype.includes = native_includes;
let native_replace = String.prototype.replace;
String.prototype.replace = function(){
let funcStr = this;
const text = 'this.gamepadMappingsToSend=[],';
if (native_includes.call(funcStr, text)){
String.prototype.replace = native_replace;
const patchFunc = () => {
let match;
let onGamepadChangedStr = this.onGamepadChanged.toString();
// match = onGamepadChangedStr.match(/onGamepadChanged\((?<type>\w+),(?<index>\w+),(?<wasAdded>\w+)\)/);
onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');
eval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);
let onGamepadInputStr = this.onGamepadInput.toString();
match = onGamepadInputStr.match(/(\w+\.GamepadIndex)/);
if (match) {
const gamepadIndexVar = match[0];
onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);
eval(`this.onGamepadInput = function ${onGamepadInputStr}`);
console.log('✅ Successfully patched local co-op support');
} else {
console.log('❌ Unable to patch local co-op support');
}
}
let patchFuncStr = patchFunc.toString();
patchFuncStr = patchFuncStr.substring(7, patchFuncStr.length - 1);
const newCode = `true; ${patchFuncStr}; true,`;
funcStr = funcStr.replace(text, text + newCode);
console.log(`应用 本地合作模式 修补`);
}
return native_replace.apply(funcStr, arguments);
}
return true;
}
return native_includes.apply(funcStr, arguments);
}
}
const settingsConfig = [
{
label: '游戏信息汉化:',
type: 'radio',
name: 'localizeGameInfo',
display: 'block',
options: [
{ value: 1, text: '开', id: 'localizeGameInfoOn' },
{ value: 0, text: '关', id: 'localizeGameInfoOff' }
],
checkedValue: Nconfig.localizeGameInfo,
needHr: true
},
{
label: '本地合作支持:',
type: 'radio',
name: 'supportLocalCoOp',
display: 'block',
options: [
{ value: 1, text: '开', id: 'supportLocalCoOpOn' },
{ value: 0, text: '关', id: 'supportLocalCoOpOff' }
],
checkedValue: Nconfig.supportLocalCoOp,
needHr: true
},
{
label: '保持标题显示:',
showLable: true,
type: 'dropdown',
name: 'alwaysShowTitle',
display: 'block',
options: [
{ value: 0, text: '关闭'},
{ value: 1, text: '开启'},
{ value: 2, text: '仅移动设备'}
],
selectedValue: Nconfig.alwaysShowTitle,
ignoreChange: true,
needHr: true
},
]
// 函数用于生成单个设置项的HTML
function generateSettingElement(setting) {
let settingHTML = `<lable style="display:${setting.display};white-space: nowrap;margin-bottom:0.375rem;" class="${setting.name + 'Dom'}">`;
if (setting.type === 'radio') {
if (setting.options != undefined) {
settingHTML += `<label style="display:block;text-align:left;"><div style="display: inline;">${setting.label}</div>`;
setting.options.forEach(option => {
if (option == null) { return; }
settingHTML += `
<label style="cursor: pointer;"><input type="radio" class="${setting.name + 'Listener'} settingsBoxInputRadio${(setting.ignoreChange != undefined && setting.ignoreChange && ' ignore-change')}" style="outline:none;" name="${setting.name}"
id="${option.id}" value="${option.value}" ${option.value === setting.checkedValue ? 'checked' : ''}>${option.text}</label>
`;
});
}
if (setting.moreDom != undefined) {
settingHTML += setting.moreDom;
}
settingHTML += '</label>';
} else if (setting.type === 'text') {
settingHTML += `<label style="display: block;text-align:left;"><div style="display: inline;">${setting.label}</div>`;
settingHTML += `
<input type="text" style="display: inline;outline: none;width: 125px;" id="${setting.name}" class="${setting.name}Listener${(setting.ignoreChange != undefined && setting.ignoreChange && ' ignore-change')}" value="${setting.value}" placeholder="请输入${setting.label}"/>
`;
settingHTML += `</label>`;
} else if (setting.type === 'dropdown') {
if (setting.showLable == true) {
settingHTML += `<label style="display: block;text-align:left;${setting.css}"><div style="display: inline;">${setting.label}</div>`;
}
if (setting.options.length == undefined) {
setting.options = Object.keys(setting.options);
}
settingHTML += `
<select style="outline: none;margin-bottom:5px;" class="${setting.name + 'Listener' + (setting.ignoreChange != undefined && setting.ignoreChange && ' ignore-change')}">
${setting.options.map(option => `<option value="${option.value ?? option}" ${(option.value ?? option) === setting.selectedValue ? 'selected' : ''}>${option.text ?? option}</option>`).join('')}
</select>
`;
if (setting.moreDom != undefined) {
settingHTML += setting.moreDom;
}
}
settingHTML += `</lable>`;
if (setting.needHr) {
settingHTML += `<hr style="background-color: black;width:95%" />`
}
return settingHTML;
}
function initSettingBox(oSettingBox){
let needrefresh = 0;
let settingsHTML = '';
settingsConfig.forEach(setting => {
settingsHTML += generateSettingElement(setting);
});
$(oSettingBox).children('button.closeSetting1').before(settingsHTML);
$(oSettingBox).find('span.blink-text:contains("更新咯~")').attr('onclick','window.open("https://greasyfork.org/zh-CN/scripts/455741");');
$(oSettingBox).find('a[href]').attr('target','_blank');
$('.closeSetting1').click(function() {
$(oSettingBox).parent().css('display', 'none');
$('body').css('overflow', 'visible');
if(getconfstring() == origin_config){
event.cancelBubble = true;
event.stopPropagation();
return false;
}
if (needrefresh == 1) history.go(0);
});
$(document).on('click', '.localizeGameInfoListener', function () {
needrefresh = 1;
localStorage.setItem("localizeGameInfoGM", $(this).val());
$('.closeSetting1').text('确定');
});
$(document).on('click', '.supportLocalCoOpListener', function () {
needrefresh = 1;
localStorage.setItem("supportLocalCoOpGM", $(this).val());
$('.closeSetting1').text('确定');
});
$(document).on('change', '.alwaysShowTitleListener', function () {
Nconfig.alwaysShowTitle = parseInt($(this).val());
localStorage.setItem("alwaysShowTitleGM", $(this).val());
toggleTitleVisible();
});
$('#popSetting').css('position','fixed');
$(oSettingBox).parent().css('height','100%');
const getconfstring = () =>{
let text = '';
$(oSettingBox).find('input[type="text"],input[type="checkbox"]:checked,input[type="radio"]:checked,select').each((i,o) => { if(!$(o).hasClass('ignore-change')) text += $(o).val() });
return text;
};
let origin_config = getconfstring();
const mutation = new MutationObserver(function(mutationRecoards, observer) {
if(mutationRecoards[0].target.innerText == '确定'){
mutationRecoards[0].target.innerText = (getconfstring() == origin_config)?'关闭':'刷新';
}
})
mutation.observe($(oSettingBox).find('.closeSetting1')[0], {
characterData: true,
childList: true
});
}
let checkSettingBox_Interval = setInterval(() => {
let oSettingBox;
if(oSettingBox = document.querySelector('#settingsBackgroud .settingsBox')){
clearInterval(checkSettingBox_Interval);
initSettingBox(oSettingBox);
}
},500);
function toggleTitleVisible(){
let action = 0;
switch(Nconfig.alwaysShowTitle){
case 0:
action = 1;
break
case 1:
action = 2;
break
case 2:
action = ('ontouchstart' in window || navigator.msMaxTouchPoints > 0)?2:1;
break
}
if(action == 1){
document.querySelector('style#showTitle')?.remove();
}else if(action == 2){
if(document.querySelector('style#showTitle')) return;
const nCss = `
[class^="GameCard-module__gameTitleInnerWrapper___"] {
max-height: 100%;
visibility: visible;
}
[class^="GameCard-module__children___"] {
visibility: hidden;
}`
const xfextraStyle = document.createElement('style');
xfextraStyle.id = 'showTitle';
xfextraStyle.innerHTML = nCss;
const docxf = document.head || document.documentElement;
docxf.appendChild(xfextraStyle);
}
}
$(document).ready(function () {
setTimeout(() => {
if (checkIpsuc < 0) return;
let oTitle = $("[class*='UnsupportedMarketPage-module__title']:visible")[0];
if (oTitle) oTitle.innerText = "如果长时间停留在本页,请尝试刷新";
}, 5000);
setTimeout(() => {toggleTitleVisible()},1000);
});
const go_origin = history.go;
history.go = (arg) => {
return go_origin.call(history, arg);
};
const setInterval_origin = oWindow.setInterval;
oWindow.setInterval = (func, interval) => {
let funcStr = func.toString();
if(funcStr.includes('if (checkIpsuc)')){
oWindow.setInterval = setInterval_origin;
return;
}
return setInterval_origin(func, interval);
};
let __PRELOADED_STATE__;
Object.defineProperty(oWindow, '__PRELOADED_STATE__', {
configurable: true,
get: () => {
return __PRELOADED_STATE__;
},
set: state => {
state.appContext.marketInfo.locale = browserFirstLanguage;
__PRELOADED_STATE__ = state;
}
});
})();