Xbox CLoud Gaming Enhancement

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

Asenna tämä skripti?
Author's suggested script

Saatat myös pitää

Asenna tämä skripti
// ==UserScript==
// @name                 Xbox CLoud Gaming Enhancement
// @namespace            https://b1ue.me
// @description          Xbox CLoud Gaming优化整合 扩展脚本.用以支持原脚本没有覆盖到的部分功能,比如 进入全屏模式时自动横屏/游戏信息汉化/一些游戏的本地多人合作
// @version              1.0.1
// @author               b1ue
// @license              MIT
// @match                https://www.xbox.com/*/*play*
// @run-at               document-start
// @grant                GM_getResourceText
// @grant                GM.xmlHttpRequest
// @grant                unsafeWindow
// @require              https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.4.1/jquery.min.js
// @connect              update.greasyfork.org
// @resource game_titles https://update.greasyfork.org/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;
    });

    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;
            if(!resText) resText = GM_getResourceText("game_titles");
            if(resText){
                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;
        }
    });

})();