MWI 市场插件

使用 mwi-moonitoring 库处理 WebSocket 事件的市场插件

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MWI Market Addon
// @name:zh-CN    MWI 市场插件
// @namespace    https://milkiway.market/
// @version      v1.0.1
// @description  Market addon using mwi-moonitoring library for WebSocket events
// @description:zh-CN 使用 mwi-moonitoring 库处理 WebSocket 事件的市场插件
// @author       mathewcst
// @license      MIT
// @match        https://www.milkywayidle.com/*
// @match        https://milkywayidle.com/*
// @match        https://milkyway.market/*
// @match        https://www.milkyway.market/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @icon         https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @require      https://cdn.c3d.gg/moonitoring/mwi-moonitoring-library.min.js#sha256=Qh9t1oFtYxej0/XuJdu1m3MLBWQRpRbn08opWuf+2GM=
// ==/UserScript==

;(function () {
	;('use strict')

	//============================================================================
	// Configuration
	//============================================================================

	const CONFIG = {
		events: {
			DATA_REQUEST: 'mwi-request-character-data',
			DATA_RESPONSE: 'mwi-character-data-response',
			DATA_UPDATED: 'mwi-character-data-updated',
		},
		storage: {
			CHAR_STATE: 'char_state_v3',
			LAST_SYNC: 'last_sync_timestamp',
		},
		performance: {
			DEBOUNCE_MS: 600000, // Batch updates every 10 minutes
			MARKET_SYNC_INTERVAL: 600000, // Market site checks every 10 minutes
		},
		// Events we're interested in processing
		IMPORTANT_EVENTS: [
			'init_character_data',
			'action_completed',
			'items_updated',
			'action_type_consumable_slots_updated',
		],
	}

	//===========================================================================
	// Enhanced Logging System - Matches Web App Pattern
	//============================================================================

	const MWM_LOG_STYLES = {
		reset: 'color: inherit',
		info: 'color: #3B82F6; font-weight: bold', // Blue - matches web app
		success: 'color: #10B981; font-weight: bold', // Green - matches web app
		warn: 'color: #F59E0B; font-weight: bold', // Amber - matches web app
		error: 'color: #DC2626; font-weight: bold', // Red - matches web app
		debug: 'color: #8B5CF6; font-weight: bold', // Purple - matches web app
		perf: 'color: #7C3AED; font-weight: bold', // Violet - matches web app
	}

	function mwmLog(tag, level, message, ...args) {
		const prefix = `%c[MWM_ADDON]${tag ? ` [${tag}]` : ''}%c ${message}`
		let style
		switch (level) {
			case 'success':
				style = MWM_LOG_STYLES.success
				break
			case 'warn':
				style = MWM_LOG_STYLES.warn
				break
			case 'error':
				style = MWM_LOG_STYLES.error
				break
			case 'debug':
				style = MWM_LOG_STYLES.debug
				break
			case 'perf':
				style = MWM_LOG_STYLES.perf
				break
			default:
				style = MWM_LOG_STYLES.info
		}
		const reset = MWM_LOG_STYLES.reset

		let method
		switch (level) {
			case 'error':
				method = console.error
				break
			case 'warn':
				method = console.warn
				break
			default:
				method = console.log
		}
		method(prefix, style, reset, ...args)
	}

	// Enhanced logger with module support - matches web app pattern
	const LOG = {
		info: (msg, ...args) => mwmLog('', 'info', msg, ...args),
		success: (msg, ...args) => mwmLog('', 'success', msg, ...args),
		warn: (msg, ...args) => mwmLog('', 'warn', msg, ...args),
		error: (msg, ...args) => mwmLog('', 'error', msg, ...args),
		debug: (msg, ...args) => mwmLog('', 'debug', msg, ...args),
		perf: (msg, ...args) => mwmLog('', 'perf', msg, ...args),

		// Module-specific loggers (matches web app pattern)
		module: (moduleName) => ({
			info: (msg, ...args) => mwmLog(moduleName, 'info', msg, ...args),
			success: (msg, ...args) => mwmLog(moduleName, 'success', msg, ...args),
			warn: (msg, ...args) => mwmLog(moduleName, 'warn', msg, ...args),
			error: (msg, ...args) => mwmLog(moduleName, 'error', msg, ...args),
			debug: (msg, ...args) => mwmLog(moduleName, 'debug', msg, ...args),
			perf: (msg, ...args) => mwmLog(moduleName, 'perf', msg, ...args),
		}),

		// Legacy support for existing code
		tag: (tag) => ({
			info: (msg, ...args) => mwmLog(tag, 'info', msg, ...args),
			success: (msg, ...args) => mwmLog(tag, 'success', msg, ...args),
			warn: (msg, ...args) => mwmLog(tag, 'warn', msg, ...args),
			error: (msg, ...args) => mwmLog(tag, 'error', msg, ...args),
			debug: (msg, ...args) => mwmLog(tag, 'debug', msg, ...args),
			perf: (msg, ...args) => mwmLog(tag, 'perf', msg, ...args),
		}),
	}

	// Pre-configured module loggers for common components
	const addonLogger = LOG.module('addon')
	const syncLogger = LOG.module('sync')
	const dbLogger = LOG.module('database')
	const stateLogger = LOG.module('state')
	const marketLogger = LOG.module('market')

	//===========================================================================
	// Library Access - Get the globally loaded MWIWebSocket library
	//===========================================================================

	// The library is loaded via @require and attached to window/global scope
	// In strict mode, we need to explicitly reference it
	const MWIWebSocket = window.MWIWebSocket || this.MWIWebSocket

	// Create isolated instance for this addon with optimized settings
	const socket = MWIWebSocket
		? MWIWebSocket.createInstance({
				batchInterval: CONFIG.performance.DEBOUNCE_MS,
				eventWhitelist: CONFIG.IMPORTANT_EVENTS, // Only process events we care about
				debug: false,
				logLevel: 'warn',
				historySize: 20, // Keep minimal history
				enableCache: true, // Cache for performance
				cacheSize: 20,
			})
		: null

	// Early check if library and instance are available
	if (!socket) {
		addonLogger.error(
			'CRITICAL: MWI-Moonitoring library not loaded or instance creation failed!',
		)
		addonLogger.error('Please check:')
		addonLogger.error('1. The @require URL is correct and accessible')
		addonLogger.error('2. The CDN is not blocked by your network')
		addonLogger.error('3. Try using the non-minified version for debugging')
		addonLogger.error('4. Check browser console for CORS or network errors')

		// For market site, we can still function without the library
		const isMarketSite = document.URL.includes('milkyway.market')
		if (!isMarketSite) {
			return // Only exit on game site
		}
	}

	class CharacterDatabase {
		constructor() {
			this.dbName = '@mwm/db'
			this.version = 3 // Increment version to ensure stores are created
			this.db = null
			this.isInitialized = false
			this.initPromise = null
		}

		async init(retryCount = 0) {
			if (this.initPromise) return this.initPromise
			if (this.isInitialized) return Promise.resolve()

			const MAX_RETRIES = 3
			const RETRY_DELAY = 150 + retryCount * 250 // Progressive delay: 150ms, 400ms, 650ms (offset from main app)

			this.initPromise = new Promise((resolve, reject) => {
				const request = indexedDB.open(this.dbName, this.version)

				request.onerror = async () => {
					const error = request.error
					const errorName = error?.name || 'UnknownError'

					LOG.tag('CharacterDB').warn(
						`Database open attempt ${retryCount + 1} failed: ${errorName}`,
						error?.message,
					)

					// Clear the promise on error so it can be retried
					this.initPromise = null

					// Check if we should retry
					if (retryCount < MAX_RETRIES) {
						// Common concurrent access errors that are worth retrying
						const retryableErrors = [
							'UnknownError',
							'AbortError',
							'InvalidStateError',
							'DataError',
						]

						if (retryableErrors.includes(errorName)) {
							LOG.tag('CharacterDB').info(
								`Retrying database open after ${RETRY_DELAY}ms...`,
							)

							// Wait before retrying (with offset to avoid collision with main app)
							await new Promise((r) => setTimeout(r, RETRY_DELAY))

							try {
								// Recursive retry with incremented count
								await this.init(retryCount + 1)
								resolve()
								return
							} catch (retryError) {
								// If retry also fails, reject with the retry error
								reject(retryError)
								return
							}
						}
					}

					// Max retries reached or non-retryable error
					LOG.tag('CharacterDB').error(
						'Failed to open database after retries:',
						error,
					)
					reject(error)
				}

				request.onsuccess = () => {
					this.db = request.result
					this.isInitialized = true
					LOG.tag('CharacterDB').success('Database initialized successfully')
					resolve()
				}

				request.onupgradeneeded = (event) => {
					const db = event.target.result
					LOG.tag('CharacterDB').info('Upgrading database schema...')

					// Create all stores that both addon and website need
					// This ensures compatibility regardless of who creates the DB first

					// Characters store - main store for character data
					if (!db.objectStoreNames.contains('characters')) {
						const store = db.createObjectStore('characters', {
							keyPath: 'characterId',
						})
						store.createIndex('name', 'characterName', { unique: false })
						store.createIndex('lastUpdate', 'lastUpdate', { unique: false })
						LOG.tag('CharacterDB').info('Created characters store')
					}

					// Metadata store - for storing app metadata
					if (!db.objectStoreNames.contains('metadata')) {
						db.createObjectStore('metadata', {
							keyPath: 'key',
						})
						LOG.tag('CharacterDB').info('Created metadata store')
					}

					// Query cache store - for caching API responses
					if (!db.objectStoreNames.contains('queryCache')) {
						const queryCacheStore = db.createObjectStore('queryCache', {
							keyPath: 'key',
						})
						queryCacheStore.createIndex('timestamp', 'timestamp', {
							unique: false,
						})
						LOG.tag('CharacterDB').info('Created queryCache store')
					}

					// Pinned items store - for user's pinned items
					if (!db.objectStoreNames.contains('pinnedItems')) {
						const pinnedStore = db.createObjectStore('pinnedItems', {
							keyPath: 'id',
						})
						pinnedStore.createIndex(
							'itemHrid+enhancement',
							['itemHrid', 'enhancement'],
							{ unique: false },
						)
						pinnedStore.createIndex('order', 'order', { unique: false })
						pinnedStore.createIndex('pinnedAt', 'pinnedAt', { unique: false })
						LOG.tag('CharacterDB').info('Created pinnedItems store')
					}
				}

				request.onblocked = () => {
					LOG.tag('CharacterDB').warn(
						'Database upgrade blocked by another connection (likely the website)',
						{
							dbName: this.dbName,
							version: this.version,
							retryCount,
						},
					)
					// The blocked event means another connection is preventing the upgrade
					// This is common when the website and addon are both active
					// The upgrade will proceed once the blocking connection closes
				}
			})

			return this.initPromise
		}

		async ensureInitialized() {
			if (!this.isInitialized) {
				await this.init()
			}
		}

		async saveCharacter(data) {
			await this.ensureInitialized()

			return new Promise((resolve, reject) => {
				const transaction = this.db.transaction(['characters'], 'readwrite')
				const store = transaction.objectStore('characters')

				const character = {
					...data,
					lastFullSync: data.lastFullSync || Date.now(),
				}

				const request = store.put(character)

				request.onsuccess = () => {
					resolve(request.result)
				}

				request.onerror = () => {
					LOG.tag('CharacterDB').error(
						'Failed to save character:',
						request.error,
					)
					reject(request.error)
				}
			})
		}

		async getCharacter(characterId) {
			await this.ensureInitialized()

			return new Promise((resolve, reject) => {
				const transaction = this.db.transaction(['characters'], 'readonly')
				const store = transaction.objectStore('characters')
				const request = store.get(characterId)

				request.onsuccess = () => resolve(request.result)
				request.onerror = () => reject(request.error)
			})
		}

		async getAllCharacters() {
			await this.ensureInitialized()

			return new Promise((resolve, reject) => {
				const transaction = this.db.transaction(['characters'], 'readonly')
				const store = transaction.objectStore('characters')
				const request = store.getAll()

				request.onsuccess = () => resolve(request.result || [])
				request.onerror = () => reject(request.error)
			})
		}
	}

	//============================================================================
	// State Management using MWI-Moonitoring Library
	//============================================================================

	class CharacterStateManager {
		constructor() {
			this.state = this.loadState() || this.createEmptyState()
			this.lastSave = Date.now()
			this.lastNotify = Date.now()
			this.isLibraryAvailable = socket !== null

			if (this.isLibraryAvailable) {
				// No need to configure here - instance was created with optimal settings

				// Initialize library and set up event subscriptions
				this.setupEventSubscriptions()
			} else {
				stateLogger.error(
					'MWI-Moonitoring library not available for CharacterStateManager',
				)
			}
		}

		createEmptyState() {
			return {
				characterId: null,
				characterName: null,
				characterLevel: 0,
				characterAvatar: null,
				characterAvatarOutfit: null,
				gameMode: null,
				skills: [],
				inventory: new Map(),
				// All buff types
				houseActionTypeBuffsMap: {},
				actionTypeDrinkSlotsMap: {},
				actionTypeFoodSlotsMap: {},
				equipmentActionTypeBuffsMap: {},
				consumableActionTypeBuffsMap: {},
				mooPassActionTypeBuffsMap: {},
				communityActionTypeBuffsMap: {},
				equipmentTaskActionBuffs: [],
				mooPassBuffs: [],
				communityBuffs: [],
				lastFullSync: 0,
				lastUpdate: Date.now(),
				version: 3,
			}
		}

		setupEventSubscriptions() {
			if (!this.isLibraryAvailable) {
				stateLogger.warn(
					'Cannot setup event subscriptions - library not available',
				)
				return
			}

			// Subscribe to full character data
			socket.on('init_character_data', (eventType, data) => {
				stateLogger.info('Received init_character_data event')
				this.handleFullCharacterData(data)
			})

			// Subscribe to incremental updates
			socket.on('action_completed', (eventType, data) => {
				this.handleActionCompleted(data)
			})

			socket.on('items_updated', (eventType, data) => {
				this.handleItemsUpdated(data)
			})

			socket.on('action_type_consumable_slots_updated', (eventType, data) => {
				this.handleConsumablesUpdated(data)
			})

			stateLogger.success('Event subscriptions set up successfully')
		}

		handleFullCharacterData(rawData) {
			stateLogger.success('Processing full character data')

			this.state = {
				...this.state,
				characterId: rawData.character?.id,
				characterName: rawData.character?.name,
				characterLevel: this.findSkillByName(rawData, 'total_level'),
				characterAvatar: rawData.character?.avatarHrid,
				characterAvatarOutfit: rawData.character?.avatarOutfitHrid,
				gameMode: rawData.character?.gameMode,
				skills: rawData.characterSkills || [],
				inventory: this.buildInventoryMap(rawData.characterItems || []),
				// Capture all buff types from raw data
				houseActionTypeBuffsMap: rawData.houseActionTypeBuffsMap || {},
				actionTypeDrinkSlotsMap: rawData.actionTypeDrinkSlotsMap || {},
				actionTypeFoodSlotsMap: rawData.actionTypeFoodSlotsMap || {},
				equipmentActionTypeBuffsMap: rawData.equipmentActionTypeBuffsMap || {},
				consumableActionTypeBuffsMap:
					rawData.consumableActionTypeBuffsMap || {},
				mooPassActionTypeBuffsMap: rawData.mooPassActionTypeBuffsMap || {},
				communityActionTypeBuffsMap: rawData.communityActionTypeBuffsMap || {},
				equipmentTaskActionBuffs: rawData.equipmentTaskActionBuffs || [],
				mooPassBuffs: rawData.mooPassBuffs || [],
				communityBuffs: rawData.communityBuffs || [],
				lastFullSync: Date.now(),
				lastUpdate: Date.now(),
			}

			this.saveState()
			this.notifyStateChange('full_sync')
		}

		handleActionCompleted(data) {
			let hasChanges = false

			// Update items - handle both old and new field names
			const items = data.characterItems || data.endCharacterItems || data.items
			if (items && Array.isArray(items)) {
				// Use merge for incremental updates
				this.state.inventory = this.mergeInventoryUpdate(items)
				hasChanges = true
			}

			// Update skills - handle both old and new field names
			const skills =
				data.characterSkills || data.endCharacterSkills || data.skills
			if (skills && Array.isArray(skills)) {
				// Merge with existing skills array
				const currentSkills = Array.isArray(this.state.skills)
					? this.state.skills
					: []
				const skillMap = new Map()

				// Add existing skills to map
				currentSkills.forEach((skill) => {
					if (skill.skillHrid) {
						skillMap.set(skill.skillHrid, skill)
					}
				})

				// Update with new skill data
				skills.forEach((skill) => {
					if (skill.skillHrid) {
						skillMap.set(skill.skillHrid, skill)
					}
				})

				this.state.skills = Array.from(skillMap.values())
				hasChanges = true
			}

			// Update character level from skills
			if (skills && Array.isArray(skills)) {
				const totalLevel = skills.find(
					(s) => s.skillHrid === '/skills/total_level',
				)?.level
				if (totalLevel) {
					this.state.characterLevel = totalLevel
					hasChanges = true
				}
			}

			if (hasChanges) {
				this.state.lastUpdate = Date.now()
				this.throttledSaveAndNotify()
			}
		}

		handleItemsUpdated(data) {
			// Handle both potential field names for items
			const items = data.characterItems || data.endCharacterItems || data.items
			if (items && Array.isArray(items)) {
				// Use merge for incremental updates
				this.state.inventory = this.mergeInventoryUpdate(items)
				this.state.lastUpdate = Date.now()
				this.throttledSaveAndNotify()
			}
		}

		handleConsumablesUpdated(data) {
			let hasChanges = false

			if (data.actionTypeDrinkSlotsMap) {
				this.state.actionTypeDrinkSlotsMap = data.actionTypeDrinkSlotsMap
				hasChanges = true
			}

			if (data.actionTypeFoodSlotsMap) {
				this.state.actionTypeFoodSlotsMap = data.actionTypeFoodSlotsMap
				hasChanges = true
			}

			if (hasChanges) {
				this.state.lastUpdate = Date.now()
				this.throttledSaveAndNotify()
			}
		}

		throttledSaveAndNotify() {
			const now = Date.now()

			// Save to storage (throttled)
			if (now - this.lastSave > 5000) {
				this.saveState()
				this.lastSave = now
			}

			// Notify changes (throttled)
			if (now - this.lastNotify > 5000) {
				this.notifyStateChange('incremental_update')
				this.lastNotify = now
			}
		}

		buildInventoryMap(items) {
			const map = new Map()
			items.forEach((item) => {
				if (item && item.itemHrid && item.count > 0) {
					const key = this.getInventoryKey(item)
					map.set(key, {
						itemHrid: item.itemHrid,
						itemLocationHrid:
							item.itemLocationHrid || '/item_locations/inventory',
						quantity: item.count,
						enhancementLevel: item.enhancementLevel || 0,
					})
				}
			})
			return map
		}

		mergeInventoryUpdate(updateItems) {
			// Start with a copy of existing inventory
			const mergedInventory = new Map(this.state.inventory || new Map())

			updateItems.forEach((item) => {
				if (item && item.itemHrid) {
					const key = this.getInventoryKey(item)
					if (item.count > 0) {
						// Update or add the item
						mergedInventory.set(key, {
							itemHrid: item.itemHrid,
							itemLocationHrid:
								item.itemLocationHrid || '/item_locations/inventory',
							quantity: item.count,
							enhancementLevel: item.enhancementLevel || 0,
						})
					} else {
						// Remove items with 0 count
						mergedInventory.delete(key)
					}
				}
			})

			return mergedInventory
		}

		getInventoryKey(item) {
			return `${item.itemHrid}:${item.enhancementLevel || 0}:${
				item.itemLocationHrid || 'inventory'
			}`
		}

		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
		}

		getFilteredState() {
			// Convert internal state to PlayerData format
			return this.convertToPlayerData()
		}

		convertToPlayerData() {
			// Return Partial<PlayerData> with all fields we track
			return {
				type: 'init_character_data',
				currentTimestamp: new Date(this.state.lastUpdate).toISOString(),
				character: {
					id: this.state.characterId || 0,
					name: this.state.characterName || '',
					gameMode: this.state.gameMode || '',
					avatarHrid: this.state.characterAvatar || '',
					avatarOutfitHrid: this.state.characterAvatarOutfit || '',
					isOnline: true,
				},
				characterSkills: (this.state.skills || []).map((skill) => ({
					characterID: this.state.characterId || 0,
					skillHrid: skill.skillHrid,
					experience: skill.experience || 0,
					level: skill.level || 0,
				})),
				characterItems: Array.from(this.state.inventory.values()).map(
					(item) => ({
						characterID: this.state.characterId || 0,
						itemLocationHrid:
							item.itemLocationHrid || '/item_locations/inventory',
						itemHrid: item.itemHrid,
						enhancementLevel: item.enhancementLevel || 0,
						count: item.quantity || 0,
					}),
				),
				// Include all buff types we capture
				houseActionTypeBuffsMap: this.state.houseActionTypeBuffsMap || {},
				actionTypeDrinkSlotsMap: this.state.actionTypeDrinkSlotsMap || {},
				actionTypeFoodSlotsMap: this.state.actionTypeFoodSlotsMap || {},
				equipmentActionTypeBuffsMap:
					this.state.equipmentActionTypeBuffsMap || {},
				consumableActionTypeBuffsMap:
					this.state.consumableActionTypeBuffsMap || {},
				mooPassActionTypeBuffsMap: this.state.mooPassActionTypeBuffsMap || {},
				communityActionTypeBuffsMap:
					this.state.communityActionTypeBuffsMap || {},
				equipmentTaskActionBuffs: this.state.equipmentTaskActionBuffs || [],
				mooPassBuffs: this.state.mooPassBuffs || [],
				communityBuffs: this.state.communityBuffs || [],
			}
		}

		saveState() {
			try {
				const storableState = {
					...this.state,
					inventory: Array.from(this.state.inventory.entries()),
				}
				GM_setValue(CONFIG.storage.CHAR_STATE, storableState)
				GM_setValue(CONFIG.storage.LAST_SYNC, Date.now())
			} catch (error) {
				stateLogger.error('Error saving state:', error)
			}
		}

		loadState() {
			try {
				const stored = GM_getValue(CONFIG.storage.CHAR_STATE, null)
				if (stored) {
					if (stored.inventory && Array.isArray(stored.inventory)) {
						stored.inventory = new Map(stored.inventory)
					}
					return stored
				}
			} catch (error) {
				stateLogger.error('Error loading state:', error)
			}
			return null
		}

		notifyStateChange(changeType) {
			const filteredState = this.getFilteredState()
			dispatchDataEvent(CONFIG.events.DATA_UPDATED, {
				data: filteredState,
				changeType: changeType,
				source: 'state_manager',
			})
		}
	}

	// ============================================================================
	// Event System
	// ============================================================================

	let stateManager = null

	function dispatchDataEvent(eventType, data, target = window) {
		try {
			const event = new CustomEvent(eventType, {
				detail: data,
				bubbles: true,
				cancelable: true,
			})
			target.dispatchEvent(event)
		} catch (error) {
			LOG.error(`Error dispatching ${eventType}:`, error)
		}
	}

	function setupEventListeners() {
		// Listen for data requests
		window.addEventListener(CONFIG.events.DATA_REQUEST, (event) => {
			syncLogger.info('Data request received')

			if (!stateManager) {
				stateManager = new CharacterStateManager()
			}

			const filteredState = stateManager.getFilteredState()

			if (filteredState && filteredState.character?.id) {
				dispatchDataEvent(CONFIG.events.DATA_RESPONSE, {
					requestId: event.detail?.requestId,
					data: filteredState,
					lastSync: stateManager.state.lastUpdate,
					source: 'state_manager',
				})
			} else {
				syncLogger.warn('No character data available to send')
			}
		})

		// Listen for cross-tab updates
		GM_addValueChangeListener(
			CONFIG.storage.CHAR_STATE,
			(_, __, newValue, remote) => {
				if (remote && newValue && stateManager) {
					syncLogger.info('State updated in another tab')
					stateManager.state = newValue
					if (
						stateManager.state.inventory &&
						Array.isArray(stateManager.state.inventory)
					) {
						stateManager.state.inventory = new Map(stateManager.state.inventory)
					}
					stateManager.notifyStateChange('cross_tab_sync')
				}
			},
		)
	}

	function setupMarketSync() {
		setInterval(() => {
			syncLogger.info('Checking for character updates')
			dispatchDataEvent(CONFIG.events.DATA_REQUEST, {
				requestId: `market-sync-${Date.now()}`,
				source: 'market_site_periodic',
			})
		}, CONFIG.performance.MARKET_SYNC_INTERVAL)
	}

	// ============================================================================
	// Storage Management
	// ============================================================================

	let characterDB = null

	async function initCharacterDB() {
		if (!characterDB) {
			characterDB = new CharacterDatabase()

			try {
				await characterDB.init()
				dbLogger.success('Character database initialized')
			} catch (error) {
				dbLogger.error('Failed to initialize character database:', error)
				characterDB = null
			}
		}

		return characterDB
	}

	async function processMarketData(data) {
		// Expect PlayerData format
		if (!data.character?.id) {
			LOG.warn('[ProcessData] Invalid character data received')
			return
		}

		try {
			const db = await initCharacterDB()
			if (!db) return

			// Save the PlayerData directly
			await db.saveCharacter({
				characterId: data.character.id,
				characterName: data.character.name,
				data: data,
				lastUpdate: Date.now(),
			})

			// Dispatch event to notify React app
			window.dispatchEvent(
				new CustomEvent('mwi-market-data-processed', {
					detail: {
						characterId: data.character.id,
						data: data,
						storage: 'indexeddb',
					},
					bubbles: true,
				}),
			)

			LOG.success(
				`Character saved: ${data.character.name} (${data.character.id})`,
			)
		} catch (error) {
			LOG.error('[ProcessData] Failed to save character:', error)
		}
	}

	// ============================================================================
	// Market Site Integration
	// ============================================================================

	function initMarketSite() {
		marketLogger.info('Initializing for market site')

		try {
			const storedState = GM_getValue(CONFIG.storage.CHAR_STATE, null)
			if (storedState) {
				LOG.info('Loading existing character data from storage')

				// Convert stored state to PlayerData format before processing
				if (storedState.inventory && Array.isArray(storedState.inventory)) {
					storedState.inventory = new Map(storedState.inventory)
				}
				// Create a temporary state manager to convert the data
				const tempManager = new CharacterStateManager()
				tempManager.state = storedState
				const playerData = tempManager.convertToPlayerData()
				processMarketData(playerData)
			} else {
				LOG.warn('No existing character data found')
			}
		} catch (error) {
			LOG.error('Error loading stored data:', error)
		}

		setupMarketSync()

		let lastCrossTabUpdate = 0
		GM_addValueChangeListener(
			CONFIG.storage.CHAR_STATE,
			(_, __, newValue, remote) => {
				if (remote && newValue) {
					const now = Date.now()
					if (now - lastCrossTabUpdate < 5000) {
						return
					}
					lastCrossTabUpdate = now

					LOG.info('Character data updated in game tab')

					// Convert newValue to PlayerData format before processing
					if (newValue.inventory && Array.isArray(newValue.inventory)) {
						newValue.inventory = new Map(newValue.inventory)
					}
					// Create a temporary state manager to convert the data
					const tempManager = new CharacterStateManager()
					tempManager.state = newValue
					const playerData = tempManager.convertToPlayerData()
					processMarketData(playerData)
				}
			},
		)

		window.addEventListener(CONFIG.events.DATA_RESPONSE, (event) => {
			processMarketData(event.detail.data)
		})

		window.addEventListener(CONFIG.events.DATA_UPDATED, (event) => {
			LOG.info(`Character ${event.detail.changeType}`)
			processMarketData(event.detail.data)
		})
	}

	// ============================================================================
	// Initialization
	// ============================================================================

	function init() {
		const isMarketSite = document.URL.includes('milkyway.market')
		const isGameSite =
			document.URL.includes('milkywayidle.com') && !isMarketSite

		addonLogger.info('Initializing', {
			isGameSite,
			isMarketSite,
		})

		if (isGameSite) {
			// Initialize state manager which will set up library subscriptions
			stateManager = new CharacterStateManager()
		} else if (isMarketSite) {
			initMarketSite()
		}

		if (isGameSite || isMarketSite) {
			setupEventListeners()
		}
	}

	// Start the addon
	init()
})()