Greasy Fork

来自缓存

Greasy Fork is available in English.

Xbox CLoud Gaming Enhancement

Xbox CLoud Gaming优化整合 扩展脚本.用以支持原脚本没有覆盖到的部分功能,比如 进入全屏模式时自动横屏/游戏信息汉化/一些游戏的本地多人合作

当前为 2024-04-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                 Xbox CLoud Gaming Enhancement
// @namespace            https://b1ue.me
// @description          Xbox CLoud Gaming优化整合 扩展脚本.用以支持原脚本没有覆盖到的部分功能,比如 进入全屏模式时自动横屏/游戏信息汉化/一些游戏的本地多人合作
// @version              0.9
// @author               b1ue
// @license              MIT
// @match                https://www.xbox.com/*/*play*
// @run-at               document-start
// @grant                GM_getResourceText
// @grant                unsafeWindow
// @require              https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.4.1/jquery.min.js
// @resource game_titles https://update.greasyfork.icu/scripts/493376/xbt-title.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;
    });
    const resText = GM_getResourceText("game_titles");
    let 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 === 'http://greasyfork.icu/zh-CN/scripts/455741-xbox-cloud-gaming%E4%BC%98%E5%8C%96%E6%95%B4%E5%90%88/versions') {
            let res = originFetch('http://greasyfork.icu/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("http://greasyfork.icu/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;
        }
    });
 
})();