MWI Market Addon

Market addon using mwi-moonitoring library for WebSocket events

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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()
})()