Greasy Fork

MWICore

toolkit, for MilkyWayIdle.一些工具函数,和一些注入对象,市场数据API等。

目前为 2025-04-13 提交的版本。查看 最新版本

// ==UserScript==
// @name         MWICore
// @namespace    http://tampermonkey.net/
// @version      0.0.3
// @description  toolkit, for MilkyWayIdle.一些工具函数,和一些注入对象,市场数据API等。
// @author       IOMisaka
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @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 = {//供外部调用的接口
        //游戏对象
        get levelExperienceTable() { return this.game.state.levelExperienceTable },//经验表
        get skillingActionTypeBuffsDict() { return this.game.state.skillingActionTypeBuffsDict },
        get characterActions() { return this.game.state.characterActions },//[0]是当前正在执行的动作,其余是队列中的动作

        lang: null,//inject, lang.zh.translation.itemNames['/items/coin']
        buffCalculator: null,//注入buff计算对象
        alchemyCalculator: null,//注入炼金计算对象

        //core市场
        /* marketJson兼容接口 */
        get marketJson() {
            return this.inited && 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[itemHridOrName]?.isTradable)//必须是可交易的物品
                return itemHridOrName;
            return null;
        },//物品名称转HRID

        hookCallback: hookCallback,
        inited: false,//是否初始化完成

    };
    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`
                },
                {
                    pattern: "class tp extends s.a.Component{constructor(e){var t;super(e),t=this,",
                    replacement: `class tp extends s.a.Component{constructor(e){var t;super(e),t=this,window.${injectSpace}.game=this,`
                },
                {
                    pattern: "var Q=W;",
                    replacement: `window.${injectSpace}.buffCalculator=W;var Q=W;`
                },
                {
                    pattern: "class Dn",
                    replacement: `window.${injectSpace}.alchemyCalculator=Mn;class Dn`
                },
                {
                    pattern: "var z=q;",
                    replacement: `window.${injectSpace}.actionManager=q;var z=q;`
                }
            ];

            injectionPoints.forEach(({ pattern, replacement }) => {
                if (sourceCode.includes(pattern)) {
                    sourceCode = sourceCode.replace(pattern, replacement);
                }
            });

            const newNode = document.createElement('script');
            newNode.textContent = sourceCode;
            document.body.appendChild(newNode);
            console.log('Script patched successfully.')
        } catch (error) {
            console.error('Script patching failed:', error);
        }
    }
    new MutationObserver((mutationsList, obs) => {
        mutationsList.forEach((mutationRecord) => {
            for (const node of mutationRecord.addedNodes) {
                if (node.src) {
                    if (node.src.endsWith('main.aecc7346.chunk.js')) {
                        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;
        };
    }


    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 = {};
        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");
            if (mwiapiJsonStr) {
                let mwiapiObj = JSON.parse(mwiapiJsonStr);
                this.mergeData(mwiapiObj);
            } else {
                fetch(MWIAPI_URL).then(res => {
                    res.text().then(mwiapiJsonStr => {
                        let mwiapiJson = JSON.parse(mwiapiJsonStr);
                        this.mergeData(mwiapiJson);
                        //更新本地缓存数据
                        localStorage.setItem("MWIAPI_JSON", mwiapiJsonStr);//更新本地缓存数据
                        console.info("MWIAPI_JSON updated:", new Date(mwiapiJson.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;
                fetch(`${HOST}/market/upload/order`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify(obj)
                });
            })
            setInterval(() => { this.save(); }, 1000 * 600);
        }

        mergeData(obj) {
            Object.entries(obj.market).forEach(([itemName, price]) => {
                let itemHrid = io.itemNameToHridDict[itemName]
                if (itemHrid) this.updateItem(itemHrid, 0, new Price(price.bid, price.ask, obj.time));
            });
            this.save();
        }
        refreshDict = {};
        getItemPrice(itemHridOrName, enhancementLevel = 0) {
            let itemHrid = io.ensureItemHrid(itemHridOrName);
            if (!itemHrid) return null;

            let priceObj = this.marketData[itemHrid + ":" + enhancementLevel];
            if (Date.now() / 1000 - this.refreshDict[itemHrid + ":" + enhancementLevel] < 300 || this.fetchCount>10) return priceObj;//5分钟内直接返回本地数据,防止频繁请求服务器
            setTimeout( () => { this.getItemPriceAsync(itemHrid, enhancementLevel); }, 0);//后台获取最新数据,防止阻塞
            return priceObj;
        }
        fetchCount = 0;
        async getItemPriceAsync(itemHridOrName, enhancementLevel = 0) {
            let itemHrid = io.ensureItemHrid(itemHridOrName);
            if (!itemHrid) return null;

            const params = new URLSearchParams();
            params.append("itemHrid", itemHrid);
            params.append("enhancementLevel", enhancementLevel);

            if (Date.now() / 1000 - this.refreshDict[itemHrid + ":" + enhancementLevel] < 300 || this.fetchCount > 10) {
                return this.marketData[itemHrid + ":" + enhancementLevel];//5分钟内直接返回本地数据,防止频繁请求服务器
            }
            

            let res = null;
            this.fetchCount++;
            try{
                this.refreshDict[itemHrid + ":" + enhancementLevel] = Date.now() / 1000;//记录更新时间
                res = await fetch(`${HOST}/market/item/price?${params}`);
            }catch(e){
                return this.getItemPrice(itemHrid, enhancementLevel);//获取失败,直接返回本地数据
            }finally{
                this.fetchCount--;
            }
            if (res.status != 200) {

                return this.getItemPrice(itemHrid, enhancementLevel);//获取失败,直接返回本地数据
            }
            let priceObj = await res.json();
            this.updateItem(res.itemHrid, res.enhancementLevel, priceObj)
            return priceObj;
        }
        updateItem(itemHrid, enhancementLevel, priceObj) {
            let localItem = this.marketData[itemHrid + ":" + enhancementLevel];
            this.refreshDict[itemHrid + ":" + enhancementLevel] = Date.now() / 1000;//更新时间戳
            if (!localItem || localItem.time < priceObj.time) {//服务器数据更新则更新本地数据
                this.marketData[itemHrid + ":" + enhancementLevel] = 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.inited = true;
        window.dispatchEvent(new CustomEvent("MWICore_inited"))
        console.log("MWICore_inited event dispatched. window.mwi.inited=true");
    }
    new Promise(resolve => {
        const interval = setInterval(() => {
            if (io.game && io.lang) {
                clearInterval(interval);
                resolve();
            }
        }, 500);
    }).then(() => {
        init();
    });

})();