您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
事件驱动的插件,用于在 MWI 和 Milkyway Market 之间实现无缝数据同步
// ==UserScript== // @name MWI Market Addon // @name:zh-CN MWI 市场插件 // @namespace https://milkiway.market/ // @version v0.0.6 // @description Event-based addon for seamless data synchronization between MWI and Milkyway Market // @description:zh-CN 事件驱动的插件,用于在 MWI 和 Milkyway Market 之间实现无缝数据同步 // @author mathewcst // @author:zh-CN mathewcst // @license MIT // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @match https://milkywayidle.com/* // @match https://www.milkyway.market/* // @match https://milkyway.market/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com // ==/UserScript== /** Credits to YangLeda and MWITools This script wouldn't be possible without their work. @see https://github.com/YangLeda/Userscripts-For-MilkyWayIdle */ ;(function () { 'use strict' // Configuration const CONFIG = { events: { DATA_READY: 'mwi-character-data-ready', DATA_REQUEST: 'mwi-request-character-data', DATA_RESPONSE: 'mwi-character-data-response', DATA_UPDATED: 'mwi-character-data-updated', ERROR: 'mwi-data-error', }, storage: { CHAR_DATA: 'char_data', LAST_SYNC: 'last_sync_timestamp', }, validation: { MAX_DATA_SIZE: 1024 * 100, // 100KB max REQUIRED_FIELDS: ['character', 'characterSkills'], }, } /** * Find skill by name * @param {Object} rawData - The raw character data * @param {string} name - The name of the skill to find * @returns {number} The skill level, or 0 if not found */ function findSkillByName(rawData, name) { if (!rawData.characterSkills || !Array.isArray(rawData.characterSkills)) { return 0 } const skill = rawData.characterSkills.find( (skill) => skill.skillHrid === `/skills/${name}`, ) return skill ? skill.level : 0 } /** * Filters character data to only include market-relevant information * @param {Object} rawData - The raw character data * @returns {Object} The filtered character data */ function filterCharacterData(rawData) { if (!rawData || typeof rawData !== 'object') { return null } try { return { // Character identification characterId: rawData.character?.id, characterName: rawData.character?.name, characterLevel: findSkillByName(rawData, 'total_level'), characterAvatar: rawData.character?.avatarHrid, characterAvatarOutfit: rawData.character?.avatarOutfitHrid, gameMode: rawData.gameMode, // Skills (only crafting and gathering related) skills: { // Crafting skills milking: findSkillByName(rawData, 'milking'), foraging: findSkillByName(rawData, 'foraging'), woodcutting: findSkillByName(rawData, 'woodcutting'), cheesesmithing: findSkillByName(rawData, 'cheesesmithing'), crafting: findSkillByName(rawData, 'crafting'), tailoring: findSkillByName(rawData, 'tailoring'), cooking: findSkillByName(rawData, 'cooking'), brewing: findSkillByName(rawData, 'brewing'), }, // Inventory (items and quantities) inventory: (rawData.characterItems || []) .filter((item) => item && item.itemHrid && item.count > 0) .map((item) => ({ itemHrid: item.itemHrid, itemLocationHrid: item.itemLocationHrid, quantity: item.count, enhancementLevel: item.enhancementLevel || 0, })), // House buffs that affect production houseBuffs: rawData.houseActionTypeBuffsMap || {}, houseActionTypeBuffsMap: rawData.houseActionTypeBuffsMap || {}, // Active drink/food buffs activeBuffs: rawData.actionTypeDrinkSlotsMap || {}, // MooPass buffs mooPassBuffs: rawData.mooPassBuffs || [], mooPassActionTypeBuffsMap: rawData.mooPassActionTypeBuffsMap || {}, // Community Buffs communityBuffs: rawData.communityBuffs || [], communityActionTypeBuffsMap: rawData.communityActionTypeBuffsMap || {}, // Equipment Buffs equipmentTaskActionBuffs: rawData.equipmentTaskActionBuffs || [], equipmentActionTypeBuffsMap: rawData.equipmentActionTypeBuffsMap || {}, // Stats noncombatStats: rawData.noncombatStats || {}, // Metadata timestamp: Date.now(), } } catch (error) { console.error( '%c[MWIMarket]%c Error filtering character data:', 'color: #ff4444; font-weight: bold', 'color: inherit', error, ) return null } } /** * Validates character data structure and content * @param {Object} data - The character data to validate * @returns {boolean} True if the data is valid, false otherwise */ function validateCharacterData(data) { if (!data || typeof data !== 'object') { return false } // Check data size const dataSize = JSON.stringify(data).length if (dataSize > CONFIG.validation.MAX_DATA_SIZE) { console.warn( '%c[MWIMarket]%c Data size %d exceeds maximum allowed size', 'color: #ff9944; font-weight: bold', 'color: inherit', dataSize, ) return false } // Check required fields if (!data.characterId || !data.skills) { console.warn( '%c[MWIMarket]%c Missing required fields in character data', 'color: #ff9944; font-weight: bold', 'color: inherit', ) return false } // Validate data types if ( typeof data.characterId !== 'number' || typeof data.skills !== 'object' ) { console.warn( '%c[MWIMarket]%c Invalid data types in character data', 'color: #ff9944; font-weight: bold', 'color: inherit', ) return false } return true } /** * Dispatches a custom event with data * @param {string} eventType - The type of event to dispatch * @param {Object} data - The data to dispatch * @param {Window} target - The target window to dispatch the event to */ function dispatchDataEvent(eventType, data, target = window) { try { const event = new CustomEvent(eventType, { detail: data, bubbles: true, cancelable: true, }) target.dispatchEvent(event) console.log( '%c[MWIMarket]%c Dispatched %s event', 'color: #44ff44; font-weight: bold', 'color: inherit', eventType, data, ) } catch (error) { console.error( '%c[MWIMarket]%c Error dispatching %s event:', 'color: #ff4444; font-weight: bold', 'color: inherit', eventType, error, ) } } /** * Hooks the WebSocket to intercept character data * @returns {void} */ function hookWS() { const dataProperty = Object.getOwnPropertyDescriptor( MessageEvent.prototype, 'data', ) const oriGet = dataProperty.get dataProperty.get = hookedGet Object.defineProperty(MessageEvent.prototype, 'data', dataProperty) function hookedGet() { const socket = this.currentTarget if (!(socket instanceof WebSocket)) { return oriGet.call(this) } if ( socket.url.indexOf('api.milkywayidle.com/ws') <= -1 && socket.url.indexOf('api-test.milkywayidle.com/ws') <= -1 ) { return oriGet.call(this) } const message = oriGet.call(this) Object.defineProperty(this, 'data', { value: message }) // Anti-loop return handleMessage(message) } } /** * Handles WebSocket messages and dispatches events * @param {string} message - The message to handle * @returns {Object} The message */ function handleMessage(message) { try { const obj = JSON.parse(message) if (obj.type === 'init_character_data') { console.log( '%c[MWIMarket]%c Character data received from WebSocket', 'color: #44ff44; font-weight: bold', 'color: inherit', ) // Store raw data GM_setValue(CONFIG.storage.CHAR_DATA, obj) GM_setValue(CONFIG.storage.LAST_SYNC, Date.now()) // Filter and validate data const filteredData = filterCharacterData(obj) if (filteredData && validateCharacterData(filteredData)) { // Dispatch event with filtered data dispatchDataEvent(CONFIG.events.DATA_READY, { data: filteredData, source: 'websocket', }) } else { dispatchDataEvent(CONFIG.events.ERROR, { error: 'Invalid character data structure', timestamp: Date.now(), }) } } } catch (error) { console.error( '%c[MWIMarket]%c Error handling WebSocket message:', 'color: #ff4444; font-weight: bold', 'color: inherit', error, ) } return message } /** * Sets up event listeners for data requests * @returns {void} */ function setupEventListeners() { // Listen for data requests window.addEventListener(CONFIG.events.DATA_REQUEST, (event) => { console.log( '%c[MWIMarket]%c Received data request:', 'color: #4444ff; font-weight: bold', 'color: inherit', event.detail, ) // Get latest data const rawData = GM_getValue(CONFIG.storage.CHAR_DATA, null) const lastSync = GM_getValue(CONFIG.storage.LAST_SYNC, 0) if (rawData) { const filteredData = filterCharacterData(rawData) if (filteredData && validateCharacterData(filteredData)) { dispatchDataEvent(CONFIG.events.DATA_RESPONSE, { requestId: event.detail?.requestId, data: filteredData, lastSync: lastSync, source: 'storage', }) } else { dispatchDataEvent(CONFIG.events.ERROR, { requestId: event.detail?.requestId, error: 'No valid character data available', timestamp: Date.now(), }) } } else { dispatchDataEvent(CONFIG.events.ERROR, { requestId: event.detail?.requestId, error: 'No character data found', timestamp: Date.now(), }) } }) // Listen for cross-tab data changes GM_addValueChangeListener( CONFIG.storage.CHAR_DATA, (name, oldValue, newValue, remote) => { if (remote && newValue) { console.log( '%c[MWIMarket]%c Character data updated in another tab', 'color: #44bbff; font-weight: bold', 'color: inherit', ) const filteredData = filterCharacterData(newValue) if (filteredData && validateCharacterData(filteredData)) { dispatchDataEvent(CONFIG.events.DATA_UPDATED, { data: filteredData, source: 'cross-tab', }) } } }, ) } /** * Market site specific functionality * @returns {void} */ function initMarketSite() { console.log( '%c[MWIMarket]%c Initializing addon for market site', 'color: #ff44ff; font-weight: bold', 'color: inherit', ) // Listen for character data events window.addEventListener(CONFIG.events.DATA_READY, (event) => { console.log( '%c[MWIMarket]%c Character data ready on market site:', 'color: #44ff44; font-weight: bold', 'color: inherit', event.detail, ) // Market site can now use the data processMarketData(event.detail.data) }) window.addEventListener(CONFIG.events.DATA_RESPONSE, (event) => { console.log( '%c[MWIMarket]%c Character data response received:', 'color: #44ff44; font-weight: bold', 'color: inherit', event.detail, ) processMarketData(event.detail.data) }) window.addEventListener(CONFIG.events.DATA_UPDATED, (event) => { console.log( '%c[MWIMarket]%c Character data updated:', 'color: #44bbff; font-weight: bold', 'color: inherit', event.detail, ) processMarketData(event.detail.data) }) window.addEventListener(CONFIG.events.ERROR, (event) => { console.error( '%c[MWIMarket]%c Data error:', 'color: #ff4444; font-weight: bold', 'color: inherit', event.detail, ) }) } /** * Process character data on market site * @param {Object} data - The character data to process * @returns {void} */ function processMarketData(data) { if (!data) return // Store in localStorage for backward compatibility localStorage.setItem( '@milkiway-market/character-data', JSON.stringify(data), ) // Dispatch a custom event that the market site can listen to window.dispatchEvent( new CustomEvent('mwi-market-data-processed', { detail: { data }, bubbles: true, }), ) } /** * Initialize based on current site * @returns {void} */ function init() { const isMarketSite = document.URL.includes('milkyway.market') const isGameSite = document.URL.includes('milkywayidle.com') && !document.URL.includes('milkyway.market') if (isGameSite) { console.log( '%c[MWIMarket]%c Initializing addon for game site', 'color: #ff44ff; font-weight: bold', 'color: inherit', ) hookWS() } else if (isMarketSite) { console.log( '%c[MWIMarket]%c Initializing addon for market site', 'color: #ff44ff; font-weight: bold', 'color: inherit', ) initMarketSite() } // Set up event listeners on both sites if (isGameSite || isMarketSite) { setupEventListeners() } } // Initialize the addon init() })()