Ranged Way Idle

一些超级有用的MWI的QoL功能

Fra og med 19.10.2025. Se den nyeste version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Ranged Way Idle
// @namespace    http://tampermonkey.net/
// @version      4.5
// @description  一些超级有用的MWI的QoL功能
// @author       AlphB
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        GM_notification
// @grant        GM.xmlHttpRequest
// @connect      https://www.milkywayidle.com/*
// @connect      https://test.milkywayidle.com/*
// @icon         https://tupian.li/images/2025/09/30/68dae3cf1fa7e.png
// @license      CC-BY-NC-SA-4.0
// ==/UserScript==

(function () {
    const configs = {
        // combat
        notifyCombatDeath: {
            type: "switch",
            value: true,
            trigger: ["ws", "init"],
            listenMessageTypes: ["new_battle", "battle_updated"]
        },
        minimumNotifyCooldownSeconds: {type: "input_number", value: 5, trigger: [],},

        // message
        notifyChatMessages: {
            type: "switch",
            value: true,
            trigger: ["ws", "ob", "init"],
            listenMessageTypes: ["chat_message_received"]
        },
        notifyChatMessagesVolume: {type: "input_range", value: 0.5, trigger: [], min: 0, max: 1, step: 0.01},
        notifyChatMessagesByRegex: {type: "switch", value: false, trigger: []},
        notifyChatMessagesFilterSelf: {type: "switch", value: true, trigger: []},

        // info
        initCharacterData: {
            type: "switch",
            value: true,
            trigger: ["ws"],
            listenMessageTypes: ["init_character_data"],
            isHidden: true
        },
        updateLocalStorageMarketPrice: {
            type: "switch",
            value: true,
            trigger: ["ws"],
            listenMessageTypes: ["market_item_order_books_updated"]
        },
        showTaskValue: {
            type: "switch",
            value: true,
            trigger: ["ws", "ob", "init"],
            listenMessageTypes: ["quests_updated"]
        },
        trackLeaderBoardData: {type: "switch", value: true, trigger: ["ob"]},

        // UI
        autoClickTaskSortButton: {type: "switch", value: true, trigger: ["ob"]},
        showMarketAPIUpdateTime: {type: "switch", value: true, trigger: ["ob"]},
        forceUpdateAPIButton: {type: "switch", value: true, trigger: ["ob"]},
        disableQueueUpgradeButton: {type: "switch", value: false, trigger: ["ob"]},
        disableActionQueueBar: {type: "switch", value: false, trigger: ["ob"]},

        // listing
        hookListingInfo: {
            type: "switch",
            value: true,
            trigger: ["ws"],
            listenMessageTypes: ["market_listings_updated", "init_character_data"],
            isHidden: true
        },
        saveListingInfoToLocalStorage: {type: "switch", value: true, trigger: []},
        saveListingInfoToLocalStorageMaxDays: {type: "input_number", value: 30, trigger: []},
        showTotalListingFunds: {
            type: "switch",
            value: true,
            trigger: ["ws", "ob"],
            listenMessageTypes: ["market_listings_updated"]
        },
        showTotalListingFundsPrecise: {type: "input_number", value: 0, trigger: []},
        showListingInfo: {
            type: "switch",
            value: true,
            trigger: ["ws", "ob"],
            listenMessageTypes: ["market_listings_updated", "market_item_order_books_updated"]
        },
        showListingPricePrecise: {type: "input_number", value: 2, trigger: []},
        showListingCreateTimeByLifespan: {type: "switch", value: false, trigger: []},
        listingSortTools: {type: "switch", value: false, isHidden: true, trigger: ["ob"]}, // TO DO
        notifyListingFilled: {
            type: "switch",
            value: false,
            trigger: ["ws"],
            listenMessageTypes: ["market_listings_updated"]
        },
        notifyListingFilledVolume: {type: "input_range", value: 0.5, trigger: [], min: 0, max: 1, step: 0.01},
        estimateListingCreateTime: {
            type: "switch",
            value: true,
            trigger: ["ws", "ob"],
            listenMessageTypes: ["market_item_order_books_updated"]
        },
        estimateListingCreateTimeColorByAccuracy: {type: "switch", value: false, trigger: []},
        estimateListingCreateTimeColorByLifespan: {type: "switch", value: false, trigger: []},

        // other
        mournForMagicWayIdle: {type: "switch", value: true, trigger: ["init"]},
        optimizeDocumentObserver: {type: "switch", value: false, trigger: []},
        debugPrintWSMessages: {type: "switch", value: false, trigger: [], listenMessageTypes: []},
        showConfigMenu: {type: "switch", value: true, trigger: ["ob"], isHidden: true},
        // testConfig: {type: "switch", value: true, trigger: [], isHidden: true, isSecret: false},
    }
    const globalVariables = {
        marketAPIUrl: document.URL.includes("test.milkywayidle.com") ?
            "https://test.milkywayidle.com/game_data/marketplace.json" :
            "https://www.milkywayidle.com/game_data/marketplace.json",
        initCharacterData: null,
        documentObserver: null,
        documentObserverFunction: null,
        webSocketMessageProcessor: null,
        functionMap: {},
        language: "zh-cn",
        notifyMessageAudio: new Audio("https://upload.thbwiki.cc/d/d1/se_bonus2.mp3"),
        notifyListingFilledAudio: new Audio("https://upload.thbwiki.cc/f/ff/se_trophy.mp3"),
        allListings: {}
    };
    unsafeWindow._rwivb = globalVariables;

    const I18NMap = {
        "ranged_way_idle_config_menu_title": {"zh-cn": "设置"},
        "notifyCombatDeath": {"zh-cn": "战斗中角色死亡时,发出通知"},
        "minimumNotifyCooldownSeconds": {"zh-cn": "角色死亡通知冷却时间(秒)"},
        "notifyChatMessages": {"zh-cn": "聊天消息含有关键词时,发出声音提醒"},
        "notifyChatMessagesVolume": {"zh-cn": "聊天消息声音提醒音量"},
        "notifyChatMessagesByRegex": {"zh-cn": "聊天消息采用正则匹配"},
        "notifyChatMessagesFilterSelf": {"zh-cn": "不提醒自己发送的聊天消息"},
        "updateLocalStorageMarketPrice": {"zh-cn": "更新localStorage中的市场价格"},
        "showTaskValue": {"zh-cn": "显示任务期望收益(依赖 食用工具)"},
        "trackLeaderBoardData": {"zh-cn": "跟踪排行榜数据"},
        "autoClickTaskSortButton": {"zh-cn": "自动点击任务排序按钮(依赖 MWI TaskManager)"},
        "showMarketAPIUpdateTime": {"zh-cn": "显示市场API更新时间"},
        "forceUpdateAPIButton": {"zh-cn": "强制更新市场API按钮"},
        "disableQueueUpgradeButton": {"zh-cn": "禁用各处队列升级按钮,以防跳转至牛铃商店"},
        "disableActionQueueBar": {"zh-cn": "禁用行动队列提示框显示"},
        "saveListingInfoToLocalStorage": {"zh-cn": "保存挂单信息到localStorage"},
        "saveListingInfoToLocalStorageMaxDays": {"zh-cn": "挂单信息本地保存时间(天)"},
        "showTotalListingFunds": {"zh-cn": "显示市场挂单的总购买预付金/出售可获金/待领取金额"},
        "showTotalListingFundsPrecise": {"zh-cn": "显示市场挂单的总购买预付金/出售可获金/待领取金额的精度"},
        "showListingInfo": {"zh-cn": "显示各个挂单的价格、创建时间信息"},
        "showListingPricePrecise": {"zh-cn": "各个挂单的购买预付金/出售可获金的价格精度"},
        "showListingCreateTimeByLifespan": {"zh-cn": "显示挂单已存在时长,而非创建的时刻"},
        "notifyListingFilled": {"zh-cn": "挂单完成时,发出声音提醒"},
        "notifyListingFilledVolume": {"zh-cn": "挂单完成声音提醒音量"},
        "estimateListingCreateTime": {"zh-cn": "依据挂单ID线性估算挂单创建时间"},
        "estimateListingCreateTimeColorByAccuracy": {"zh-cn": "依据精度为挂单创建时间着色(越偏向绿色 精度越高)该项为真时,覆盖下一选项设置"},
        "estimateListingCreateTimeColorByLifespan": {"zh-cn": "依据存在时间为挂单创建时间着色(越偏向绿色 创建时间越短)"},
        "mournForMagicWayIdle": {"zh-cn": "在控制台为Magic Way Idle默哀"},
        "optimizeDocumentObserver": {"zh-cn": "优化document监听器,减少性能开销(可能有bug,出现问题请关闭)"},
        "debugPrintWSMessages": {"zh-cn": "打印WebSocket消息(不推荐打开)"},

        "configNoteText": {"zh-cn": "部分设置可能需要刷新页面才能生效。如果完全无效,或者控制台大量报错,请尝试更新本插件或前置插件"},
        "notifyChatMessagesAddRowButton": {"zh-cn": "添加聊天消息监听关键词"},
        "taskExpectedValueText": {"zh-cn": "任务期望收益:"},
        "trackLeaderBoardDataLeaderboardStoreButton": {"zh-cn": "记录当前排行榜数据"},
        "trackLeaderBoardDataLeaderboardDeleteButton": {"zh-cn": "删除本地数据"},
        "trackLeaderBoardDataLeaderboardRecordTimeText": {"zh-cn": "本地数据记录于:${recordTime}(${timeDelta}小时前)"},
        "trackLeaderBoardDataLeaderboardNoRecordTimeText": {"zh-cn": "无本地数据记录"},
        "trackLeaderBoardDataNoteText": {"zh-cn": "由于排行榜数据每20分钟记录一次,增速和超越时间有误差,仅供参考。"},
        "trackLeaderBoardDataDifference": {"zh-cn": "增量"},
        "trackLeaderBoardDataSpeed": {"zh-cn": "增速"},
        "trackLeaderBoardDataCatchupTime": {"zh-cn": "超越时间"},
        "trackLeaderBoardDataCatchupTimeNow": {"zh-cn": "现在!"},
        "trackLeaderBoardDataNewRecordText": {"zh-cn": "新上榜"},
        "showMarketAPIUpdateTimeText": {"zh-cn": "市场API更新时间于:"},
        "forceUpdateAPIButtonText": {"zh-cn": "强制更新市场API"},
        "forceUpdateAPIButtonTextSuccess": {"zh-cn": "更新成功。市场数据更新于"},
        "forceUpdateAPIButtonTextError": {"zh-cn": "更新失败。请稍后重试。"},
        "forceUpdateAPIButtonTextTimeout": {"zh-cn": "更新超时。请稍后重试。"},
        "totalUnclaimedCoinsText": {"zh-cn": "待领取金额"},
        "totalPrepaidCoinsText": {"zh-cn": "购买预付金"},
        "totalSellResultCoinsText": {"zh-cn": "出售可获金"},
        "showListingInfoCreateTimeAt": {"zh-cn": "创建于"},
        "showListingInfoCreateTimeLifespan": {"zh-cn": "已存在 ${days}天${hours}时${minutes}分${seconds}秒"},
        "showListingInfoTopOrderPriceText": {"zh-cn": "左一/右一 价格"},
        "showListingInfoTotalPriceText": {"zh-cn": "购买预付金/出售可获金"},
        "estimateListingCreateTimeText": {"zh-cn": "估计创建时间"},

        "/chat_channel_types/general": {"zh-cn": "英语"},
        "/chat_channel_types/chinese": {"zh-cn": "中文"},
        "/chat_channel_types/ironcow": {"zh-cn": "铁牛"},
        "/chat_channel_types/trade": {"zh-cn": "交易"},
        "/chat_channel_types/recruit": {"zh-cn": "招募"},
        "/chat_channel_types/beginner": {"zh-cn": "新手"},
        "/chat_channel_types/guild": {"zh-cn": "公会"},
        "/chat_channel_types/party": {"zh-cn": "队伍"},
        "/chat_channel_types/whisper": {"zh-cn": "私聊"},
        "/chat_channel_types/moderator": {"zh-cn": "管理员"},

        "/chat_channel_types/arabic": {"zh-cn": "العربية"},
        "/chat_channel_types/french": {"zh-cn": "Français"},
        "/chat_channel_types/german": {"zh-cn": "Deutsch"},
        "/chat_channel_types/hebrew": {"zh-cn": "עברית"},
        "/chat_channel_types/hindi": {"zh-cn": "हिंदी"},
        "/chat_channel_types/japanese": {"zh-cn": "日本語"},
        "/chat_channel_types/korean": {"zh-cn": "한국어"},
        "/chat_channel_types/portuguese": {"zh-cn": "Português"},
        "/chat_channel_types/russian": {"zh-cn": "Русский"},
        "/chat_channel_types/spanish": {"zh-cn": "Español"},
        "/chat_channel_types/vietnamese": {"zh-cn": "Tiếng Việt"},

    };

    function initScript() {
        const allFunctionsObject = new AllFunctions();
        for (const configName in configs) {
            if (configs[configName].trigger.length === 0) continue;
            if (!allFunctionsObject[configName]) {
                console.warn("No function found for config: " + configName);
                continue;
            }
            globalVariables.functionMap[configName] = allFunctionsObject[configName]();
        }
        globalVariables.functionMap["showConfigMenu"].loadLocalConfig();

        hookWebSocket();
        initDocumentObserver();

        for (const configName in configs) {
            if (configs[configName].type !== 'switch' || !configs[configName].value) continue;
            if (globalVariables.functionMap[configName] && configs[configName].trigger.includes('init')) {
                try {
                    globalVariables.functionMap[configName].init();
                } catch (err) {
                    console.error(err);
                }
            }
        }

        function hookWebSocket() {
            // message processor
            globalVariables.webSocketMessageProcessor = function (message, type) {
                const obj = JSON.parse(message);
                if (configs.debugPrintWSMessages.value) console.log(type, obj);
                if (type !== 'get' || !obj) return;
                const messageType = obj.type;
                for (const configName in configs) {
                    if (configs[configName].type !== 'switch' || !configs[configName].value) continue;
                    if (globalVariables.functionMap[configName] && configs[configName].trigger.includes('ws') &&
                        configs[configName].listenMessageTypes && configs[configName].listenMessageTypes.includes(messageType)) {
                        try {
                            globalVariables.functionMap[configName].ws(obj);
                        } catch (err) {
                            console.error(err);
                        }
                    }
                }
            };

            // get
            const oriGet = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data").get;

            function hookedGet() {
                const socket = this.currentTarget;
                if (!(socket instanceof WebSocket) || !socket.url) {
                    return oriGet.call(this);
                }
                const message = oriGet.call(this);
                try {
                    globalVariables.webSocketMessageProcessor(message, 'get')
                } catch (err) {
                    console.error(err);
                }
                return message;
            }

            Object.defineProperty(MessageEvent.prototype, "data", {
                get: hookedGet,
                configurable: true,
                enumerable: true
            });

            // send
            const originalSend = WebSocket.prototype.send;

            WebSocket.prototype.send = function (message) {
                try {
                    globalVariables.webSocketMessageProcessor(message, 'send');
                } catch (err) {
                    console.error(err);
                }
                return originalSend.call(this, message);
            };
        }

        function initDocumentObserver() {
            globalVariables.documentObserverFunction = function documentObserverFunction(mutationsList, observer) {
                const node = configs.optimizeDocumentObserver.enable ? mutationsList[0].target : document;
                for (const configName in configs) {
                    if (configs[configName].type !== 'switch' || !configs[configName].value) continue;
                    if (globalVariables.functionMap[configName] && configs[configName].trigger.includes('ob')) {
                        try {
                            globalVariables.functionMap[configName].ob(node);
                        } catch (err) {
                            console.error(err);
                        }
                    }
                }
            }
            globalVariables.documentObserver = new MutationObserver(globalVariables.documentObserverFunction);
            globalVariables.documentObserver.observe(document, {childList: true, subtree: true});
        }
    }

    class AllFunctions {
        showConfigMenu() {
            function loadLocalConfig() {
                // delete old version config
                delete localStorage["ranged_way_idle_config"];

                const localConfig = localStorage.getItem("ranged_way_idle_configs");
                const localConfigObject = localConfig ? JSON.parse(localConfig) : {};
                for (const configName in localConfigObject) {
                    if (configs[configName]) {
                        configs[configName].value = localConfigObject[configName];
                    }
                }
            }

            function saveLocalConfig() {
                const localConfig = localStorage.getItem("ranged_way_idle_configs");
                const localConfigObject = localConfig ? JSON.parse(localConfig) : {};
                for (const configName in configs) {
                    localConfigObject[configName] = configs[configName].value;
                }
                localStorage.setItem("ranged_way_idle_configs", JSON.stringify(localConfigObject));
            }

            function setConfig(configName, value) {
                // forbid changing hidden config
                if (configs[configName].isHidden) return;

                configs[configName].value = value;
                saveLocalConfig();
            }

            function ob(node) {
                const settingPanelNode = node.querySelector(".SettingsPanel_profileTab__214Bj");
                if (!settingPanelNode) return;
                if (settingPanelNode.querySelector(".RangedWayIdleConfigMenuRoot")) return;
                const configMenuRootNode = document.createElement("div");
                configMenuRootNode.classList.add("RangedWayIdleConfigMenuRoot");
                configMenuRootNode.style.display = "flex";
                configMenuRootNode.style.flexDirection = "column";

                // head
                const headNode = document.createElement("div");
                const headSpanNode1 = document.createElement("span");
                headSpanNode1.textContent = "Ranged Way Idle";
                headSpanNode1.style.fontSize = "1.5rem";
                headSpanNode1.style.color = "#66CCFF";
                headNode.appendChild(headSpanNode1);
                const headSpanNode2 = document.createElement("span");
                headSpanNode2.textContent = I18N("ranged_way_idle_config_menu_title");
                headSpanNode2.style.fontSize = "1.5rem";
                headNode.appendChild(headSpanNode2);
                configMenuRootNode.appendChild(headNode);

                // note text
                const noteTextNode = document.createElement("div");
                noteTextNode.textContent = I18N("configNoteText");
                configMenuRootNode.appendChild(noteTextNode);

                // if contains secret setting, add additional text
                if (Object.values(configs).some(config => config.isSecret)) {
                    // 没错我就是有隐藏功能不给大伙用,不服你就憋着嘿嘿嘿 ᗜˬᗜ
                    const secretTextNode = document.createElement("div");
                    secretTextNode.innerHTML = `<span style="color:#66CCFF">天依蓝</span>为内部功能,严禁外传!截图也不行!`;
                    configMenuRootNode.appendChild(secretTextNode);
                }

                // body
                for (const configName in configs) {
                    if (configs[configName].isHidden) continue;
                    const divNode = document.createElement("div");
                    divNode.style.display = "flex";
                    divNode.style.alignItems = "center";
                    if (configs[configName].type === "switch") {
                        const inputNode = document.createElement("input");
                        inputNode.type = "checkbox";
                        inputNode.checked = configs[configName].value;
                        inputNode.addEventListener("change", () => {
                            setConfig(configName, inputNode.checked);
                        });
                        inputNode.id = configName;
                        divNode.appendChild(inputNode);

                        const textNode = document.createElement("span");
                        textNode.textContent = I18N(configName);
                        if (configs[configName].isSecret) {
                            textNode.style.color = "#66CCFF";
                        }
                        divNode.appendChild(textNode);

                    } else if (configs[configName].type === "input_number") {
                        const textNode = document.createElement("span");
                        textNode.textContent = I18N(configName);
                        if (configs[configName].isSecret) {
                            textNode.style.color = "#66CCFF";
                        }
                        divNode.appendChild(textNode);

                        const inputNode = document.createElement("input");
                        inputNode.type = "number";
                        inputNode.value = configs[configName].value;
                        inputNode.addEventListener("change", () => {
                            setConfig(configName, Number(inputNode.value));
                        });
                        inputNode.id = configName;
                        inputNode.style.width = "5rem";
                        divNode.appendChild(inputNode);
                    } else if (configs[configName].type === "input_range") {
                        const textNode = document.createElement("span");
                        textNode.textContent = I18N(configName);
                        if (configs[configName].isSecret) {
                            textNode.style.color = "#66CCFF";
                        }
                        divNode.appendChild(textNode);

                        const inputNode = document.createElement("input");
                        inputNode.type = "range";
                        inputNode.min = configs[configName].min;
                        inputNode.max = configs[configName].max;
                        inputNode.step = configs[configName].step;
                        inputNode.value = configs[configName].value;
                        inputNode.addEventListener("change", () => {
                            setConfig(configName, Number(inputNode.value));
                        });
                        inputNode.id = configName;
                        inputNode.style.width = "10rem";
                        divNode.appendChild(inputNode);
                    }
                    configMenuRootNode.appendChild(divNode);
                }

                // add to panel
                settingPanelNode.appendChild(configMenuRootNode);
            }


            return {loadLocalConfig: loadLocalConfig, ob: ob};
        }

        notifyCombatDeath() {
            const players = [];
            let lastNotificationTime = 0;

            function newBattle(obj) {
                players.length = 0;
                for (const player of obj.players) {
                    players.push({
                        name: player.name,
                        isAlive: player.currentHitpoints > 0
                    });
                    if (player.currentHitpoints === 0) {
                        new Notification('战斗提醒', {body: `${player.name} 死了!`});
                    }
                }
            }

            function battleUpdated(obj) {
                for (const playerIndex in obj.pMap) {
                    const player = players[playerIndex];
                    if (player.isAlive && obj.pMap[playerIndex].cHP === 0 &&
                        Date.now() - lastNotificationTime > 1000 * configs.minimumNotifyCooldownSeconds.value) {
                        new Notification('战斗提醒', {body: `${player.name} 死了!`});
                        lastNotificationTime = Date.now();
                    }
                    player.isAlive = obj.pMap[playerIndex].cHP > 0;
                }
            }

            function ws(obj) {
                if (obj.type === "new_battle") {
                    newBattle(obj);
                } else if (obj.type === "battle_updated") {
                    battleUpdated(obj);
                }
            }

            function init() {
                Notification.requestPermission();
            }

            return {ws: ws, init: init};
        }

        notifyChatMessages() {
            const allChannels = [
                "/chat_channel_types/chinese",
                "/chat_channel_types/general",
                "/chat_channel_types/ironcow",
                "/chat_channel_types/trade",
                "/chat_channel_types/recruit",
                "/chat_channel_types/beginner",
                "/chat_channel_types/guild",
                "/chat_channel_types/party",
                "/chat_channel_types/whisper",
                "/chat_channel_types/moderator",

                "/chat_channel_types/arabic",
                "/chat_channel_types/french",
                "/chat_channel_types/german",
                "/chat_channel_types/hebrew",
                "/chat_channel_types/hindi",
                "/chat_channel_types/japanese",
                "/chat_channel_types/korean",
                "/chat_channel_types/portuguese",
                "/chat_channel_types/russian",
                "/chat_channel_types/spanish",
                "/chat_channel_types/vietnamese",
            ];
            let listenObject = {};
            let messageListerMenuRootNode;

            function createNewRow(selectedChannel = "", inputText = "") {
                const listenRow = document.createElement("div");
                listenRow.classList.add("RangedWayIdleMessageListenRow");

                // channel select
                const selectNode = document.createElement('select');
                allChannels.forEach(channel => {
                    const option = document.createElement('option');
                    option.value = channel;
                    option.textContent = I18N(channel);
                    if (channel === selectedChannel) {
                        option.selected = true;
                    }
                    selectNode.appendChild(option);
                });
                selectNode.addEventListener('change', updateListenObject);

                // input text
                const inputNode = document.createElement('input');
                inputNode.type = 'text';
                inputNode.value = inputText;
                inputNode.addEventListener('input', updateListenObject);

                // delete button
                const deleteButton = document.createElement('button');
                deleteButton.textContent = "×";
                deleteButton.addEventListener('click', function () {
                    listenRow.remove();
                    updateListenObject();
                });
                deleteButton.style.backgroundColor = "#F44444";

                // add to row
                listenRow.appendChild(selectNode);
                listenRow.appendChild(inputNode);
                listenRow.appendChild(deleteButton);

                return listenRow;
            }

            function updateListenObject() {
                const newListenObject = {};
                for (const channel of allChannels) {
                    newListenObject[channel] = [];
                }

                // collect channel and text from rows
                for (const row of messageListerMenuRootNode.querySelectorAll('.RangedWayIdleMessageListenRow')) {
                    const channel = row.querySelector('select').value;
                    const text = row.querySelector('input').value.trim();
                    newListenObject[channel].push(text);
                }

                listenObject = newListenObject;
                localStorage.setItem("ranged_way_idle_listen_chat_messages", JSON.stringify(listenObject));
            }

            function ws(obj) {
                if (obj.type === "chat_message_received") {
                    const channel = obj.message.chan;
                    const text = obj.message.m;
                    if (configs.notifyChatMessagesFilterSelf.value && obj.message.cId === globalVariables.initCharacterData.character.id) return;
                    if (!listenObject[channel]) return;
                    for (const listenText of listenObject[channel]) {
                        if (configs.notifyChatMessagesByRegex.value) {
                            const regex = new RegExp(listenText, "g");
                            if (regex.test(text)) {
                                globalVariables.notifyMessageAudio.volume = configs.notifyChatMessagesVolume.value;
                                globalVariables.notifyMessageAudio.play();
                                break;
                            }
                        } else {
                            if (text.includes(listenText)) {
                                globalVariables.notifyMessageAudio.volume = configs.notifyChatMessagesVolume.value;
                                globalVariables.notifyMessageAudio.play();
                                break;
                            }
                        }
                    }

                }
            }

            function ob(node) {
                // add this after config menu
                const configMenuRootNode = node.querySelector(".RangedWayIdleConfigMenuRoot");
                if (!configMenuRootNode) return;
                if (node.querySelector(".RangedWayIdleMessageListerMenu")) return;
                messageListerMenuRootNode = document.createElement("div");
                messageListerMenuRootNode.classList.add("RangedWayIdleMessageListerMenu");

                // new row button
                const addNewRowButton = document.createElement("button");
                addNewRowButton.textContent = I18N("notifyChatMessagesAddRowButton");
                addNewRowButton.addEventListener("click", () => {
                    messageListerMenuRootNode.appendChild(createNewRow());
                });
                addNewRowButton.style.backgroundColor = "#66CCFF";
                addNewRowButton.style.color = "#000000";
                messageListerMenuRootNode.appendChild(addNewRowButton);

                // load local listeners
                for (const channel of allChannels) {
                    if (listenObject[channel]) {
                        for (const text of listenObject[channel]) {
                            messageListerMenuRootNode.appendChild(createNewRow(channel, text));
                        }
                    }
                }

                configMenuRootNode.insertAdjacentElement("afterend", messageListerMenuRootNode);
            }

            function init() {
                const localListenObject = localStorage.getItem("ranged_way_idle_listen_chat_messages");
                if (localListenObject) {
                    listenObject = JSON.parse(localListenObject);
                }
            }

            return {ws: ws, ob: ob, init: init};
        }

        initCharacterData() {
            function ws(obj) {
                globalVariables.initCharacterData = obj;
            }

            return {ws: ws};
        }

        updateLocalStorageMarketPrice() {
            function ws(obj) {
                if (obj.type === "market_item_order_books_updated") {
                    const localMarketAPIJson = JSON.parse(localStorage.getItem('MWITools_marketAPI_json'));
                    const itemHrid = obj.marketItemOrderBooks.itemHrid;
                    const orderBooks = obj.marketItemOrderBooks.orderBooks;
                    for (let enhanceLevel = 0; enhanceLevel <= 20; enhanceLevel++) {
                        if (orderBooks[enhanceLevel]) {
                            // 如果左右至少有一个挂单,则需要更新为该价格
                            let askValue = -1;
                            const ask = orderBooks[enhanceLevel].asks;
                            if (ask && ask.length) {
                                askValue = Math.min(...ask.map(listing => listing.price));
                            }
                            let bidValue = -1;
                            const bid = orderBooks[enhanceLevel].bids;
                            if (bid && bid.length) {
                                bidValue = Math.max(...bid.map(listing => listing.price));
                            }

                            if (askValue !== -1 || bidValue !== -1) {
                                localMarketAPIJson.marketData[itemHrid][enhanceLevel] = {
                                    a: askValue,
                                    b: bidValue
                                };
                            }
                        } else if (enhanceLevel === 0) {
                            // 左右都没有,强化等级为+0,记录为-1
                            localMarketAPIJson.marketData[itemHrid][enhanceLevel] = {
                                a: -1,
                                b: -1
                            }
                        } else {
                            // 左右都没有,强化等级不为+0,删除记录
                            delete localMarketAPIJson.marketData[itemHrid][enhanceLevel];
                        }
                    }
                    // 将修改后结果写回marketAPI缓存,完成对marketAPI价格的强制修改
                    localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(localMarketAPIJson));
                }
            }

            return {ws: ws};
        }

        showTaskValue() {
            let taskValueObject;

            function getTaskTokenValue() {
                const chestDropData = JSON.parse(localStorage.getItem("Edible_Tools")).Chest_Drop_Data;
                const lootsName = ["大陨石舱", "大工匠匣", "大宝箱"];
                const bidValueList = [
                    parseFloat(chestDropData["Large Meteorite Cache"]["期望产出" + "Bid"]),
                    parseFloat(chestDropData["Large Artisan's Crate"]["期望产出" + "Bid"]),
                    parseFloat(chestDropData["Large Treasure Chest"]["期望产出" + "Bid"]),
                ];
                const askValueList = [
                    parseFloat(chestDropData["Large Meteorite Cache"]["期望产出" + "Ask"]),
                    parseFloat(chestDropData["Large Artisan's Crate"]["期望产出" + "Ask"]),
                    parseFloat(chestDropData["Large Treasure Chest"]["期望产出" + "Ask"]),
                ];
                const res = {
                    bidValue: Math.max(...bidValueList),
                    askValue: Math.max(...askValueList)
                };
                // bid和ask的最佳兑换选项
                res.bidLoots = lootsName[bidValueList.indexOf(res.bidValue)];
                res.askLoots = lootsName[askValueList.indexOf(res.askValue)];
                // bid和ask的任务代币价值
                res.bidValue = Math.round(res.bidValue / 30);
                res.askValue = Math.round(res.askValue / 30);
                // 小紫牛的礼物的额外价值计算
                res.giftValueBid = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出" + "Bid"]));
                res.giftValueAsk = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出" + "Ask"]));

                res.rewardValueBid = res.bidValue + res.giftValueBid / 50;
                res.rewardValueAsk = res.askValue + res.giftValueAsk / 50;
                return res;
            }

            function updateTaskValueNode(node) {
                const taskListNode = node.querySelector(".TasksPanel_taskList__2xh4k");
                if (!taskListNode) return;
                if (taskListNode.querySelector(".RangedWayIdleTaskValue")) return;

                for (const taskNode of taskListNode.querySelectorAll(".RandomTask_taskInfo__1uasf")) {
                    const rewardsNode = taskNode.querySelector(".RandomTask_rewards__YZk7D");
                    let coinCount = 0;
                    let taskTokenCount = 0;
                    for (const itemContainerNode of rewardsNode.querySelectorAll(".Item_itemContainer__x7kH1")) {
                        if (itemContainerNode.querySelector("use").href.baseVal.includes("coin")) {
                            coinCount = parseItemCount(itemContainerNode.querySelector(".Item_count__1HVvv").textContent);
                        } else if (itemContainerNode.querySelector("use").href.baseVal.includes("task_token")) {
                            taskTokenCount = parseItemCount(itemContainerNode.querySelector(".Item_count__1HVvv").textContent);
                        }
                    }

                    const askValue = taskTokenCount * taskValueObject.rewardValueAsk + coinCount;
                    const bidValue = taskTokenCount * taskValueObject.rewardValueBid + coinCount;

                    const taskValueDivNode = document.createElement("div");
                    taskValueDivNode.classList.add("RangedWayIdleTaskValue");
                    taskValueDivNode.textContent = I18N("taskExpectedValueText") + `${formatItemCount(askValue)} / ${formatItemCount(bidValue)}`;
                    taskValueDivNode.style.color = "#66CCFF";
                    taskValueDivNode.style.fontSize = "0.75rem";
                    taskNode.querySelector(".RandomTask_action__3eC6o").appendChild(taskValueDivNode);
                }
            }

            function updateTaskShopItemValue(node) {
                const taskShopPanelNode = node.querySelector(".TasksPanel_taskShop__q5sHL");
                if (!taskShopPanelNode) return;
                if (taskShopPanelNode.classList.contains("RangedWayIdleTaskShopValueSet")) return;
                const chestDropData = JSON.parse(localStorage.getItem("Edible_Tools")).Chest_Drop_Data;
                taskShopPanelNode.classList.add("RangedWayIdleTaskShopValueSet");
                const nameMap = {
                    "large_meteorite_cache": "Large Meteorite Cache",
                    "large_artisans_crate": "Large Artisan's Crate",
                    "large_treasure_chest": "Large Treasure Chest"
                }
                for (const taskShopItemNode of taskShopPanelNode.querySelectorAll(".TasksPanel_item__DWSpv")) {
                    const item = taskShopItemNode.querySelector(".TasksPanel_iconContainer__2JGVN use").href.baseVal.split("#")[1];
                    if (!Object.keys(nameMap).includes(item)) {
                        continue;
                    }
                    const name = nameMap[item];
                    const askValue = parseFloat(chestDropData[name]["期望产出" + "Ask"]);
                    const bidValue = parseFloat(chestDropData[name]["期望产出" + "Bid"]);
                    const divNode = document.createElement("div");
                    divNode.textContent = `${formatItemCount(askValue)} / ${formatItemCount(bidValue)}`;
                    divNode.style.color = "#66CCFF";
                    taskShopItemNode.insertBefore(divNode, taskShopItemNode.lastChild);
                }
            }

            function ws(obj) {
                if (obj.type === "quests_updated") {
                    // remove old task value nodes
                    document.querySelectorAll(".RangedWayIdleTaskValue").forEach(node => {
                        node.remove();
                    });
                }
            }

            function ob(node) {
                // set task expected value
                updateTaskValueNode(node);

                // set task shop item value
                updateTaskShopItemValue(node);
            }

            function init() {
                taskValueObject = getTaskTokenValue();
                if (configs.updateLocalStorageMarketPrice.value) {
                    const localMarketAPIJson = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
                    localMarketAPIJson.marketData["/items/task_token"] = {
                        "0": {
                            a: taskValueObject.askValue,
                            b: taskValueObject.bidValue
                        }
                    };
                    localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(localMarketAPIJson));
                }
            }

            return {ws: ws, ob: ob, init: init};
        }

        trackLeaderBoardData() {
            function getCurrentKey() {
                const selectedTabs = document.querySelectorAll(".LeaderboardPanel_tabsComponentContainer__mIgnw .Mui-selected");
                if (selectedTabs.length === 0) return;
                const selectedText = Array.from(selectedTabs).map((tab) => tab.textContent);
                return selectedText.join("-");
            }

            function createNoteAndButton(noteNode) {
                const keyString = getCurrentKey();

                // store data button
                const storeButton = document.createElement("button");
                storeButton.textContent = I18N("trackLeaderBoardDataLeaderboardStoreButton");
                storeButton.style.backgroundColor = "#66CCFF";
                storeButton.addEventListener("click", function () {
                    // get data
                    const leaderBoardData = {};
                    const tableNode = document.querySelector(".LeaderboardPanel_leaderboardTable__3JLvu");
                    for (const row of tableNode.querySelectorAll("tbody tr")) {
                        const characterNameNode = row.querySelector(".LeaderboardPanel_name__3hpvo").querySelector("span");
                        const guildNameNode = row.querySelector(".LeaderboardPanel_guildName__2RYcC");
                        const name = characterNameNode ? characterNameNode.textContent : guildNameNode.textContent;
                        const valueNode1 = row.querySelector(".LeaderboardPanel_valueColumn1__2HFDb");
                        const valueNode2 = row.querySelector(".LeaderboardPanel_valueColumn2__1ejF2");
                        const value = Number((valueNode2 ? valueNode2.textContent : valueNode1.textContent).replaceAll(",", ""));
                        leaderBoardData[name] = value || 0;
                    }

                    // store data
                    const localData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                    localData[keyString] = {
                        data: leaderBoardData,
                        timestamp: new Date().getTime()
                    };
                    localStorage.setItem("ranged_way_idle_leaderboard_data", JSON.stringify(localData));
                });
                noteNode.appendChild(storeButton);

                // delete data button
                const deleteDataButton = document.createElement("button");
                deleteDataButton.textContent = I18N("trackLeaderBoardDataLeaderboardDeleteButton");
                deleteDataButton.style.backgroundColor = "#F44444";
                deleteDataButton.addEventListener("click", function () {
                    const localData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                    delete localData[keyString];
                    localStorage.setItem("ranged_way_idle_leaderboard_data", JSON.stringify(localData));
                });
                noteNode.appendChild(deleteDataButton);

                // record time text node
                const localData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                const recordTimeTextNode = document.createElement("div");
                if (localData[keyString]) {
                    const recordTime = new Date(localData[keyString].timestamp);
                    const timeDelta = (new Date().getTime() - localData[keyString].timestamp) / 3600000;
                    recordTimeTextNode.textContent = I18N("trackLeaderBoardDataLeaderboardRecordTimeText", {
                        recordTime: recordTime.toLocaleString(),
                        timeDelta: timeDelta.toFixed(2)
                    });
                } else {
                    recordTimeTextNode.textContent = I18N("trackLeaderBoardDataLeaderboardNoRecordTimeText");
                }
                noteNode.appendChild(recordTimeTextNode);

                // hint text node
                const noteTextNode = document.createElement("div");
                noteTextNode.textContent = I18N("trackLeaderBoardDataNoteText");
                noteNode.appendChild(noteTextNode);
            }

            function showDifference(leaderBoardContentNode) {
                const keyString = getCurrentKey();

                const allStoreData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                if (!allStoreData || !allStoreData[keyString]) {
                    return;
                }
                // expand panel
                leaderBoardContentNode.style.maxWidth = '60rem';

                // get current data
                const localData = allStoreData[keyString].data;
                const timeDelta = (new Date().getTime() - allStoreData[keyString].timestamp) / 1000;
                const hourDelta = timeDelta / 3600;

                const tableNode = leaderBoardContentNode.querySelector(".LeaderboardPanel_leaderboardTable__3JLvu");

                // head
                const headNode = tableNode.querySelector("thead").firstChild;
                const diffNode = document.createElement("th");
                diffNode.textContent = I18N("trackLeaderBoardDataDifference");
                headNode.appendChild(diffNode);
                const speedNode = document.createElement("th");
                speedNode.textContent = I18N("trackLeaderBoardDataSpeed");
                headNode.appendChild(speedNode);
                const catchupTimeNode = document.createElement("th");
                catchupTimeNode.textContent = I18N("trackLeaderBoardDataCatchupTime");
                headNode.appendChild(catchupTimeNode);

                // body
                let previousRowValue = null;
                let previousRowSpeed = null;
                let maxSpeedValue = 0.0;
                let personalRow = null;
                let personalName = null;

                // calculate max speed for set color
                for (const row of tableNode.querySelectorAll("tbody tr")) {
                    const characterNameNode = row.querySelector(".LeaderboardPanel_name__3hpvo").querySelector("span");
                    const guildNameNode = row.querySelector(".LeaderboardPanel_guildName__2RYcC");
                    const name = characterNameNode ? characterNameNode.textContent : guildNameNode.textContent;
                    const valueNode1 = row.querySelector(".LeaderboardPanel_valueColumn1__2HFDb");
                    const valueNode2 = row.querySelector(".LeaderboardPanel_valueColumn2__1ejF2");
                    const value = Number((valueNode2 ? valueNode2.textContent : valueNode1.textContent).replaceAll(",", ""));
                    if (localData[name]) {
                        const diffValue = value - localData[name];
                        maxSpeedValue = Math.max(maxSpeedValue, diffValue / hourDelta);
                    }
                    if (row.classList.contains("LeaderboardPanel_personal__DZ7Nr")) {
                        personalRow = row;
                        personalName = name;
                    }
                }

                for (const row of tableNode.querySelectorAll("tbody tr")) {
                    const characterNameNode = row.querySelector(".LeaderboardPanel_name__3hpvo").querySelector("span");
                    const guildNameNode = row.querySelector(".LeaderboardPanel_guildName__2RYcC");
                    const name = characterNameNode ? characterNameNode.textContent : guildNameNode.textContent;
                    const valueNode1 = row.querySelector(".LeaderboardPanel_valueColumn1__2HFDb");
                    const valueNode2 = row.querySelector(".LeaderboardPanel_valueColumn2__1ejF2");
                    const value = Number((valueNode2 ? valueNode2.textContent : valueNode1.textContent).replaceAll(",", ""));

                    const diffValueNode = document.createElement("td");
                    diffValueNode.classList.add("RangedWayIdleLeaderBoardDiffValue");
                    const speedValueNode = document.createElement("td");
                    speedValueNode.classList.add("RangedWayIdleLeaderBoardSpeedValue");
                    const catchupTimeValueNode = document.createElement("td");
                    catchupTimeValueNode.classList.add("RangedWayIdleLeaderBoardCatchupTimeValue");

                    if (localData[name]) {
                        const diffValue = value - localData[name];
                        diffValueNode.textContent = diffValue.toLocaleString();
                        const speedValue = diffValue / hourDelta;
                        speedValueNode.textContent = formatItemCount(speedValue, 2) + "/h";

                        const k1 = Math.log(1 + (Math.E - 1) * speedValue / maxSpeedValue);
                        diffValueNode.style.color = `rgb(${255 - k1 * 255}, ${k1 * 255}, 0)`;
                        speedValueNode.style.color = `rgb(${255 - k1 * 255}, ${k1 * 255}, 0)`;

                        if (previousRowValue === null || previousRowSpeed === null) {
                            catchupTimeValueNode.textContent = "?????";
                            catchupTimeValueNode.style.color = "#66CCFF";
                        } else {
                            const deltaSpeed = speedValue - previousRowSpeed;
                            if (deltaSpeed === 0) {
                                if (previousRowValue === value) {
                                    catchupTimeValueNode.textContent = I18N("trackLeaderBoardDataCatchupTimeNow");
                                    catchupTimeValueNode.style.color = "#00FF00";
                                } else {
                                    catchupTimeValueNode.textContent = "∞";
                                    catchupTimeValueNode.style.color = "#FF0000";
                                }
                            } else {
                                const catchupTimeValue = (previousRowValue - value) / deltaSpeed;
                                if (catchupTimeValue > 0) {
                                    catchupTimeValueNode.textContent = formatItemCount(catchupTimeValue, 2) + "h";
                                    const k2 = 10000 / (10000 + catchupTimeValue * catchupTimeValue);
                                    catchupTimeValueNode.style.color = `rgb(${255 - k2 * 255}, ${k2 * 255}, 0)`;
                                } else if (catchupTimeValue === 0) {
                                    catchupTimeValueNode.textContent = "?????";
                                    catchupTimeValueNode.style.color = "#66CCFF";
                                } else {
                                    catchupTimeValueNode.textContent = "∞";
                                    catchupTimeValueNode.style.color = "#FF0000";
                                }
                            }
                        }
                        previousRowSpeed = speedValue;
                    } else {
                        diffValueNode.textContent = I18N("trackLeaderBoardDataNewRecordText");
                        speedValueNode.textContent = I18N("trackLeaderBoardDataNewRecordText");
                        catchupTimeValueNode.textContent = I18N("trackLeaderBoardDataNewRecordText");
                        diffValueNode.style.color = "#66CCFF";
                        speedValueNode.style.color = "#66CCFF";
                        catchupTimeValueNode.style.color = "#66CCFF";
                        previousRowSpeed = null;
                    }
                    previousRowValue = value;

                    // personal row
                    if (row.classList.contains("LeaderboardPanel_personal__DZ7Nr")) {
                        previousRowValue = null;
                        previousRowSpeed = null;
                    }

                    row.appendChild(diffValueNode);
                    row.appendChild(speedValueNode);
                    row.appendChild(catchupTimeValueNode);

                    if (personalRow && personalName === name) {
                        personalRow.querySelector(".RangedWayIdleLeaderBoardCatchupTimeValue").textContent = catchupTimeValueNode.textContent;
                        personalRow.querySelector(".RangedWayIdleLeaderBoardCatchupTimeValue").style.color = catchupTimeValueNode.style.color;
                    }
                }
            }

            function ob(node) {
                const leaderBoardRootNode = node.querySelector(".LeaderboardPanel_leaderboardPanel__19U0W");
                if (!leaderBoardRootNode) return;
                const noteNode = leaderBoardRootNode.querySelector(".LeaderboardPanel_note__z4OpJ");
                if (!noteNode) return;

                // make note and buttons
                if (noteNode.classList.contains("RangedWayIdleLeaderBoardNote")) return;
                noteNode.classList.add("RangedWayIdleLeaderBoardNote");
                createNoteAndButton(noteNode);

                // show difference
                const leaderBoardContentNode = leaderBoardRootNode.querySelector(".LeaderboardPanel_content__p_WNw");
                showDifference(leaderBoardContentNode);
            }

            return {ob: ob};
        }

        autoClickTaskSortButton() {
            function ob(node) {
                const buttonNode = node.querySelector('#TaskSort');
                if (!buttonNode || buttonNode.classList.contains("RangedWayIdleAutoClicked")) return;
                buttonNode.click();
                buttonNode.classList.add("RangedWayIdleAutoClicked");
            }

            return {ob: ob};
        }

        showMarketAPIUpdateTime() {
            let lastTime = 0;

            function ob(node) {
                const buttonContainerNode = node.querySelector(".MarketplacePanel_buttonContainer__vJQud");
                if (!buttonContainerNode) return;
                const nowTime = JSON.parse(localStorage.getItem('MWITools_marketAPI_json')).timestamp;
                const lastNode = buttonContainerNode.querySelector(".RangedWayIdleShowMarketAPIUpdateTime");
                if (lastNode) lastNode.remove();
                if (nowTime === lastTime) return;
                lastTime = nowTime;
                const divNode = document.createElement("div");
                divNode.textContent = I18N("showMarketAPIUpdateTimeText") + " " + new Date(nowTime * 1000).toLocaleString();
                divNode.style.color = "rgb(102,204,255)";
                divNode.classList.add("RangedWayIdleShowMarketAPIUpdateTime");
                buttonContainerNode.insertBefore(divNode, buttonContainerNode.lastChild);
            }

            return {ob: ob};
        }

        forceUpdateAPIButton() {
            function ob(node) {
                const listingContainerNode = node.querySelector(".MarketplacePanel_listingCount__3nVY_");
                if (!listingContainerNode || !listingContainerNode.querySelector("button")) return;
                if (listingContainerNode.querySelector(".RangedWayIdleForceUpdateAPIButton")) return;
                const buttonNode = listingContainerNode.querySelector("button").cloneNode(true);
                buttonNode.classList.add("RangedWayIdleForceUpdateAPIButton");
                buttonNode.textContent = I18N("forceUpdateAPIButtonText");
                buttonNode.addEventListener("click", async function () {
                    if (GM && GM.xmlHttpRequest) {
                        GM.xmlHttpRequest({
                            method: 'GET',
                            url: globalVariables.marketAPIUrl,
                            onload: function (response) {
                                const text = response.responseText;
                                localStorage.setItem("MWITools_marketAPI_json", text);
                                alert(I18N("forceUpdateAPIButtonTextSuccess") + new Date(JSON.parse(text).timestamp * 1000).toLocaleString());
                            },
                            onerror: function (err) {
                                alert(I18N("forceUpdateAPIButtonTextError"));
                                console.error(err);
                            },
                            ontimeout: function () {
                                alert(I18N("forceUpdateAPIButtonTextTimeout"));
                                console.error('timeout');
                            }
                        });
                    } else {
                        const resp = await fetch(globalVariable.marketURL);
                        const text = await resp.text();
                        localStorage.setItem("MWITools_marketAPI_json", text);
                        alert(I18N("forceUpdateAPIButtonTextSuccess") + new Date(JSON.parse(text).timestamp * 1000).toLocaleString());
                    }
                });
                listingContainerNode.appendChild(buttonNode);
            }

            return {ob: ob};
        }

        disableQueueUpgradeButton() {
            const disabledButtons = [];

            function ob(node) {
                const buttons = node.querySelectorAll("button");
                for (const button of buttons) {
                    if ((button.textContent === "Upgrade Queue Capacity" || button.textContent === "升级行动队列") && !button.disabled) {
                        button.disabled = true;
                        disabledButtons.push(button);
                    }
                }
                for (let i = disabledButtons.length - 1; i >= 0; i--) {
                    const button = disabledButtons[i];
                    if (!button.isConnected || (button.textContent !== "Upgrade Queue Capacity" && button.textContent !== "升级行动队列")) {
                        button.disabled = false;
                        disabledButtons.splice(i, 1);
                    }
                }
            }

            return {ob: ob};
        }

        disableActionQueueBar() {
            function ob(node) {
                const actionQueueBarNode = node.querySelector(".QueuedActions_queuedActionsEditMenu__3OoQH");
                if (!actionQueueBarNode) return;
                const buttonNode = node.querySelector(".QueuedActions_queuedActions__2xerL ");
                buttonNode.click();
            }

            return {ob: ob};
        }

        hookListingInfo() {
            function handleListing(listing) {
                if (listing.status === "/market_listing_status/cancelled" ||
                    (listing.status === "/market_listing_status/filled" && listing.unclaimedItemCount === 0 && listing.unclaimedCoinCount === 0)) {
                    delete globalVariables.allListings[listing.id];
                    return;
                }
                globalVariables.allListings[listing.id] = {
                    id: listing.id,
                    isSell: listing.isSell,
                    itemHrid: listing.itemHrid,
                    enhancementLevel: listing.enhancementLevel,
                    orderQuantity: listing.orderQuantity,
                    filledQuantity: listing.filledQuantity,
                    price: listing.price,
                    coinsAvailable: listing.coinsAvailable,
                    unclaimedItemCount: listing.unclaimedItemCount,
                    unclaimedCoinCount: listing.unclaimedCoinCount,
                    createdTimestamp: listing.createdTimestamp,
                }
            }

            function saveListings() {
                const obj = JSON.parse(localStorage.getItem('ranged_way_idle_market_listings') || "{}");
                const characterId = globalVariables.initCharacterData.character.id;
                if (!obj[characterId])
                    obj[characterId] = {};
                for (const listingId in globalVariables.allListings) {
                    if (obj[characterId][listingId]) return;
                    const listing = globalVariables.allListings[listingId];
                    obj[characterId][listingId] = {
                        id: listing.id,
                        isSell: listing.isSell,
                        itemHrid: listing.itemHrid,
                        enhancementLevel: listing.enhancementLevel,
                        orderQuantity: listing.orderQuantity,
                        filledQuantity: listing.filledQuantity,
                        price: listing.price,
                        createdTimestamp: listing.createdTimestamp,
                    }
                }
                const nowTime = new Date().getTime();
                for (const listingId in obj[characterId]) {
                    const listing = obj[characterId][listingId];
                    if (nowTime - new Date(listing.createdTimestamp).getTime() > configs.saveListingInfoToLocalStorageMaxDays * 24 * 60 * 60 * 1000) {
                        delete obj[characterId][listingId];
                    }
                }
                localStorage.setItem('ranged_way_idle_market_listings', JSON.stringify(obj));
            }

            function ws(obj) {
                if (obj.type === "init_character_data") {
                    for (const listing of obj.myMarketListings) {
                        handleListing(listing);
                    }
                    if (configs.saveListingInfoToLocalStorage.value) {
                        saveListings();
                    }
                } else if (obj.type === "market_listings_updated") {
                    for (const listing of obj.endMarketListings) {
                        handleListing(listing);
                    }
                    if (configs.saveListingInfoToLocalStorage.value) {
                        saveListings();
                    }
                }
            }

            return {ws: ws};
        }

        showTotalListingFunds() {
            function ws(obj) {
                if (obj.type === "market_listings_updated") {
                    document.querySelectorAll(".RangedWayIdleTotalListingFunds").forEach(node => {
                        node.remove();
                    });
                }
            }

            function ob(node) {
                const marketplacePanelNode = node.querySelector(".MarketplacePanel_marketplacePanel__21b7o");
                if (!marketplacePanelNode) return;
                if (marketplacePanelNode.querySelector(".RangedWayIdleTotalListingFunds")) return;

                let totalUnclaimedCoins = 0;
                let totalPrepaidCoins = 0;
                let totalSellResultCoins = 0;

                for (const listing of Object.values(globalVariables.allListings)) {
                    totalUnclaimedCoins += listing.unclaimedCoinCount;
                    totalPrepaidCoins += listing.coinsAvailable;
                    if (listing.isSell) {
                        const tax = listing.itemHrid === "/items/bag_of_10_cowbells" ? 0.82 : 0.98;
                        totalSellResultCoins += (listing.orderQuantity - listing.filledQuantity) * Math.floor(listing.price * tax)
                    }
                }

                const currentCoinNode = marketplacePanelNode.querySelector(".MarketplacePanel_coinStack__1l0UD");

                const totalUnclaimedCoinsNode = currentCoinNode.cloneNode(true);
                const totalPrepaidCoinsNode = currentCoinNode.cloneNode(true);
                const totalSellResultCoinsNode = currentCoinNode.cloneNode(true);

                totalUnclaimedCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalUnclaimedCoins, configs.showTotalListingFundsPrecise.value);
                totalPrepaidCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalPrepaidCoins, configs.showTotalListingFundsPrecise.value);
                totalSellResultCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalSellResultCoins, configs.showTotalListingFundsPrecise.value);

                totalUnclaimedCoinsNode.querySelector(".Item_name__2C42x").textContent = I18N("totalUnclaimedCoinsText");
                totalPrepaidCoinsNode.querySelector(".Item_name__2C42x").textContent = I18N("totalPrepaidCoinsText");
                totalSellResultCoinsNode.querySelector(".Item_name__2C42x").textContent = I18N("totalSellResultCoinsText");

                totalUnclaimedCoinsNode.querySelector(".Item_name__2C42x").style.color = "#66CCFF";
                totalPrepaidCoinsNode.querySelector(".Item_name__2C42x").style.color = "#66CCFF";
                totalSellResultCoinsNode.querySelector(".Item_name__2C42x").style.color = "#66CCFF";

                currentCoinNode.style.left = "0rem";
                currentCoinNode.style.top = "0rem";
                totalUnclaimedCoinsNode.style.left = "0rem";
                totalUnclaimedCoinsNode.style.top = "1.5rem";
                totalPrepaidCoinsNode.style.left = "8rem";
                totalPrepaidCoinsNode.style.top = "0rem";
                totalSellResultCoinsNode.style.left = "8rem";
                totalSellResultCoinsNode.style.top = "1.5rem";

                totalUnclaimedCoinsNode.classList.add("RangedWayIdleTotalListingFunds");
                totalPrepaidCoinsNode.classList.add("RangedWayIdleTotalListingFunds");
                totalSellResultCoinsNode.classList.add("RangedWayIdleTotalListingFunds");

                marketplacePanelNode.insertBefore(totalUnclaimedCoinsNode, currentCoinNode.nextSibling);
                marketplacePanelNode.insertBefore(totalPrepaidCoinsNode, currentCoinNode.nextSibling);
                marketplacePanelNode.insertBefore(totalSellResultCoinsNode, currentCoinNode.nextSibling);
            }

            return {ws: ws, ob: ob}
        }

        showListingInfo() {
            const allCreateTimeNodes = [];
            let intervalId = null;

            function formatUTCTime(date) {
                return I18N("showListingInfoCreateTimeAt") + " " + date.toLocaleString('en-US', {
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                }).replace(/\//g, '-').replace(',', '');
            }

            function formatLifespan(date) {
                const diffMs = new Date() - date;
                const seconds = Math.floor(diffMs / 1000);
                const minutes = Math.floor(seconds / 60);
                const hours = Math.floor(minutes / 60);
                const days = Math.floor(hours / 24);
                return I18N("showListingInfoCreateTimeLifespan", {
                    days: days,
                    hours: hours % 24,
                    minutes: minutes % 60,
                    seconds: seconds % 60
                });
            }

            function handleTableHead(trNode) {
                const topOrderPriceNode = document.createElement("th");
                topOrderPriceNode.classList.add("RangedWayIdleShowListingInfo");
                const totalPriceNode = document.createElement("th");
                totalPriceNode.classList.add("RangedWayIdleShowListingInfo");

                topOrderPriceNode.textContent = I18N("showListingInfoTopOrderPriceText");
                totalPriceNode.textContent = I18N("showListingInfoTotalPriceText");

                trNode.insertBefore(topOrderPriceNode, trNode.children[4]);
                trNode.insertBefore(totalPriceNode, trNode.children[5]);
            }

            function addDataToRows(bodyNode) {
                let index = Object.keys(globalVariables.allListings).length - 1;
                for (const listingId in globalVariables.allListings) {
                    const trNode = bodyNode.childNodes[index];
                    for (const key in globalVariables.allListings[listingId]) {
                        trNode.dataset[key] = globalVariables.allListings[listingId][key];
                    }
                    trNode.dataset.originalIndex = index;
                    index--;
                }
            }

            function handleTableBody(tbodyNode) {
                const localMarketAPIJson = JSON.parse(localStorage.getItem('MWITools_marketAPI_json'));
                for (const trNode of tbodyNode.querySelectorAll("tr")) {
                    const dataSet = trNode.dataset;

                    // top order price
                    const topOrderPriceNode = document.createElement("td");
                    topOrderPriceNode.classList.add("RangedWayIdleShowListingInfo");
                    const topOrderPriceSpanNode = document.createElement("span");
                    topOrderPriceSpanNode.classList.add("RangedWayIdleShowListingInfo");
                    const itemHrid = dataSet.itemHrid;
                    const enhancementLevel = Number(dataSet.enhancementLevel);
                    const isSell = dataSet.isSell === 'true';
                    const price = Number(dataSet.price);
                    let localPrice = null;
                    try {
                        localPrice = localMarketAPIJson.marketData[itemHrid][enhancementLevel][isSell ? "a" : "b"];
                    } catch (e) {
                    }
                    if (localPrice === -1) localPrice = null;
                    topOrderPriceSpanNode.textContent = formatItemCount(localPrice, 1);
                    if (localPrice === null) {
                        topOrderPriceSpanNode.style.color = "#004FFF";
                    } else if (isSell) {
                        topOrderPriceSpanNode.style.color = localPrice < price ? "#FF0000" : "#00FF00";
                    } else {
                        topOrderPriceSpanNode.style.color = localPrice > price ? "#FF0000" : "#00FF00";
                    }
                    topOrderPriceNode.appendChild(topOrderPriceSpanNode);
                    trNode.insertBefore(topOrderPriceNode, trNode.children[4]);

                    // total price
                    const totalPriceNode = document.createElement("td");
                    totalPriceNode.classList.add("RangedWayIdleShowListingInfo");
                    const totalPriceSpanNode = document.createElement("span");
                    totalPriceSpanNode.classList.add("RangedWayIdleShowListingInfo");
                    const orderQuantity = Number(dataSet.orderQuantity);
                    const filledQuantity = Number(dataSet.filledQuantity);
                    const tax = isSell ? (itemHrid === "/items/bag_of_10_cowbells" ? 0.82 : 0.98) : 1.0;
                    const totalPrice = (orderQuantity - filledQuantity) * Math.floor(price * tax);
                    totalPriceSpanNode.textContent = formatItemCount(totalPrice, configs.showListingPricePrecise.value);
                    totalPriceSpanNode.style.color = itemCountColorMap(totalPrice);
                    totalPriceNode.appendChild(totalPriceSpanNode);
                    trNode.insertBefore(totalPriceNode, trNode.children[5]);

                    // add create time
                    const createTimeNode = document.createElement("div");
                    createTimeNode.classList.add("RangedWayIdleShowListingInfo");
                    createTimeNode.style.fontSize = '0.75rem';
                    if (configs.showListingCreateTimeByLifespan.value) {
                        createTimeNode.textContent = formatLifespan(new Date(dataSet.createdTimestamp));
                        allCreateTimeNodes.push(createTimeNode);
                    } else {
                        createTimeNode.textContent = formatUTCTime(new Date(dataSet.createdTimestamp));
                    }
                    createTimeNode.style.color = "gray";
                    trNode.firstChild.appendChild(createTimeNode);
                }
            }

            function updateLifespan() {
                if (!configs.showListingCreateTimeByLifespan.value) {
                    allCreateTimeNodes.length = 0;
                    if (intervalId !== null) {
                        resetAll();
                        clearInterval(intervalId);
                        intervalId = null;
                    }
                    return;
                }
                allCreateTimeNodes.forEach(node => {
                    if (!node.isConnected) {
                        allCreateTimeNodes.splice(allCreateTimeNodes.indexOf(node), 1);
                        node.remove();
                        return;
                    }
                    const newText = formatLifespan(new Date(node.parentNode.parentNode.dataset.createdTimestamp));
                    if (newText !== node.textContent) {
                        node.textContent = newText;
                    }
                });
                if (intervalId === null) {
                    resetAll();
                    intervalId = setInterval(updateLifespan, 250);
                }
            }

            function resetAll() {
                const myListingTableNode = document.querySelector(".MarketplacePanel_myListingsTable__3P1aT");
                if (!myListingTableNode) return;
                const bodyNode = myListingTableNode.querySelector("tbody");
                if (!bodyNode) return;
                const sortedChildren = Array.from(bodyNode.childNodes).sort((a, b) => parseInt(b.dataset.id) - parseInt(a.dataset.id));
                sortedChildren.forEach(node => bodyNode.appendChild(node));
                myListingTableNode.classList.remove("RangedWayIdleShowListingInfoSet");
                document.querySelectorAll(".RangedWayIdleShowListingInfo").forEach(node => {
                    node.remove();
                });
            }

            function ws(obj) {
                if (obj.type === "market_listings_updated") {
                    resetAll();
                } else if (obj.type === "market_item_order_books_updated") {
                    resetAll();
                }
            }

            function ob(node) {
                updateLifespan();
                const myListingTableNode = node.querySelector(".MarketplacePanel_myListingsTable__3P1aT");
                if (!myListingTableNode) return;
                if (myListingTableNode.classList.contains("RangedWayIdleShowListingInfoSet")) return;
                if (myListingTableNode.querySelectorAll("tbody tr").length !== Object.keys(globalVariables.allListings).length) {
                    // console.error("Listings length not match!");
                    return;
                }
                myListingTableNode.classList.add("RangedWayIdleShowListingInfoSet");

                handleTableHead(myListingTableNode.querySelector("thead tr"));
                addDataToRows(myListingTableNode.querySelector("tbody"));
                handleTableBody(myListingTableNode.querySelector("tbody"));
            }

            return {ws: ws, ob: ob};
        }

        notifyListingFilled() {
            function ws(obj) {
                if (obj.type === "market_listings_updated") {
                    for (const listing of obj.endMarketListings) {
                        if (listing.status === "/market_listing_status/filled" && (listing.unclaimedCoinCount || listing.unclaimedItemCount)) {
                            globalVariables.notifyListingFilledAudio.volume = configs.notifyListingFilledVolume.value;
                            globalVariables.notifyListingFilledAudio.play();
                            return;
                        }
                    }
                }
            }

            return {ws: ws};
        }

        estimateListingCreateTime() {
            let lastMarketItemOrderBooks = null;

            function formatUTCTime(date) {

                return date.toLocaleString('en-US', {
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                }).replace(/\//g, '-').replace(',', '').replace('24:', '00:');
            }

            function getListingData() {
                // author's data
                const data = [
                    {id: 97888637, timestamp: 1760266805648},
                    {id: 98545826, timestamp: 1760496508616},
                    {id: 98724734, timestamp: 1760551920380},
                    {id: 98978743, timestamp: 1760637750329}
                ];
                const localListings = JSON.parse(localStorage.getItem('ranged_way_idle_market_listings'));
                if (localListings) {
                    for (const characterId in localListings) {
                        for (const listingId in localListings[characterId]) {
                            const listing = localListings[characterId][listingId];
                            data.push({id: listing.id, timestamp: new Date(listing.createdTimestamp).getTime()});
                        }
                    }
                } else {
                    for (const listing of Object.values(globalVariables.allListings)) {
                        data.push({id: listing.id, timestamp: new Date(listing.createdTimestamp).getTime()});
                    }
                }
                return [...data].sort((a, b) => a.id - b.id);
            }

            function estimateCreateTime(sortedData, id) {
                const minId = sortedData[0].id;
                const maxId = sortedData[sortedData.length - 1].id;
                if (minId <= id && id <= maxId) {
                    return linearInterpolationEstimate();
                } else {
                    return linearRegressionEstimate();
                }

                function linearInterpolationEstimate() {
                    let leftIndex = 0;
                    let rightIndex = sortedData.length - 1;
                    for (let i = 0; i < sortedData.length; i++) {
                        if (sortedData[i].id === id) {
                            return sortedData[i].timestamp;
                        }
                    }
                    for (let i = 0; i < sortedData.length - 1; i++) {
                        if (id >= sortedData[i].id && id <= sortedData[i + 1].id) {
                            leftIndex = i;
                            rightIndex = i + 1;
                            break;
                        }
                    }
                    const left = sortedData[leftIndex];
                    const right = sortedData[rightIndex];
                    const rightLeftDistance = right.id - left.id;
                    const leftDistance = id - left.id;
                    const k = leftDistance / rightLeftDistance;
                    return (1 - k) * left.timestamp + k * right.timestamp;
                }

                function linearRegressionEstimate() {
                    let sumX = 0, sumY = 0;
                    for (const point of sortedData) {
                        sumX += point.id;
                        sumY += point.timestamp;
                    }
                    const meanX = sumX / sortedData.length;
                    const meanY = sumY / sortedData.length;
                    let numerator = 0;
                    let denominator = 0;
                    for (const datum of sortedData) {
                        numerator += (datum.id - meanX) * (datum.timestamp - meanY);
                        denominator += (datum.id - meanX) * (datum.timestamp - meanX);
                    }
                    const slope = numerator / denominator;
                    if (id > maxId) {
                        return slope * (id - maxId) + sortedData[sortedData.length - 1].timestamp;
                    } else {
                        return slope * (id - minId) + sortedData[0].timestamp;
                    }
                }
            }

            function colorByAccuracy(sortedData, timestamp) {
                const timeDelta = Math.min(...sortedData.map(item => Math.abs(item.timestamp - timestamp)));
                return Math.max(1 - timeDelta / 43200_000, 0.0);
            }

            function colorByLifespan(sortedData, timestamp) {
                const timeDelta = Math.max(new Date().getTime() - timestamp, 0);
                const meanTime = 172800_000;
                return (meanTime * meanTime) / (meanTime * meanTime + timeDelta * timeDelta);
            }

            function ws(obj) {
                if (obj.type === "market_item_order_books_updated") {
                    lastMarketItemOrderBooks = obj.marketItemOrderBooks;
                    document.querySelectorAll(".RangedWayIdleEstimateListingCreateTimeSet").forEach(node => node.classList.remove("RangedWayIdleEstimateListingCreateTimeSet"));
                }
            }

            function ob(node) {
                const targetItemNode = node.querySelector(".MarketplacePanel_currentItem__3ercC");
                if (!targetItemNode) return;
                if (node.querySelector(".RangedWayIdleEstimateListingCreateTimeSet")) return;
                document.querySelectorAll(".RangedWayIdleEstimateListingCreateTime").forEach(node => {
                    node.remove();
                });

                const itemHrid = "/items/" + targetItemNode.querySelector("use").href.baseVal.split('#')[1];
                const enhanceLevelNode = targetItemNode.querySelector(".Item_enhancementLevel__19g-e");
                const enhanceLevel = enhanceLevelNode ? Number(enhanceLevelNode.textContent.substring(1)) : 0;
                if (itemHrid !== lastMarketItemOrderBooks.itemHrid) return;

                const listingContainer = node.querySelector(".MarketplacePanel_orderBooksContainer__B4YE-");
                const askContainer = listingContainer ? listingContainer.childNodes[0] : node.querySelectorAll(".MarketplacePanel_orderBookTableContainer__hUu-X")[0];
                const bidContainer = listingContainer ? listingContainer.childNodes[1] : node.querySelectorAll(".MarketplacePanel_orderBookTableContainer__hUu-X")[1];
                if (!askContainer || !bidContainer) return;
                if (!askContainer || !bidContainer) return;
                const askTable = askContainer.querySelector("table");
                const bidTable = bidContainer.querySelector("table");
                if (!askTable || !bidTable) return;
                if (askTable.querySelector("tbody").childNodes.length !== lastMarketItemOrderBooks.orderBooks[enhanceLevel].asks.length ||
                    bidTable.querySelector("tbody").childNodes.length !== lastMarketItemOrderBooks.orderBooks[enhanceLevel].bids.length) {
                    return;
                }
                askContainer.classList.add("RangedWayIdleEstimateListingCreateTimeSet");
                bidContainer.classList.add("RangedWayIdleEstimateListingCreateTimeSet");

                // head
                const askTimeHead = document.createElement("th");
                askTimeHead.classList.add("RangedWayIdleEstimateListingCreateTime");
                const bidTimeHead = document.createElement("th");
                bidTimeHead.classList.add("RangedWayIdleEstimateListingCreateTime");
                askTimeHead.textContent = I18N("estimateListingCreateTimeText");
                bidTimeHead.textContent = I18N("estimateListingCreateTimeText");
                askTable.querySelector("thead tr").insertBefore(askTimeHead, askTable.querySelector("thead tr").lastChild);
                bidTable.querySelector("thead tr").insertBefore(bidTimeHead, bidTable.querySelector("thead tr").lastChild);


                // body
                const sortedData = getListingData();
                let askIndex = 0, bidIndex = 0;
                for (const row of askTable.querySelectorAll("tbody tr")) {
                    const listingId = lastMarketItemOrderBooks.orderBooks[enhanceLevel].asks[askIndex].listingId;
                    const estimatedTime = estimateCreateTime(sortedData, listingId);
                    const node = document.createElement("td");
                    node.classList.add("RangedWayIdleEstimateListingCreateTime");
                    node.textContent = formatUTCTime(new Date(estimatedTime));
                    if (configs.estimateListingCreateTimeColorByAccuracy.value) {
                        const k = colorByAccuracy(sortedData, estimatedTime);
                        node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                    } else if (configs.estimateListingCreateTimeColorByLifespan.value) {
                        const k = colorByLifespan(sortedData, estimatedTime);
                        node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                    }
                    row.insertBefore(node, row.lastChild);
                    askIndex++;
                }
                for (const row of bidTable.querySelectorAll("tbody tr")) {
                    const listingId = lastMarketItemOrderBooks.orderBooks[enhanceLevel].bids[bidIndex].listingId;
                    const estimatedTime = estimateCreateTime(sortedData, listingId)
                    const node = document.createElement("td");
                    node.classList.add("RangedWayIdleEstimateListingCreateTime");
                    node.textContent = formatUTCTime(new Date(estimatedTime));
                    if (configs.estimateListingCreateTimeColorByAccuracy.value) {
                        const k = colorByAccuracy(sortedData, estimatedTime);
                        node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                    } else if (configs.estimateListingCreateTimeColorByLifespan.value) {
                        const k = colorByLifespan(sortedData, estimatedTime);
                        node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                    }
                    row.insertBefore(node, row.lastChild);
                    bidIndex++;
                }

            }

            return {ws: ws, ob: ob};
        }

        mournForMagicWayIdle() {
            function init() {
                console.log("为法师助手默哀");
            }

            return {init: init};
        }
    }

    function I18N(key, data) {
        const defaultLanguage = "zh-cn";
        let i18nValue;
        if (!I18NMap[key]) {
            i18nValue = key;
        } else if (I18NMap[key][globalVariables.language]) {
            i18nValue = I18NMap[key][globalVariables.language];
        } else if (I18NMap[key][defaultLanguage]) {
            i18nValue = I18NMap[key][defaultLanguage];
        } else {
            i18nValue = key;
        }
        return fillTemplate(i18nValue, data || {});

        function fillTemplate(template, data) {
            return template.replace(/\$\{(\w+)\}/g, (match, key) => {
                return data[key] !== undefined ? data[key] : match;
            });
        }
    }

    function formatItemCount(num, precise = 0) {
        if (num === null) return "NULL";
        num = Number(num);
        if (isNaN(num)) {
            return "NULL";
        }
        const divisorMap = [
            {threshold: 1e13, divisor: 1e12, unit: "T"},
            {threshold: 1e10, divisor: 1e9, unit: "B"},
            {threshold: 1e7, divisor: 1e6, unit: "M"},
            {threshold: 1e4, divisor: 1e3, unit: "K"}
        ];
        for (const {threshold, divisor, unit} of divisorMap) {
            if (Math.abs(num) >= threshold) {
                const value = Math.floor(num / divisor * Math.pow(10, precise)) / Math.pow(10, precise);
                return value + unit;
            }
        }
        return Math.floor(num * Math.pow(10, precise)) / Math.pow(10, precise);
    }

    function parseItemCount(str) {
        const unitMap = {
            "T": 1e12,
            "B": 1e9,
            "M": 1e6,
            "K": 1e3
        }
        for (const unit in unitMap) {
            if (str.endsWith(unit)) {
                const value = Number(str.slice(0, -1));
                return value * unitMap[unit];
            }
        }
        return Number(str);
    }

    function itemCountColorMap(num) {
        if (Math.abs(num) < 1e5) {
            return "#FFFFFF";
        }
        if (Math.abs(num) < 1e7) {
            return "#FDDAA5";
        }
        if (Math.abs(num) < 1e10) {
            return "#82DCCA";
        }
        if (Math.abs(num) < 1e13) {
            return "#77BAEC";
        }
        if (Math.abs(num) < 1e16) {
            return "#AC8FD4";
        }
        return "#F800F8";
    }

    initScript();
})();