MWICore

mwicore已弃用,不再更新,仅保留,请安装mooket

// ==UserScript==
// @name         MWICore
// @namespace    http://tampermonkey.net/
// @version      0.2.1
// @description  mwicore已弃用,不再更新,仅保留,请安装mooket
// @author       IOMisaka
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';
    let injectSpace = "mwi";//use window.mwi to access the injected object
    if (window[injectSpace]) return;//已经注入
    let io = {//供外部调用的接口
        version: "0.1.2",//版本号,未改动原有接口只更新最后一个版本号,更改了接口会更改次版本号,主版本暂时不更新,等稳定之后再考虑主版本号更新
        MWICoreInitialized: false,//是否初始化完成,完成会还会通过window发送一个自定义事件 MWICoreInitialized

        /*一些可以直接用的游戏数据,欢迎大家一起来整理
        game.state.levelExperienceTable //经验表
        game.state.skillingActionTypeBuffsDict },
        game.state.characterActions //[0]是当前正在执行的动作,其余是队列中的动作
        */
        game: null,//注入游戏对象,可以直接访问游戏中的大量数据和方法以及消息事件等
        lang: null,//语言翻译, 例如中文物品lang.zh.translation.itemNames['/items/coin']
        buffCalculator: null,//注入buff计算对象buffCalculator.mergeBuffs()合并buffs,计算加成效果等
        alchemyCalculator: null,//注入炼金计算对象


        /* marketJson兼容接口 */
        get marketJson() {
            return this.MWICoreInitialized && new Proxy(this.coreMarket, {
                get(coreMarket, prop) {
                    if (prop === "market") {
                        return new Proxy(coreMarket, {
                            get(coreMarket, itemHridOrName) {
                                return coreMarket.getItemPrice(itemHridOrName);
                            }
                        });
                    }
                    return null;
                }

            });
        },
        coreMarket: null,//coreMarket.marketData 格式{"/items/apple_yogurt:0":{ask,bid,time}}
        itemNameToHridDict: null,//物品名称反查表
        ensureItemHrid: function (itemHridOrName) {
            let itemHrid = this.itemNameToHridDict[itemHridOrName];
            if (itemHrid) return itemHrid;
            if (itemHridOrName?.startsWith("/items/") && this?.game?.state?.itemDetailDict) return itemHridOrName;
            return null;
        },//各种名字转itemHrid,找不到返回原itemHrid或者null
        hookCallback: hookCallback,//hook回调,用于hook游戏事件等 例如聊天消息mwi.hookCallback(mwi.game, "handleMessageChatMessageReceived", (_,obj)=>{console.log(obj)})
        fetchWithTimeout: fetchWithTimeout,//带超时的fetch
    };
    window[injectSpace] = io;

    async function patchScript(node) {
        try {
            const scriptUrl = node.src;
            node.remove();
            const response = await fetch(scriptUrl);
            if (!response.ok) throw new Error(`Failed to fetch script: ${response.status}`);

            let sourceCode = await response.text();

            // Define injection points as configurable patterns
            const injectionPoints = [
                {
                    pattern: "Ca.a.use",
                    replacement: `window.${injectSpace}.lang=Oa;Ca.a.use`,
                    description: "注入语言翻译对象"
                },
                {
                    pattern: "class lp extends s.a.Component{constructor(e){var t;super(e),t=this,",
                    replacement: `class lp extends s.a.Component{constructor(e){var t;super(e),t=this,window.${injectSpace}.game=this,`,
                    description: "注入游戏对象"

                },
                {
                    pattern: "var Q=W;",
                    replacement: `window.${injectSpace}.buffCalculator=W;var Q=W;`,
                    description: "注入buff计算对象"
                },
                {
                    pattern: "class Dn",
                    replacement: `window.${injectSpace}.alchemyCalculator=Mn;class Dn`, 
                    description: "注入炼金计算对象"
                },
                {
                    pattern: "var z=q;",
                    replacement: `window.${injectSpace}.actionManager=q;var z=q;`,
                    description: "注入动作管理对象"
                }
            ];

            injectionPoints.forEach(({ pattern, replacement,description }) => {
                if (sourceCode.includes(pattern)) {
                    sourceCode = sourceCode.replace(pattern, replacement);
                    console.info(`MWICore injecting: ${description}`);
                }else{
                    console.warn(`MWICore injecting failed: ${description}`);
                }
            });

            const newNode = document.createElement('script');
            newNode.textContent = sourceCode;
            document.body.appendChild(newNode);
            console.info('MWICore patched successfully.')
        } catch (error) {
            console.error('MWICore patching failed:', error);
        }
    }
    new MutationObserver((mutationsList, obs) => {
        mutationsList.forEach((mutationRecord) => {
            for (const node of mutationRecord.addedNodes) {
                if (node.src) {
                    if (node.src.search(/.*main\..*\.chunk.js/)===0) {
                        obs.disconnect();
                        patchScript(node);
                    }
                }
            }
        });
    }).observe(document, { childList: true, subtree: true });

    /**
     * Hook回调函数并添加后处理
     * @param {Object} targetObj 目标对象
     * @param {string} callbackProp 回调属性名
     * @param {Function} handler 后处理函数
     */
    function hookCallback(targetObj, callbackProp, handler) {
        const originalCallback = targetObj[callbackProp];

        if (!originalCallback) {
            throw new Error(`Callback ${callbackProp} does not exist`);
        }

        targetObj[callbackProp] = function (...args) {
            const result = originalCallback.apply(this, args);

            // 异步处理
            if (result && typeof result.then === 'function') {
                return result.then(res => {
                    handler(res, ...args);
                    return res;
                });
            }

            // 同步处理
            handler(result, ...args);
            return result;
        };

        // 返回取消Hook的方法
        return () => {
            targetObj[callbackProp] = originalCallback;
        };
    }
    /**
     * 带超时功能的fetch封装
     * @param {string} url - 请求URL
     * @param {object} options - fetch选项
     * @param {number} timeout - 超时时间(毫秒),默认10秒
     * @returns {Promise} - 返回fetch的Promise
     */
    function fetchWithTimeout(url, options = {}, timeout = 10000) {
        // 创建AbortController实例
        const controller = new AbortController();
        const { signal } = controller;

        // 设置超时计时器
        const timeoutId = setTimeout(() => {
            controller.abort(new Error(`请求超时: ${timeout}ms`));
        }, timeout);

        // 合并选项,添加signal
        const fetchOptions = {
            ...options,
            signal
        };

        // 发起fetch请求
        return fetch(url, fetchOptions)
            .then(response => {
                // 清除超时计时器
                clearTimeout(timeoutId);

                if (!response.ok) {
                    throw new Error(`HTTP错误! 状态码: ${response.status}`);
                }
                return response;
            })
            .catch(error => {
                // 清除超时计时器
                clearTimeout(timeoutId);

                // 如果是中止错误,重新抛出超时错误
                if (error.name === 'AbortError') {
                    throw new Error(`请求超时: ${timeout}ms`);
                }
                throw error;
            });
    }

    /*实时市场模块*/
    const HOST = "https://mooket.qi-e.top";
    const MWIAPI_URL = "https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json";

    class Price {
        bid = -1;
        ask = -1;
        time = -1;
        constructor(bid, ask, time) {
            this.bid = bid;
            this.ask = ask;
            this.time = time;
        }
    }
    class CoreMarket {
        marketData = {};//市场数据,带强化等级,存储格式{"/items/apple_yogurt:0":{ask,bid,time}}
        fetchTimeDict = {};//记录上次API请求时间,防止频繁请求
        ttl = 300;//缓存时间,单位秒

        constructor() {
            //core data
            let marketDataStr = localStorage.getItem("MWICore_marketData") || "{}";
            this.marketData = JSON.parse(marketDataStr);

            //mwiapi data
            let mwiapiJsonStr = localStorage.getItem("MWIAPI_JSON") || localStorage.getItem("MWITools_marketAPI_json");
            let mwiapiObj = null;
            if (mwiapiJsonStr) {
                mwiapiObj = JSON.parse(mwiapiJsonStr);
                this.mergeMWIData(mwiapiObj);
            }
            if (!mwiapiObj || Date.now() / 1000 - mwiapiObj.time > 1800) {//超过半小时才更新,因为mwiapi每小时更新一次,频繁请求github会报错
                fetch(MWIAPI_URL).then(res => {
                    res.text().then(mwiapiJsonStr => {
                        mwiapiObj = JSON.parse(mwiapiJsonStr);
                        this.mergeMWIData(mwiapiObj);
                        //更新本地缓存数据
                        localStorage.setItem("MWIAPI_JSON", mwiapiJsonStr);//更新本地缓存数据
                        console.info("MWIAPI_JSON updated:", new Date(mwiapiObj.time * 1000).toLocaleString());
                    })
                });
            }

            //市场数据更新
            hookCallback(io.game, "handleMessageMarketItemOrderBooksUpdated", (res, obj) => {
                //更新本地
                let timestamp = parseInt(Date.now() / 1000);
                let itemHrid = obj.marketItemOrderBooks.itemHrid;
                obj.marketItemOrderBooks?.orderBooks?.forEach((item, enhancementLevel) => {
                    let bid = item.bids?.length > 0 ? item.bids[0].price : -1;
                    let ask = item.asks?.length > 0 ? item.asks[0].price : -1;
                    this.updateItem(itemHrid + ":" + enhancementLevel, new Price(bid, ask, timestamp));
                });
                //上报数据
                obj.time = timestamp;
                fetchWithTimeout(`${HOST}/market/upload/order`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify(obj)
                });
            })
            setInterval(() => { this.save(); }, 1000 * 600);//十分钟保存一次
        }

        /**
         * 合并MWIAPI数据,只包含0级物品
         *
         * @param obj 包含市场数据的对象
         */
        mergeMWIData(obj) {
            Object.entries(obj.market).forEach(([itemName, price]) => {
                let itemHrid = io.ensureItemHrid(itemName);
                if (itemHrid) this.updateItem(itemHrid + ":" + 0, new Price(price.bid, price.ask, obj.time), false);//本地更新
            });
            this.save();
        }
        mergeCoreDataBeforeSave() {
            let obj = JSON.parse(localStorage.getItem("MWICore_marketData") || "{}");
            Object.entries(obj).forEach(([itemHridLevel, priceObj]) => {
                this.updateItem(itemHridLevel, priceObj, false);//本地更新
            });
            //不保存,只合并
        }
        save() {//保存到localStorage
            this.mergeCoreDataBeforeSave();//从其他角色合并保存的数据
            localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
        }

        /**
         * 部分特殊物品的价格
         * 例如金币固定1,牛铃固定为牛铃袋/10的价格
         * @param {string} itemHrid - 物品hrid
         * @returns {Price|null} - 返回对应商品的价格对象,如果没有则null
         */
        getSpecialPrice(itemHrid) {
            switch (itemHrid) {
                case "/items/coin":
                    return new Price(1, 1, Date.now() / 1000);
                case "/items/cowbell": {
                    let cowbells = this.getItemPrice("/items/bag_of_10_cowbells");
                    return cowbells && { bid: cowbells.bid / 10, ask: cowbells.ask / 10, time: cowbells.time };
                }
                default:
                    return null;
            }
        }
        /**
         * 获取商品的价格
         *
         * @param {string} itemHridOrName 商品HRID或名称
         * @param {number} [enhancementLevel=0] 装备强化等级,普通商品默认为0
         * @returns {number|null} 返回商品的价格,如果商品不存在或无法获取价格则返回null
         */
        getItemPrice(itemHridOrName, enhancementLevel = 0) {
            let itemHrid = io.ensureItemHrid(itemHridOrName);
            if (!itemHrid) return null;
            let specialPrice = this.getSpecialPrice(itemHrid);
            if (specialPrice) return specialPrice;

            let priceObj = this.marketData[itemHrid + ":" + enhancementLevel];
            if (Date.now() / 1000 - this.fetchTimeDict[itemHrid + ":" + enhancementLevel] < this.ttl) return priceObj;//1分钟内直接返回本地数据,防止频繁请求服务器
            if (this.fetchCount > 10) return priceObj;//过于频繁请求服务器
            
            this.fetchCount++;
            setTimeout(() => { this.fetchCount--;this.getItemPriceAsync(itemHrid, enhancementLevel); }, this.fetchCount*200);//后台获取最新数据,防止阻塞
            return priceObj;
        }
        fetchCount = 0;//防止频繁请求服务器,后台获取最新数据

        /**
         * 异步获取物品价格
         *
         * @param {string} itemHridOrName 物品HRID或名称
         * @param {number} [enhancementLevel=0] 增强等级,默认为0
         * @returns {Promise<Object|null>} 返回物品价格对象或null
         */
        async getItemPriceAsync(itemHridOrName, enhancementLevel = 0) {
            let itemHrid = io.ensureItemHrid(itemHridOrName);
            if (!itemHrid) return null;
            let specialPrice = this.getSpecialPrice(itemHrid);
            if (specialPrice) return specialPrice;
            let itemHridLevel = itemHrid + ":" + enhancementLevel;
            if (Date.now() / 1000 - this.fetchTimeDict[itemHridLevel] < this.ttl) return this.marketData[itemHridLevel];//1分钟内请求直接返回本地数据,防止频繁请求服务器

            // 构造请求参数
            const params = new URLSearchParams();
            params.append("name", itemHrid);
            params.append("level", enhancementLevel);

            let res = null;
            try {
                this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//记录请求时间
                res = await fetchWithTimeout(`${HOST}/market/item/price?${params}`);
            } catch (e) {
                return this.marketData[itemHridLevel];//获取失败,直接返回本地数据
            } finally {
                
            }
            if (res.status != 200) {
                return this.marketData[itemHridLevel];//获取失败,直接返回本地数据
            }
            let resObj = await res.json();
            let priceObj = new Price(resObj.bid, resObj.ask, Date.now() / 1000);
            if (resObj.ttl) this.ttl = resObj.ttl;//更新ttl
            this.updateItem(itemHridLevel, priceObj);
            return priceObj;
        }
        updateItem(itemHridLevel, priceObj, isFetch = true) {
            let localItem = this.marketData[itemHridLevel];
            if (isFetch) this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//fetch时间戳
            if (!localItem || localItem.time < priceObj.time) {//服务器数据更新则更新本地数据
                this.marketData[itemHridLevel] = priceObj;
            }
        }
        save() {
            localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
        }
    }
    function init() {
        io.itemNameToHridDict = {};
        Object.entries(io.lang.en.translation.itemNames).forEach(([k, v]) => { io.itemNameToHridDict[v] = k });
        Object.entries(io.lang.zh.translation.itemNames).forEach(([k, v]) => { io.itemNameToHridDict[v] = k });
        io.coreMarket = new CoreMarket();
        io.MWICoreInitialized = true;
        window.dispatchEvent(new CustomEvent("MWICoreInitialized"))
        console.info("MWICoreInitialized event dispatched. window.mwi.MWICoreInitialized=true");
    }
    new Promise(resolve => {
        const interval = setInterval(() => {
            if (io.game && io.lang) {//等待必须组件加载完毕后再初始化
                clearInterval(interval);
                resolve();
            }
        }, 100);
    }).then(() => {
        init();
    });

})();