Xbox Cloud Gaming 游戏信息汉化

汉化信息并使游戏搜索支持中文

安裝腳本?
作者推薦腳本

您可能也會喜歡 Xbox CLoud Gaming优化整合

安裝腳本
// ==UserScript==
// @name                 Xbox Cloud Gaming 游戏信息汉化
// @namespace            https://b1ue.me
// @description          汉化信息并使游戏搜索支持中文
// @version              1.0.8
// @author               b1ue
// @license              MIT
// @match                https://www.xbox.com/*/*play*
// @run-at               document-start
// @grant                GM_getValue
// @grant                GM_setValue
// @grant                GM_registerMenuCommand
// @grant                GM_notification
// @grant                unsafeWindow
// ==/UserScript==

(function() {
    'use strict';
    const Nconfig = {
        localizeGameInfo: true,			//游戏信息汉化
        alwaysShowTitle: 2,				//移动端保持标题显示
        fullScreenlandscape: false,		//全屏时强制横屏
    };

    const BXCG = {
        getValue: (key, defaultValue) => {
            if(typeof GM_getValue === 'function') return GM_getValue(key, defaultValue);
            let _val = localStorage.getItem('BXCG_' + key) ?? JSON.stringify(defaultValue);
            try { _val = JSON.parse(_val) ;} catch (e) {}
            return _val;
        },
        setValue: (key, value) => {
            if(typeof GM_setValue === 'function') return GM_setValue(key, value);
            return localStorage.setItem('BXCG_' + key, JSON.stringify(value));
        },
        getTitleList: async() => {
            return await fetch("https://update.greasyfork.org/scripts/493376/xbt-title.js", {cache: "reload"}).then(res => res.text()).catch(() => null);
        }
    };

    const oWindow = self.unsafeWindow || window;
    Object.keys(Nconfig).forEach(key => {
        let _val = BXCG.getValue(key);
        if(_val != null) Nconfig[key] = _val;
    });
    let game_titles = {};
    (async () => {
        const timestamp = () => Math.floor(new Date().getTime() / 1000);
        let resText = BXCG.getValue('game_titles');
        const game_titles_gettime = BXCG.getValue("game_titles_gettime", 0);
        if(!resText || timestamp() - game_titles_gettime > 7200){
            resText = await BXCG.getTitleList();
            if(resText){
                BXCG.setValue("game_titles", resText);
                BXCG.setValue("game_titles_gettime", 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];
    }

    document.addEventListener("fullscreenchange", function (e) {
        if (document.fullscreenElement) {
            try {
                Nconfig.fullScreenlandscape && screen?.orientation?.lock("landscape");
            } catch (e) {}
        }
    });

    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(!Nconfig.localizeGameInfo) 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('/sigls/v2')) {
            let ourl = new URL(url);
            ourl.searchParams.set("language",browserFirstLanguage);
            let nurl = ourl.toString();
            arg[0] = new Request(nurl, {
                method: arg0.method,
                headers: arg0.headers,
            });
        } 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);
    }

    function toggleTitleVisible(){
        let action = 0;
        switch(Nconfig.alwaysShowTitle){
            case 0:
            default:
                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%!important;
	visibility: visible!important;
}
[class^="GameCard-module__children___"] {
	visibility: hidden!important;
}`
            const xfextraStyle = document.createElement('style');
            xfextraStyle.id = 'showTitle';
            xfextraStyle.innerHTML = nCss;
            const docxf = document.head || document.documentElement;
            docxf.appendChild(xfextraStyle);
        }
    }

    let __PRELOADED_STATE__;
    const handle_state = (state) => {
        if(Nconfig.localizeGameInfo){
            try {
                state.appContext.marketInfo.locale = browserFirstLanguage;
                for(let gId in state.xcloud.products.data){
                    let title_zh = "";
                    if(gId in game_titles && (title_zh = game_titles[gId][0])) state.xcloud.products.data[gId].data.title = title_zh;
                }
                for(let i in state.xcloud.hero.heroData.data){
                    const gId = state.xcloud.hero.heroData.data[i].productID;
                    let title_zh = "";
                    if(gId in game_titles && (title_zh = game_titles[gId][0])) state.xcloud.hero.heroData.data[i].title = title_zh;
                    state.xcloud.hero.heroData.data[i].subtitle = '';
                }
            } catch (e) {}
        }
        __PRELOADED_STATE__ = state;
    };

    Object.defineProperty(oWindow, '__PRELOADED_STATE__', {
        configurable: true,
        get: () => {
            return __PRELOADED_STATE__;
        },
        set: state => {
            handle_state(state);
        }
    });

    const NATIVE_Object_defineProperty = Object.defineProperty;
    Object.defineProperty = (obj, prop, descriptor) => {
        if(obj === oWindow && prop === '__PRELOADED_STATE__'){
            if(descriptor && descriptor?.hasOwnProperty('set')){
                const NATIVE_descriptor_set = descriptor.set;
                descriptor.set = (state, ...arg) => {
                    handle_state(state);

                    descriptor.set = NATIVE_descriptor_set;
                    Object.defineProperty = NATIVE_Object_defineProperty;
                    return NATIVE_descriptor_set(state, ...arg);
                };
            }
        }
        return NATIVE_Object_defineProperty(obj, prop, descriptor);
    };

    if(typeof GM_registerMenuCommand === 'function'){
        let updateMenu = (param) => {
            (param <= 1) && GM_registerMenuCommand(`${BXCG.getValue('localizeGameInfo',Nconfig.localizeGameInfo)?'✅':'❌'} 游戏信息汉化`, (event) => {
                BXCG.setValue("localizeGameInfo",!BXCG.getValue('localizeGameInfo',Nconfig.localizeGameInfo));
                if(typeof GM_notification === 'function') GM_notification({title: '设置变更', text: '修改将在刷新后生效!', tag:'notify', timeout: 2000});
                updateMenu(1);
            },{id:'localizeGameInfo_id', autoClose:false});
            if(param <= 2){
                let inx = Nconfig.alwaysShowTitle; (inx>2 || inx<0) && (inx=0);
                GM_registerMenuCommand(`${['❌','1️⃣','2️⃣'][inx]}保持标题显示[${['关闭','开启','仅移动设备'][inx]}] - 点击切换`, (event) => {
                    Nconfig.alwaysShowTitle = ++inx>2?0:inx;
                    BXCG.setValue("alwaysShowTitle", Nconfig.alwaysShowTitle);
                    toggleTitleVisible();
                    updateMenu(2);
                },{id:'alwaysShowTitle_id', autoClose:false});
            }
            (param <= 3) && GM_registerMenuCommand(`${Nconfig.fullScreenlandscape?'✅':'❌'} 全屏时强制横屏`, (event) => {
                Nconfig.fullScreenlandscape = !Nconfig.fullScreenlandscape;
                BXCG.setValue("fullScreenlandscape",Nconfig.fullScreenlandscape);
                updateMenu(3);
            },{id:'fullScreenlandscape_id', autoClose:false});
        }
        updateMenu(0);
    }

    document.addEventListener("DOMContentLoaded", (event) => {
        setTimeout(() => {toggleTitleVisible()},100);
    });

})();