Luvie / Majsoul Plus ResourcePack Loader (JP)

// ==UserScript==
// @name         Majsoul Plus ResourcePack Loader (JP)
// @namespace    majsoul
// @version      0.0.5
// @description  Apply *multiple* majsoul plus resource pack using UserScript, added ingame announce data replacer.
// @author       YF-DEV, SHINJJANGGU, rishubil, Yudong, Luvie
// @license      MIT
// @include      https://game.mahjongsoul.com/*
// @grant        unsafeWindow
// @run-at       document-start
// @updateURL    https://openuserjs.org/meta/Luvie/Majsoul_Plus_ResourcePack_Loader_(JP).meta.js
// @downloadURL  https://openuserjs.org/install/Luvie/Majsoul_Plus_ResourcePack_Loader_(JP).user.js
// ==/UserScript==

(async function () {
    'use strict';

    const GAME_BASE_URL = 'https://game.mahjongsoul.com/';
    const version_re = /v\d+\.\d+\.\d+\.w\//i;

    /**
       적용할 리소스팩의 이름, Url, 적용 여부(적용하려면 true, 적용하지 않으려면 false)를 아래 resourceArray에 작성하면 됩니다.
       resourceUrl의 root에 majsoul plus 2.0의 포맷에 맞는 resourcepack.json 파일이 있어야합니다.
       기본적인 replace구문만 지원하며 from/to 구문은 지원하지 않습니다.
       (https://github.com/MajsoulPlus/majsoul-plus/wiki/v2_resourcepack)

       각각의 resourcepack.json에 정의된 리소스 파일을 게임 시작시 교체해줍니다.
       resourceUrl의 root에서 원본과 동일한 path에 교체할 리소스 파일이 업로드 되어 있어야 합니다.

       여러 리소스팩을 동시에 적용할 수 있도록 작성한 코드입니다.
       또한 한글 패치 스크립트에 신규 추가된 공지사항 번역 기능도 같이 추가되어 있습니다. (announce 변수를 참조하십시오)

       한글화 패치의 리소스 및 스크립트 코드를 상당 부분 그대로 사용하였음을 밝힙니다. 감사합니다.
       (https://openuserjs.org/scripts/Shijjanggu/JakhonHangle_JP_SERVER)
    */

    let resourceArray = [
        {name: "majsoul_eng_tiles", resourceUrl: "https://luviels.github.io/majsoulEngTiles/", isEnable: true},
        {name: "majsoul_korean_pack", resourceUrl: "https://shinjjanggu.github.io/jakhonplus/korean/", isEnable: true}
    ];

    let announce = await(await fetch("https://shinjjanggu.github.io/jakhonplus/korean/announce.json")).json()

    resourceArray.forEach(async function(data) {
        if (!data.resourceUrl.endsWith("/")) {
            console.log("resourceUrl not endsWith slash, force add slash.")
            data.resourceUrl = data.resourceUrl + "/";
        }

        let jsonData = await(await fetch(data.resourceUrl + "resourcepack.json")).json()
        data.json = jsonData
    });

    console.log(resourceArray);

    replaceXhrOpen(); // metadata(json, fnt, atlas...) replace when XMLHttpRequest is open.
    replaceCodeScript(); // image, sound, ttf replace when code.js is onload.

    function replaceCodeScript() {
        let observer = null;
        observer = new MutationObserver(function (mutations) {
            mutations.forEach(function (mutation) {
                const scripts = document.getElementsByTagName('script');
                for (let i = 0; i < scripts.length; i++) {
                    const script = scripts[i];
                    if (script.src && script.src.indexOf('code.js') !== -1) {
                        script.onload = function () {
                            replaceLayaLoadImage();
                            replaceLayaLoadSound();
                            replaceLayaLoadTtf();
                            replaceAnnounce();
                        };
                        observer.disconnect();
                    }
                }
            });
        });
        const config = {
            childList: true,
            subtree: true
        };
        observer.observe(document, config);
    }

    function updateUrl(url) {
        const original_url = url;
        if (url.startsWith(GAME_BASE_URL)) {
            url = url.substring(GAME_BASE_URL.length);
        }
        url = url.replace(version_re, '');

        for (var idx in resourceArray){
            if (resourceArray[idx].isEnable && resourceArray[idx].json.replace.includes(url)) {
                url = resourceArray[idx].resourceUrl + 'assets/' + url;
                return url;
            }
        };

         return original_url;
    }

    function replaceXhrOpen() {
        const original_function = window.XMLHttpRequest.prototype.open;

        window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
            return original_function.call(this, method, updateUrl(url), async, user, password);
        };
    }

    function replaceLayaLoadImage() {
        const original_function = Laya.Loader.prototype._loadImage;
        
        Laya.Loader.prototype._loadImage = function (url) {
            return original_function.call(this, updateUrl(url));
        }
    }

    function replaceLayaLoadSound() {
        const original_function = Laya.Loader.prototype._loadSound;
        
        Laya.Loader.prototype._loadSound = function (url) {
            return original_function.call(this, updateUrl(url));
        }
    }

    function replaceLayaLoadTtf() {
        const original_function = Laya.Loader.prototype._loadTTF;
        
        Laya.Loader.prototype._loadTTF = function (url) {
            return original_function.call(this, updateUrl(url));
        }
    }

    // replace via announcements' array index (somewhat risky?)
    function replaceAnnounce() {
        const original_function = uiscript.UI_Info._refreshAnnouncements
        uiscript.UI_Info._refreshAnnouncements = function (t) {
            t.announcements.forEach((a)=> {
                if (announce[a.id]) {
                    a.title = announce[a.id].title
                    a.content = announce[a.id].content
                }
            })
            return original_function.call(this, t)
        }
    }
})();