MWICommon

Common API for MWIScript

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MWICommon
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  Common API for MWIScript
// @author       Lhiok
// @license      MIT
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @supportURL   https://github.com/Lhiok/MWIScript/
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    "use strict";

    const selfSpace = "mwi_common";

    if (window[selfSpace]) return;

    let mwi_common = window[selfSpace] = {
        
        /**************************************** Public ****************************************/

        eventNames: null, // 事件名称

        injected: false, // 是否注入完成 完成触发document的mwi_common_injected事件
        marketDataLoaded: false, // 市场数据是否加载完成
        isZh: false, // 是否中文

        handleGameAPI: null, // 调用游戏API

        addNotification: null, // 添加通知
        openItemDictionary: null, // 打开物品字典
        openItemToolTip: null, // 打开物品提示
        closeItemToolTip: null, // 关闭物品提示
        gotoMarket: null, // 打开市场
        goToAction: null, // 前往行动
        goToMonster: null, // 前往打怪

        getItemNameByHrid: null, // 通过物品ID获取名称
        getItemHridByName: null, // 通过名称获取物品ID
        getActionHridByName: null, // 通过名称获取行动ID
        getMonsterHridByName: null, // 通过名称获取怪物ID

        getCharacterId: null, // 获取角色ID
        getEquipmentByLocationHrid: null, // 通过位置获取装备
        getItemNumByHrid: null, // 通过物品ID获取数量
        getItemPriceByHrid: null, // 通过物品ID获取价格
        getItemPriceYesterdayByHrid: null, // 通过物品ID获取昨日价格

        /**************************************** Private ****************************************/

        game: null, // 游戏对象
        lang: null, // 语言翻译对象

        itemNameToHrid: null, // 名称到物品ID
        marketData: null, // 市场数据
        marketDataYesterday: null, // 市场数据昨日
        marketDataInvalidTime: 0, // 市场数据失效时间
    };

    /**************************************** Log ****************************************/
    
    function info(info) {
        console.info('[MWI_Common]: ' + info);
    }

    function warn(info) {
        console.warn('[MWI_Common]: ' + info);
    }

    function error(info) {
        console.error('[MWI_Common]: ' + info);
    }
    
    /**************************************** Private ****************************************/
    
    let marketDataRetryTimeoutId = 0;
    const marketDataUrl = "https://www.milkywayidle.com/game_data/marketplace.json";
    const marketDataYesterdayUrl = "https://raw.githubusercontent.com/Lhiok/MWIScript/json/marketplace_yesterday.json";
    const langDataUrl = "https://raw.githubusercontent.com/Lhiok/MWIScript/main/lang.json";

    const marketDataStorageKey = "mwi_common_market_data";
    const marketDataInvalidTimeStorageKey = "mwi_common_market_data_invalid_time";
    
    const eventNames = mwi_common.eventNames = {
        injected: "mwi_common_injected", // 注入完成
        langDataLoaded: "mwi_common_lang_data_loaded", // lang数据加载完成
        marketDataLoaded: "mwi_common_market_data_loaded", // 市场数据加载完成
        marketDataUpdate: "mwi_common_market_data_update", // 市场数据更新
        login: "mwi_common_login", // 登录
        actionUpdate : "mwi_common_action_update", // 行动队列变更
        actionComplete: "mwi_common_action_complete", // 完成一次行动
        taskUpdate: "mwi_common_task_update", // 任务变更
        itemUpdate: "mwi_common_item_update", // 物品变更
        skillUpdate: "mwi_common_skill_update", // 技能变更 (更换技能&使用技能书)
        lootOpened: "mwi_common_loot_opened", // 打开宝箱
        marketListingsUpdate: "mwi_common_market_listings_update", // 市场挂牌更新 (上下架&成功交易&提取金币)
        equipmentUpdate: "mwi_common_equipment_update", // 装备变更
        consumableUpdate: "mwi_common_consumable_update", // 消耗品变更 (更换&持续时间更新)
        battleConditionUpdate: "mwi_common_battle_condition_update", // 更改战斗条件预设
        loadoutsUpdate: "mwi_common_loadouts_update", // 装配方案更新
        houseRoomsUpdate: "mwi_common_house_rooms_update", // 房屋更新
        actionBuffUpdate: "mwi_common_action_buff_update", // 行动加成更新
        newBattle: "mwi_common_new_battle", // 新一轮战斗 单个EPH算一轮
        gotoMarket: "mwi_common_goto_market", // 前往市场
        marketItemUpdate: "mwi_common_market_item_update", // 市场物品变更
    };

    // API调用事件
    const gameAPICallEvents = {
        ["handleMessageInitCharacterData"]: [eventNames.login],
        ["handleMessageActionsUpdated"]: [eventNames.actionUpdate],
        ["handleMessageActionCompleted"]: [eventNames.actionComplete],
        ["handleMessageQuestsUpdated"]: [eventNames.taskUpdate],
        ["handleMessageItemsUpdated"]: [eventNames.itemUpdate],
        ["handleMessageAbilitiesUpdated"]: [eventNames.skillUpdate],
        ["handleMessageLootOpened"]: [eventNames.lootOpened],
        ["handleMessageMarketListingsUpdated"]: [eventNames.marketListingsUpdate],
        ["handleMessageCharacterStatsUpdated"]: [eventNames.equipmentUpdate],
        ["handleMessageActionTypeConsumableSlotsUpdated"]: [eventNames.consumableUpdate],
        ["handleMessageAllCombatTriggersUpdated"]: [eventNames.battleConditionUpdate],
        ["handleMessageCombatTriggersUpdated"]: [eventNames.battleConditionUpdate],
        ["handleMessageLoadoutsUpdated"]: [eventNames.loadoutsUpdate],
        ["handleMessageHouseRoomsUpdated"]: [eventNames.houseRoomsUpdate],
        ["handleMessageCommunityBuffsUpdated"]: [eventNames.actionBuffUpdate],
        ["handleMessageConsumableBuffsUpdated"]: [eventNames.actionBuffUpdate],
        ["handleMessageEquipmentBuffsUpdated"]: [eventNames.actionBuffUpdate],
        ["handleMessageNewBattle"]: [eventNames.newBattle],
        ["handleGoToMarketplace"]: [eventNames.gotoMarket],
        ["handleMessageMarketItemOrderBooksUpdated"]: [eventNames.marketItemUpdate],
    };

    /**
     * 检测对象是否注入
     * @param {string} objName 
     * @returns 
     */
    function checkObjectInjected(objName) {
        if (!mwi_common.injected) {
            warn("not injected");
            return false;
        }
        if (!mwi_common[objName]) {
            warn(objName + " injected failed");
            return false;
        }
        return true;
    }

    /**
     * 监听游戏API调用
     * @param {string} apiName 
     * @param {string} eventBefore 调用前执行事件
     * @param {string} eventAfter 调用后执行事件
     */
    function hookGameAPICall(apiName, ...events) {
        if (!checkObjectInjected("game")) return;
        const api = mwi_common.game[apiName];
        if (!api) {
            error("game api not found: " + apiName);
            return;
        }
        mwi_common.game[apiName] = function(...args) {
            api.apply(this, args);
            events.forEach(event => document.dispatchEvent(new CustomEvent(event, { detail: args })));
        }
    }

    async function loadMarketData() {
        info("loading market data");
        const marketData = await fetch(marketDataUrl);
        if (!marketData.ok) {
            error("failed to load market data");
            return false;
        }

        const marketJson = await marketData.json();
        if (!marketJson || !marketJson.marketData || !marketJson.timestamp) {
            error("failed to parse market data");
            return false;
        }

        mwi_common.marketData = marketJson.marketData;
        mwi_common.marketDataInvalidTime = marketJson.timestamp + 6 * 60 * 60;
        localStorage.setItem(marketDataStorageKey, JSON.stringify(mwi_common.marketData));
        localStorage.setItem(marketDataInvalidTimeStorageKey, mwi_common.marketDataInvalidTime);

        info("market data loaded");
        return true;
    }

    async function loadMarketDataYesterday() {
        info("loading market data yesterday");
        const marketDataYesterday = await fetch(marketDataYesterdayUrl);
        if (!marketDataYesterday.ok) {
            error("failed to load market data yesterday");
            return false;
        }

        const marketJsonYesterday = await marketDataYesterday.json();
        if (!marketJsonYesterday || !marketJsonYesterday.marketData) {
            error("failed to parse market data yesterday");
            return false;
        }

        mwi_common.marketDataYesterday = marketJsonYesterday.marketData;
        info("market data yesterday loaded");
        return true;
    }

    async function updateMarketData() {
        // 重试中取消更新
        if (marketDataRetryTimeoutId) {
            return;
        }

        const currentSec = Date.now() / 1000;
        if (mwi_common.marketDataInvalidTime && currentSec < mwi_common.marketDataInvalidTime) return;

        // 防止同时间多次请求
        mwi_common.marketDataInvalidTime = currentSec + 5 * 60;

        // 昨日数据不是特别重要 拉取今日数据时顺带加载
        loadMarketDataYesterday();

        info("updating market data");
        if (!await loadMarketData()) {
            error("failed to update market data");
            return;
        }

        if (!mwi_common.marketDataLoaded) {
            info("market data loaded");
            mwi_common.marketDataLoaded = true;
            mwi_common.addNotification(mwi_common.isZh? "市场数据已加载": "Market data loaded", false);
            document.dispatchEvent(new CustomEvent(eventNames.marketDataLoaded));
            return;
        }

        // 市场数据更新存在随机性 未更新推迟15分钟
        if (mwi_common.marketDataInvalidTime <= currentSec) {
            info("market data update delayed");
            mwi_common.marketDataInvalidTime = currentSec + 15 * 60;
            localStorage.setItem(marketDataInvalidTimeStorageKey, mwi_common.marketDataInvalidTime);
            return;
        }
        
        mwi_common.addNotification(mwi_common.isZh? "市场数据已更新": "Market data updated", false);
        document.dispatchEvent(new CustomEvent(eventNames.marketDataUpdate));
    }

    document.addEventListener(eventNames.marketItemUpdate, (evt) => {
        if (!mwi_common.marketDataLoaded || !mwi_common.marketData) {
            return;
        }
        
        if (!evt || !evt.detail || !evt.detail[0]) {
            return;
        }

        const marketItemOrderBooks = evt.detail[0].marketItemOrderBooks;
        if (!marketItemOrderBooks) {
            return;
        }

        const itemHrid = marketItemOrderBooks.itemHrid;
        const orderBooks = marketItemOrderBooks.orderBooks;
        if (!itemHrid || itemHrid === "" || !orderBooks || !orderBooks.length) {
            return;
        }

        let itemMarketData = mwi_common.marketData[itemHrid];
        if (!itemMarketData) {
            itemMarketData = {};
            mwi_common.marketData[itemHrid] = itemMarketData;
        }

        orderBooks.forEach((orderBook, level) => {
            let itemLevelMarketData = itemMarketData[level];
            if (!itemLevelMarketData) {
                itemLevelMarketData = { a: -1, b: -1 };
                itemMarketData[level] = itemLevelMarketData;
            }
            orderBook.asks && orderBook.asks[0] && (itemLevelMarketData.a = orderBook.asks[0].price);
            orderBook.bids && orderBook.bids[0] && (itemLevelMarketData.b = orderBook.bids[0].price);
        });

        localStorage.setItem(marketDataStorageKey, JSON.stringify(mwi_common.marketData));
        document.dispatchEvent(new CustomEvent(eventNames.marketDataUpdate));
    });

    /**************************************** Public ****************************************/

    if (localStorage.getItem("i18nextLng") && localStorage.getItem("i18nextLng").startsWith("zh")) {
        mwi_common.isZh = true;
    }

    /**
     * 调用游戏API
     * @param {string} apiName 
     * @param  {...any} args 
     * @returns 
     */
    mwi_common.handleGameAPI = function(apiName, ...args) {
        if (!checkObjectInjected("game")) return;
        if (!mwi_common.game[apiName]) {
            error("game api not found: " + apiName);
            return;
        }
        mwi_common.game[apiName](...args);
    }

    /**
     * 添加通知
     * @param {string} msg 消息
     * @param {boolean} isError 是否错误提示
     */
    mwi_common.addNotification = (msg, isError) => mwi_common.handleGameAPI("updateNotifications", isError? "error": "info", msg);

    /**
     * 打开物品字典
     * @param {string} itemHrid 物品ID
     */
    mwi_common.openItemDictionary = (itemHrid) => mwi_common.handleGameAPI("handleOpenItemDictionary", itemHrid);

    /**
     * 打开物品提示
     * @param {string} itemHrid 物品ID
     * @param {number} itemLevel 物品等级
     * @param {number} itemCount 物品数量
     * @param {number} left
     * @param {number} top
     * @returns div-node
     */
    mwi_common.openItemToolTip = function(itemHrid, itemLevel, itemCount, left, top) {
        const div = document.createElement("div");
        div.role = "tooltip";
        div.setAttribute("id", `mwiCommonItemToolTip${itemHrid}`);
        div.setAttribute("class", "MuiPopper-root MuiTooltip-popper css-112l0a2");
        div.style.position = "absolute";
        div.style.inset = "0px auto auto 0px";
        div.style.margin = "0px";
        div.style.transform = `translate3d(${left}px, ${top}px, 0px)`;
        div.setAttribute("data-popper-placement", "top");
        div.innerHTML = `<div class="MuiTooltip-tooltip MuiTooltip-tooltipPlacementTop css-1spb1s5" style="opacity: 1; transition: opacity cubic-bezier(0.4, 0, 0.2, 1);"
            <div class="ItemTooltipText_itemTooltipText__zFq3A">
                <div class="ItemTooltipText_name__2JAHA">
                    <span>${mwi_common.getItemNameByHrid(itemHrid, mwi_common.isZh)}</span>
                </div>
                <div>
                    <span>${mwi_common.isZh? "数量": "Count"}: ${itemCount}</span>
                </div><div></div>
                <div class="ItemTooltipText_consumableDetail__2_42s"></div>
            </div>
        </div>`;
        document.body.appendChild(div);
        return div;
    }

    /**
     * 关闭物品提示
     * @param {string} itemHrid 物品ID
     */
    mwi_common.closeItemToolTip = function(itemHrid) {
        const div = document.getElementById(`mwiCommonItemToolTip${itemHrid}`);
        if (div) div.remove();
    }

    /**
     * 前往市场
     * @param {string} itemHrid 物品ID
     * @param {number} itemLevel 物品等级
     */
    mwi_common.gotoMarket = (itemHrid, itemLevel) => mwi_common.handleGameAPI("handleGoToMarketplace", itemHrid, itemLevel);

    /**
     * 前往行动
     * @param {string} actionHrid 行动ID 
     * @param {number} actionCount 行动次数
     */
    mwi_common.goToAction = (actionHrid, actionCount) => mwi_common.handleGameAPI("handleGoToAction", actionHrid, actionCount);

    /**
     * 前往打怪
     * @param {string} monsterHrid 怪物ID
     * @param {number} actionCount 行动次数
     */
    mwi_common.goToMonster = (monsterHrid, actionCount) => mwi_common.handleGameAPI("handleGoToMonster", monsterHrid, actionCount);

    /**
     * 通过物品ID获取名称
     * @param {string} itemHrid 物品ID
     * @param {boolean} isZh 是否中文
     * @returns 物品名称
     */
    mwi_common.getItemNameByHrid = function(itemHrid, isZh) {
        if (!mwi_common.lang) return "";
        return (isZh? mwi_common.lang.zh: mwi_common.lang.en).itemNames[itemHrid];
    }

    /**
     * 通过名称获取物品ID
     * @param {string} itemName 物品名称
     * @returns 物品ID
     */
    mwi_common.getItemHridByName = function(itemName) {
        if (mwi_common.itemNameToHrid) return mwi_common.itemNameToHrid[itemName];
        if (!mwi_common.lang) return "";

        const itemNameToHrid = mwi_common.itemNameToHrid = {};
        const enItemNames = mwi_common.lang.en.itemNames;
        const zhItemNames = mwi_common.lang.zh.itemNames;
        for (const itemHrid in enItemNames) itemNameToHrid[enItemNames[itemHrid]] = itemHrid;
        for (const itemHrid in zhItemNames) itemNameToHrid[zhItemNames[itemHrid]] = itemHrid;
        return itemNameToHrid[itemName];
    }

    /**
     * 通过名称获取行动ID
     * @param {string} actionName 行动名称
     * @param {boolean} isZh 是否中文
     * @returns 行动ID
     */
    mwi_common.getActionHridByName = function(actionName, isZh) {
        if (!mwi_common.lang) return "";

        const autionNames = (isZh? mwi_common.lang.zh: mwi_common.lang.en).actionNames;
        for (const actionHrid in autionNames) if (autionNames[actionHrid] === actionName) return actionHrid;

        error("action name not found: " + actionName);
        return "";
    }

    /**
     * 通过名称获取怪物ID
     * @param {string} monsterName 怪物名称
     * @param {boolean} isZh 是否中文
     * @returns 怪物ID
     */
    mwi_common.getMonsterHridByName = function(monsterName, isZh) {
        if (!mwi_common.lang) return "";

        const autionNames = (isZh? mwi_common.lang.zh: mwi_common.lang.en).monsterNames;
        for (const monsterHrid in autionNames) if (autionNames[monsterHrid] === monsterName) return monsterHrid;

        error("monster name not found: " + monsterName);
        return "";
    }

    /**
     * 获取角色ID
     * @returns 角色ID
     */
    mwi_common.getCharacterId = function() {
        if (!checkObjectInjected("game")) return 0;
        if (!mwi_common.game.state || !mwi_common.game.state.character) {
            error("character not found");
            return 0;
        }
        return mwi_common.game.state.character.id;
    }

    /**
     * 通过位置ID获取装备ID
     * @param {string} locationHrid 装备位置ID "/item_locations/body"
     * @returns 装备信息 获取失败返回null
     */
    mwi_common.getEquipmentByLocationHrid = function(locationHrid) {
        if (!checkObjectInjected("game")) return null;
        if (!mwi_common.game.state || !mwi_common.game.state.characterItemByLocationMap) {
            error("characterItemByLocationMap not found");
            return null;
        }
        return mwi_common.game.state.characterItemByLocationMap.get(locationHrid);
    }

    /**
     * 通过物品ID获取物品数量
     * @param {string} itemHrid 物品ID
     * @param {number} itemLevel 物品等级
     * @returns 物品数量
     */
    mwi_common.getItemNumByHrid = function(itemHrid, itemLevel) {
        if (!checkObjectInjected("game")) return 0;
        if (!mwi_common.game.state || !mwi_common.game.state.characterItemMap) {
            error("characterItemMap not found");
            return 0;
        }
        const key = `${mwi_common.getCharacterId()}::/item_locations/inventory::${itemHrid}::${itemLevel}`;
        const item = mwi_common.game.state.characterItemMap.get(key);
        return item? item.count: 0;
    }

    /**
     * 获取物品价格
     * @param {string} itemHrid 
     * @param {"ask" | "bid"} type
     * @returns 查询失败返回-1
     */
    mwi_common.getItemPriceByHrid = function(itemHrid, itemLevel, type) {
        // 调用时自动更新
        updateMarketData();

        if (!mwi_common.marketDataLoaded || !mwi_common.marketData) {
            warn("market data not loaded");
            return -1;
        }
        
        const itemPrices = mwi_common.marketData[itemHrid];
        if (itemPrices === undefined) {
            warn("item not found in market data: " + itemHrid);
            return -1;
        }

        const targetLevelPrice = itemPrices[itemLevel];
        if (targetLevelPrice === undefined) return -1;

        if (type === "ask") {
            if (targetLevelPrice.a === undefined) return -1;
            return targetLevelPrice.a;
        }

        if (type === "bid") {
            if (targetLevelPrice.b === undefined) return -1;
            return targetLevelPrice.b;
        }

        warn("invalid type: " + type);
        return -1;
    }

    mwi_common.getItemPriceYesterdayByHrid = function(itemHrid, itemLevel, type) {
        if (!mwi_common.marketDataYesterday) {
            warn("market data yesterday not loaded");
            return -1;
        }
        
        const itemPrices = mwi_common.marketDataYesterday[itemHrid];
        if (itemPrices === undefined) {
            warn("item not found in market data yesterday: " + itemHrid);
            return -1;
        }

        const targetLevelPrice = itemPrices[itemLevel];
        if (targetLevelPrice === undefined) return -1;

        if (type === "ask") {
            if (targetLevelPrice.a === undefined) return -1;
            return targetLevelPrice.a;
        }

        if (type === "bid") {
            if (targetLevelPrice.b === undefined) return -1;
            return targetLevelPrice.b;
        }

        warn("invalid type: " + type);
        return -1;
    }

    /**************************************** MutationObserver注入 ****************************************/

    const mooketSpace = "mwi";
    const mooketInjectedEventName = "MWICoreInitialized";

    function onInjected() {
        info("injected");
        mwi_common.injected = true;

        info("hooking game api");
        for (let key in gameAPICallEvents) hookGameAPICall(key, ...gameAPICallEvents[key]);
        
        info("dispatch injected event");
        mwi_common.addNotification(mwi_common.isZh? "已注入": "Injected", false);
        document.dispatchEvent(new CustomEvent(eventNames.injected));

        // 市场数据先加载完成补提示
        mwi_common.marketDataLoaded && mwi_common.addNotification(mwi_common.isZh? "市场数据已加载": "Market data loaded", false);
    }

    function initWithMooket() {
        info("init with mooket");
        mwi_common.game = window[mooketSpace].game;
        onInjected();
    }

    async function injectedScriptNode(node) {
        info("patching script: " + node.src);

        try {
            // 移除原始脚本
            const scriptUrl = node.src;
            const nodeParent = node.parentNode;
            node.remove();
            // 注入脚本文件
            const response = await fetch(scriptUrl);
            if (!response.ok) throw new Error(response.status);
            let sourceCode = await response.text();
            const injectionPoints = [
                {
                    name: "game",
                    pattern: "this.sendPing=",
                    replacement: `window.${selfSpace}.game=this,this.sendPing=`,
                }
            ];
            injectionPoints.forEach(injectionPoint => {
                if (sourceCode.includes(injectionPoint.pattern)) {
                    sourceCode = sourceCode.replace(injectionPoint.pattern, injectionPoint.replacement);
                    info(`${injectionPoint.name} injected successfully`);
                }
                else {
                    error(`${injectionPoint.name} injection failed`);
                }
            });
            // 替换脚本
            const newNode = document.createElement('script');
            newNode.textContent = sourceCode;
            document.body.appendChild(newNode);
            info("script patched successfully");
            onInjected();
        } catch (error) {
            error("patching failed: " + error);
        }
    }

    new MutationObserver(async (mutationsList, obs) => {
        // 兼容mooket插件
        if (window[mooketSpace]) {
            info("mooket detected");
            if (window[mooketSpace].MWICoreInitialized) {
                info("mooket core initialized");
                initWithMooket();
            }
            else {
                info("waiting for mooket core");
                window.addEventListener(mooketInjectedEventName, initWithMooket);
            }
            obs.disconnect();
            return;
        }
        for (let idx = mutationsList.length - 1; ~idx; --idx) {
            const mutationRecord = mutationsList[idx];
            for (let idx2 = mutationRecord.addedNodes.length - 1; ~idx2; --idx2) {
                const node = mutationRecord.addedNodes[idx2];
                if (node.tagName === "SCRIPT" && node.src && node.src.search(/.*main\..*\.chunk.js/) === 0) {
                    obs.disconnect();
                    await injectedScriptNode(node);
                    idx = 0;
                    break;
                }
            }
        }
    }).observe(document, { childList: true, subtree: true });
    
    /**************************************** 初始化lang ****************************************/

    async function initLangData(retryCount = 0) {
        info(retryCount? `retry to init lang data ${retryCount}`: "init lang data");
        
        const langData = await fetch(langDataUrl);
        if (!langData.ok) {
            error("failed to load lang data");
            if (retryCount <= 3) {
                setTimeout(initLangData, 3000, ++retryCount);
            }
            return;
        }

        const langJson = await langData.json();
        if (!langJson) {
            error("failed to parse lang data");
            return;
        }
        
        info("lang data initialized");
        mwi_common.lang = langJson;
        mwi_common.addNotification(mwi_common.isZh? "Lang数据已加载": "Lang data loaded", false);
        document.dispatchEvent(new CustomEvent(eventNames.langDataLoaded));
    }

    initLangData();
    
    /**************************************** 初始化官方市场数据 ****************************************/

    async function requestMarketData(retryCount = 0) {
        retryCount && info(`retry to request market data ${retryCount}`);
        marketDataRetryTimeoutId = 0;

        if (!await loadMarketData()) {
            error("failed to init market data");
            if (retryCount <= 3) {
                marketDataRetryTimeoutId = setTimeout(initMarketData, 3000, ++retryCount);
            }
            return;
        }

        info("market data initialized");
        mwi_common.marketDataLoaded = true;
        mwi_common.addNotification(mwi_common.isZh? "市场数据已加载": "Market data loaded", false);
        document.dispatchEvent(new CustomEvent(eventNames.marketDataLoaded));
    }

    async function initMarketData() {
        try {
            info("init market data");

            const storageInvalidTime = localStorage.getItem(marketDataInvalidTimeStorageKey);
            const marketDataInvalidTime = storageInvalidTime? parseInt(storageInvalidTime): 0;
            if (!marketDataInvalidTime || Date.now() / 1000 >= marketDataInvalidTime) {
                requestMarketData();
                return;
            }

            const storageMarketData = localStorage.getItem(marketDataStorageKey);
            const marketData = storageMarketData? JSON.parse(storageMarketData): null;
            if (!marketData) {
                requestMarketData();
                return;
            }

            mwi_common.marketData = marketData;
            mwi_common.marketDataInvalidTime = marketDataInvalidTime;
            mwi_common.marketDataLoaded = true;
            mwi_common.addNotification(mwi_common.isZh? "市场数据已加载": "Market data loaded", false);
            document.dispatchEvent(new CustomEvent(eventNames.marketDataLoaded));
        }
        catch (err) {
            error("failed to init market data: " + err);
            requestMarketData();
        }
    }

    initMarketData();
    
    /**************************************** 初始化昨日市场数据 ****************************************/

    async function initMarketDataYesterday(retryCount = 0) {
        info(retryCount? `retry to init market data yesterday ${retryCount}`: "init market data yesterday");

        if (!await loadMarketDataYesterday()) {
            error("failed to init market data yesterday");
            if (retryCount <= 3) {
                setTimeout(initMarketDataYesterday, 3000, ++retryCount);
            }
            return;
        }

        info("market data yesterday initialized");
    }

    initMarketDataYesterday();

    info("initialized");
})();