diff --git a/plugins/analytics/__init__.py b/plugins/analytics/__init__.py
deleted file mode 100644
index 76dab5e..0000000
--- a/plugins/analytics/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .plugin import AnalyticsPlugin
-
-__all__ = ['AnalyticsPlugin']
diff --git a/plugins/analytics/plugin.py b/plugins/analytics/plugin.py
deleted file mode 100644
index e8491bb..0000000
--- a/plugins/analytics/plugin.py
+++ /dev/null
@@ -1,525 +0,0 @@
-# Description: Analytics and monitoring system for EU-Utility
-# Privacy-focused usage tracking and performance monitoring
-
-"""
-EU-Utility Analytics System
-
-Privacy-focused analytics with opt-in tracking:
-- Feature usage statistics
-- Performance monitoring (FPS, memory, CPU)
-- Error reporting
-- Health checks
-
-All data is stored locally by default.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QCheckBox, QProgressBar, QTableWidget,
- QTableWidgetItem, QTabWidget, QGroupBox
-)
-from PyQt6.QtCore import QTimer, pyqtSignal, Qt
-from plugins.base_plugin import BasePlugin
-from datetime import datetime, timedelta
-import json
-import os
-import psutil
-import time
-
-
-class AnalyticsPlugin(BasePlugin):
- """
- Analytics and monitoring dashboard for EU-Utility.
-
- Tracks:
- - Feature usage (opt-in)
- - System performance
- - Error occurrences
- - Health status
- """
-
- name = "Analytics"
- version = "1.0.0"
- author = "LemonNexus"
- description = "Usage analytics and performance monitoring"
- icon = "bar-chart"
-
- def initialize(self):
- """Initialize analytics system."""
- # Load settings
- self.enabled = self.load_data("enabled", False)
- self.track_performance = self.load_data("track_performance", True)
- self.track_usage = self.load_data("track_usage", False)
-
- # Data storage
- self.usage_data = self.load_data("usage", {})
- self.performance_data = self.load_data("performance", [])
- self.error_data = self.load_data("errors", [])
-
- # Performance tracking
- self.start_time = time.time()
- self.session_events = []
-
- # Setup timers
- if self.track_performance:
- self._setup_performance_monitoring()
-
- def _setup_performance_monitoring(self):
- """Setup periodic performance monitoring."""
- self.performance_timer = QTimer()
- self.performance_timer.timeout.connect(self._record_performance)
- self.performance_timer.start(30000) # Every 30 seconds
-
- # Initial record
- self._record_performance()
-
- def _record_performance(self):
- """Record current system performance."""
- try:
- # Get system stats
- cpu_percent = psutil.cpu_percent(interval=1)
- memory = psutil.virtual_memory()
-
- # Get process info
- process = psutil.Process()
- process_memory = process.memory_info().rss / 1024 / 1024 # MB
-
- record = {
- 'timestamp': datetime.now().isoformat(),
- 'cpu_percent': cpu_percent,
- 'memory_percent': memory.percent,
- 'memory_used_mb': process_memory,
- 'uptime_seconds': time.time() - self.start_time
- }
-
- self.performance_data.append(record)
-
- # Keep only last 1000 records
- if len(self.performance_data) > 1000:
- self.performance_data = self.performance_data[-1000:]
-
- self.save_data("performance", self.performance_data)
-
- except Exception as e:
- self.log_error(f"Performance recording failed: {e}")
-
- def record_event(self, event_type, details=None):
- """Record a usage event (if tracking enabled)."""
- if not self.track_usage:
- return
-
- event = {
- 'type': event_type,
- 'timestamp': datetime.now().isoformat(),
- 'details': details or {}
- }
-
- self.session_events.append(event)
-
- # Update usage stats
- if event_type not in self.usage_data:
- self.usage_data[event_type] = {'count': 0, 'last_used': None}
-
- self.usage_data[event_type]['count'] += 1
- self.usage_data[event_type]['last_used'] = datetime.now().isoformat()
- self.save_data("usage", self.usage_data)
-
- def record_error(self, error_message, context=None):
- """Record an error occurrence."""
- error = {
- 'message': str(error_message),
- 'timestamp': datetime.now().isoformat(),
- 'context': context or {},
- 'session_uptime': time.time() - self.start_time
- }
-
- self.error_data.append(error)
-
- # Keep only last 100 errors
- if len(self.error_data) > 100:
- self.error_data = self.error_data[-100:]
-
- self.save_data("errors", self.error_data)
-
- def get_ui(self):
- """Create analytics dashboard UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
-
- # Title
- title = QLabel("Analytics & Monitoring")
- title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;")
- layout.addWidget(title)
-
- # Tabs
- tabs = QTabWidget()
-
- # Overview tab
- tabs.addTab(self._create_overview_tab(), "Overview")
-
- # Performance tab
- tabs.addTab(self._create_performance_tab(), "Performance")
-
- # Usage tab
- tabs.addTab(self._create_usage_tab(), "Usage")
-
- # Errors tab
- tabs.addTab(self._create_errors_tab(), "Errors")
-
- # Settings tab
- tabs.addTab(self._create_settings_tab(), "Settings")
-
- layout.addWidget(tabs)
-
- # Refresh button
- refresh_btn = QPushButton("Refresh Data")
- refresh_btn.clicked.connect(self._refresh_all)
- layout.addWidget(refresh_btn)
-
- return widget
-
- def _create_overview_tab(self):
- """Create overview dashboard."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # System health
- health_group = QGroupBox("System Health")
- health_layout = QVBoxLayout(health_group)
-
- self.health_status = QLabel("Checking...")
- self.health_status.setStyleSheet("font-size: 16px; font-weight: bold;")
- health_layout.addWidget(self.health_status)
-
- # Health metrics
- self.cpu_label = QLabel("CPU: --")
- self.memory_label = QLabel("Memory: --")
- self.uptime_label = QLabel("Uptime: --")
-
- health_layout.addWidget(self.cpu_label)
- health_layout.addWidget(self.memory_label)
- health_layout.addWidget(self.uptime_label)
-
- layout.addWidget(health_group)
-
- # Session stats
- stats_group = QGroupBox("Session Statistics")
- stats_layout = QVBoxLayout(stats_group)
-
- self.events_label = QLabel(f"Events recorded: {len(self.session_events)}")
- self.errors_label = QLabel(f"Errors: {len(self.error_data)}")
- self.plugins_label = QLabel(f"Active plugins: --")
-
- stats_layout.addWidget(self.events_label)
- stats_layout.addWidget(self.errors_label)
- stats_layout.addWidget(self.plugins_label)
-
- layout.addWidget(stats_group)
-
- layout.addStretch()
-
- # Update immediately
- self._update_overview()
-
- return tab
-
- def _create_performance_tab(self):
- """Create performance monitoring tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # Current stats
- current_group = QGroupBox("Current Performance")
- current_layout = QVBoxLayout(current_group)
-
- self.perf_cpu = QLabel("CPU Usage: --")
- self.perf_memory = QLabel("Memory Usage: --")
- self.perf_process = QLabel("Process Memory: --")
-
- current_layout.addWidget(self.perf_cpu)
- current_layout.addWidget(self.perf_memory)
- current_layout.addWidget(self.perf_process)
-
- layout.addWidget(current_group)
-
- # Historical data
- history_group = QGroupBox("Performance History (Last Hour)")
- history_layout = QVBoxLayout(history_group)
-
- self.perf_table = QTableWidget()
- self.perf_table.setColumnCount(4)
- self.perf_table.setHorizontalHeaderLabels(["Time", "CPU %", "Memory %", "Process MB"])
- self.perf_table.horizontalHeader().setStretchLastSection(True)
-
- history_layout.addWidget(self.perf_table)
- layout.addWidget(history_group)
-
- layout.addStretch()
-
- self._update_performance_tab()
-
- return tab
-
- def _create_usage_tab(self):
- """Create usage statistics tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # Usage table
- self.usage_table = QTableWidget()
- self.usage_table.setColumnCount(3)
- self.usage_table.setHorizontalHeaderLabels(["Feature", "Usage Count", "Last Used"])
- self.usage_table.horizontalHeader().setStretchLastSection(True)
-
- layout.addWidget(self.usage_table)
-
- # Update data
- self._update_usage_tab()
-
- return tab
-
- def _create_errors_tab(self):
- """Create error log tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # Error table
- self.error_table = QTableWidget()
- self.error_table.setColumnCount(3)
- self.error_table.setHorizontalHeaderLabels(["Time", "Error", "Context"])
- self.error_table.horizontalHeader().setStretchLastSection(True)
-
- layout.addWidget(self.error_table)
-
- # Clear button
- clear_btn = QPushButton("Clear Error Log")
- clear_btn.clicked.connect(self._clear_errors)
- layout.addWidget(clear_btn)
-
- self._update_errors_tab()
-
- return tab
-
- def _create_settings_tab(self):
- """Create analytics settings tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # Privacy notice
- privacy = QLabel(
- "๐ Privacy Notice:\n"
- "All analytics data is stored locally on your machine. "
- "No data is sent to external servers unless you explicitly configure it."
- )
- privacy.setStyleSheet("background-color: #2a3a40; padding: 10px; border-radius: 4px;")
- privacy.setWordWrap(True)
- layout.addWidget(privacy)
-
- # Enable analytics
- self.enable_checkbox = QCheckBox("Enable Analytics")
- self.enable_checkbox.setChecked(self.enabled)
- self.enable_checkbox.toggled.connect(self._on_enable_changed)
- layout.addWidget(self.enable_checkbox)
-
- # Performance tracking
- self.perf_checkbox = QCheckBox("Track System Performance")
- self.perf_checkbox.setChecked(self.track_performance)
- self.perf_checkbox.toggled.connect(self._on_perf_changed)
- layout.addWidget(self.perf_checkbox)
-
- # Usage tracking
- self.usage_checkbox = QCheckBox("Track Feature Usage (Opt-in)")
- self.usage_checkbox.setChecked(self.track_usage)
- self.usage_checkbox.toggled.connect(self._on_usage_changed)
- layout.addWidget(self.usage_checkbox)
-
- # Data management
- layout.addWidget(QLabel("Data Management:"))
-
- export_btn = QPushButton("Export Analytics Data")
- export_btn.clicked.connect(self._export_data)
- layout.addWidget(export_btn)
-
- clear_all_btn = QPushButton("Clear All Analytics Data")
- clear_all_btn.setStyleSheet("color: #f44336;")
- clear_all_btn.clicked.connect(self._clear_all_data)
- layout.addWidget(clear_all_btn)
-
- layout.addStretch()
-
- return tab
-
- def _update_overview(self):
- """Update overview tab."""
- try:
- # Get current stats
- cpu = psutil.cpu_percent(interval=0.5)
- memory = psutil.virtual_memory()
-
- # Health status
- if cpu < 50 and memory.percent < 80:
- status = "โ Healthy"
- color = "#4caf50"
- elif cpu < 80 and memory.percent < 90:
- status = "โ Warning"
- color = "#ff9800"
- else:
- status = "โ Critical"
- color = "#f44336"
-
- self.health_status.setText(status)
- self.health_status.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {color};")
-
- self.cpu_label.setText(f"CPU: {cpu:.1f}%")
- self.memory_label.setText(f"Memory: {memory.percent:.1f}%")
-
- # Uptime
- uptime = time.time() - self.start_time
- hours = int(uptime // 3600)
- minutes = int((uptime % 3600) // 60)
- self.uptime_label.setText(f"Uptime: {hours}h {minutes}m")
-
- # Stats
- self.events_label.setText(f"Events recorded: {len(self.session_events)}")
- self.errors_label.setText(f"Total errors: {len(self.error_data)}")
-
- except Exception as e:
- self.log_error(f"Overview update failed: {e}")
-
- def _update_performance_tab(self):
- """Update performance tab."""
- try:
- # Current stats
- cpu = psutil.cpu_percent(interval=0.5)
- memory = psutil.virtual_memory()
- process = psutil.Process()
- process_mem = process.memory_info().rss / 1024 / 1024
-
- self.perf_cpu.setText(f"CPU Usage: {cpu:.1f}%")
- self.perf_memory.setText(f"Memory Usage: {memory.percent:.1f}%")
- self.perf_process.setText(f"Process Memory: {process_mem:.1f} MB")
-
- # Historical data (last 20 records)
- recent_data = self.performance_data[-20:]
- self.perf_table.setRowCount(len(recent_data))
-
- for i, record in enumerate(reversed(recent_data)):
- time_str = record['timestamp'][11:19] # HH:MM:SS
- self.perf_table.setItem(i, 0, QTableWidgetItem(time_str))
- self.perf_table.setItem(i, 1, QTableWidgetItem(f"{record['cpu_percent']:.1f}%"))
- self.perf_table.setItem(i, 2, QTableWidgetItem(f"{record['memory_percent']:.1f}%"))
- self.perf_table.setItem(i, 3, QTableWidgetItem(f"{record['memory_used_mb']:.1f}"))
-
- except Exception as e:
- self.log_error(f"Performance tab update failed: {e}")
-
- def _update_usage_tab(self):
- """Update usage tab."""
- self.usage_table.setRowCount(len(self.usage_data))
-
- for i, (feature, data) in enumerate(sorted(self.usage_data.items())):
- self.usage_table.setItem(i, 0, QTableWidgetItem(feature))
- self.usage_table.setItem(i, 1, QTableWidgetItem(str(data['count'])))
-
- last_used = data.get('last_used', 'Never')
- if last_used and last_used != 'Never':
- last_used = last_used[:16].replace('T', ' ') # Format datetime
- self.usage_table.setItem(i, 2, QTableWidgetItem(last_used))
-
- def _update_errors_tab(self):
- """Update errors tab."""
- self.error_table.setRowCount(len(self.error_data))
-
- for i, error in enumerate(reversed(self.error_data[-50:])): # Last 50 errors
- time_str = error['timestamp'][11:19]
- self.error_table.setItem(i, 0, QTableWidgetItem(time_str))
- self.error_table.setItem(i, 1, QTableWidgetItem(error['message'][:50]))
- self.error_table.setItem(i, 2, QTableWidgetItem(str(error.get('context', ''))[:50]))
-
- def _refresh_all(self):
- """Refresh all tabs."""
- self._update_overview()
- self._update_performance_tab()
- self._update_usage_tab()
- self._update_errors_tab()
-
- def _on_enable_changed(self, checked):
- """Handle analytics enable toggle."""
- self.enabled = checked
- self.save_data("enabled", checked)
-
- if checked and self.track_performance:
- self._setup_performance_monitoring()
- elif not checked and hasattr(self, 'performance_timer'):
- self.performance_timer.stop()
-
- def _on_perf_changed(self, checked):
- """Handle performance tracking toggle."""
- self.track_performance = checked
- self.save_data("track_performance", checked)
-
- if checked and self.enabled:
- self._setup_performance_monitoring()
- elif hasattr(self, 'performance_timer'):
- self.performance_timer.stop()
-
- def _on_usage_changed(self, checked):
- """Handle usage tracking toggle."""
- self.track_usage = checked
- self.save_data("track_usage", checked)
-
- def _export_data(self):
- """Export analytics data."""
- data = {
- 'exported_at': datetime.now().isoformat(),
- 'usage': self.usage_data,
- 'performance_samples': len(self.performance_data),
- 'errors': len(self.error_data)
- }
-
- # Save to file
- export_path = os.path.expanduser('~/.eu-utility/analytics_export.json')
- os.makedirs(os.path.dirname(export_path), exist_ok=True)
-
- with open(export_path, 'w') as f:
- json.dump(data, f, indent=2)
-
- self.notify_info("Export Complete", f"Data exported to:\n{export_path}")
-
- def _clear_all_data(self):
- """Clear all analytics data."""
- self.usage_data = {}
- self.performance_data = []
- self.error_data = []
- self.session_events = []
-
- self.save_data("usage", {})
- self.save_data("performance", [])
- self.save_data("errors", [])
-
- self._refresh_all()
- self.notify_info("Data Cleared", "All analytics data has been cleared.")
-
- def _clear_errors(self):
- """Clear error log."""
- self.error_data = []
- self.save_data("errors", [])
- self._update_errors_tab()
-
- def on_show(self):
- """Update when tab shown."""
- self._refresh_all()
-
- def shutdown(self):
- """Cleanup on shutdown."""
- if hasattr(self, 'performance_timer'):
- self.performance_timer.stop()
-
- # Record final stats
- if self.enabled:
- self.save_data("final_session", {
- 'session_duration': time.time() - self.start_time,
- 'events_recorded': len(self.session_events),
- 'timestamp': datetime.now().isoformat()
- })
diff --git a/plugins/auction_tracker/__init__.py b/plugins/auction_tracker/__init__.py
deleted file mode 100644
index 2b39bff..0000000
--- a/plugins/auction_tracker/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Auction Tracker Plugin
-"""
-
-from .plugin import AuctionTrackerPlugin
-
-__all__ = ["AuctionTrackerPlugin"]
diff --git a/plugins/auction_tracker/plugin.py b/plugins/auction_tracker/plugin.py
deleted file mode 100644
index 78b945d..0000000
--- a/plugins/auction_tracker/plugin.py
+++ /dev/null
@@ -1,261 +0,0 @@
-"""
-EU-Utility - Auction Tracker Plugin
-
-Track auction prices and market trends.
-"""
-
-import json
-from datetime import datetime, timedelta
-from pathlib import Path
-from collections import defaultdict
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem,
- QLineEdit, QComboBox, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-from core.icon_manager import get_icon_manager
-from PyQt6.QtGui import QIcon
-
-
-class AuctionTrackerPlugin(BasePlugin):
- """Track auction prices and analyze market trends."""
-
- name = "Auction Tracker"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track prices, markups, and market trends"
- hotkey = "ctrl+shift+a"
-
- def initialize(self):
- """Setup auction tracker."""
- self.data_file = Path("data/auction_tracker.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.price_history = defaultdict(list)
- self.watchlist = []
-
- self._load_data()
-
- def _load_data(self):
- """Load auction data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.price_history = defaultdict(list, data.get('prices', {}))
- self.watchlist = data.get('watchlist', [])
- except:
- pass
-
- def _save_data(self):
- """Save auction data."""
- with open(self.data_file, 'w') as f:
- json.dump({
- 'prices': dict(self.price_history),
- 'watchlist': self.watchlist
- }, f, indent=2)
-
- def get_ui(self):
- """Create auction tracker UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Get icon manager
- icon_mgr = get_icon_manager()
-
- # Title with icon
- title_layout = QHBoxLayout()
-
- title_icon = QLabel()
- icon_pixmap = icon_mgr.get_pixmap('trending-up', size=20)
- title_icon.setPixmap(icon_pixmap)
- title_icon.setFixedSize(20, 20)
- title_layout.addWidget(title_icon)
-
- title = QLabel("Auction Tracker")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- title_layout.addWidget(title)
- title_layout.addStretch()
-
- layout.addLayout(title_layout)
-
- # Search
- search_layout = QHBoxLayout()
-
- self.search_input = QLineEdit()
- self.search_input.setPlaceholderText("Search item...")
- self.search_input.setStyleSheet("""
- QLineEdit {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 4px;
- padding: 8px;
- }
- """)
- search_layout.addWidget(self.search_input)
-
- search_btn = QPushButton()
- search_pixmap = icon_mgr.get_pixmap('search', size=16)
- search_btn.setIcon(QIcon(search_pixmap))
- search_btn.setIconSize(Qt.QSize(16, 16))
- search_btn.setFixedSize(32, 32)
- search_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- border: none;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #ffa060;
- }
- """)
- search_btn.clicked.connect(self._search_item)
- search_layout.addWidget(search_btn)
-
- layout.addLayout(search_layout)
-
- # Price table
- self.price_table = QTableWidget()
- self.price_table.setColumnCount(6)
- self.price_table.setHorizontalHeaderLabels(["Item", "Bid", "Buyout", "Markup", "Trend", "Time"])
- self.price_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 8px;
- font-weight: bold;
- font-size: 11px;
- }
- """)
- self.price_table.horizontalHeader().setStretchLastSection(True)
- layout.addWidget(self.price_table)
-
- # Quick scan
- scan_btn = QPushButton("Scan Auction Window")
- scan_btn.setStyleSheet("""
- QPushButton {
- background-color: #ffc107;
- color: black;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #ffd54f;
- }
- """)
- scan_btn.clicked.connect(self._scan_auction)
- layout.addWidget(scan_btn)
-
- # Sample data
- self._add_sample_data()
-
- layout.addStretch()
- return widget
-
- def _add_sample_data(self):
- """Add sample auction data."""
- sample_items = [
- {"name": "Nanocube", "bid": 304.00, "buyout": 304.00, "markup": 101.33},
- {"name": "Aakas Plating", "bid": 154.00, "buyout": 159.00, "markup": 102.67},
- {"name": "Wenrex Ingot", "bid": 111.00, "buyout": 118.00, "markup": 108.82},
- ]
-
- self.price_table.setRowCount(len(sample_items))
- for i, item in enumerate(sample_items):
- self.price_table.setItem(i, 0, QTableWidgetItem(item['name']))
- self.price_table.setItem(i, 1, QTableWidgetItem(f"{item['bid']:.2f}"))
- self.price_table.setItem(i, 2, QTableWidgetItem(f"{item['buyout']:.2f}"))
-
- markup_item = QTableWidgetItem(f"{item['markup']:.2f}%")
- if item['markup'] > 105:
- markup_item.setForeground(Qt.GlobalColor.red)
- elif item['markup'] < 102:
- markup_item.setForeground(Qt.GlobalColor.green)
- self.price_table.setItem(i, 3, markup_item)
-
- self.price_table.setItem(i, 4, QTableWidgetItem("โ"))
- self.price_table.setItem(i, 5, QTableWidgetItem("2m ago"))
-
- def _search_item(self):
- """Search for item price history."""
- query = self.search_input.text().lower()
- # TODO: Implement search
-
- def _scan_auction(self):
- """Scan auction window with OCR."""
- # TODO: Implement OCR scanning
- pass
-
- def record_price(self, item_name, bid, buyout, tt_value=None):
- """Record a price observation."""
- entry = {
- 'time': datetime.now().isoformat(),
- 'bid': bid,
- 'buyout': buyout,
- 'tt': tt_value,
- 'markup': (buyout / tt_value * 100) if tt_value else None
- }
-
- self.price_history[item_name].append(entry)
-
- # Keep last 100 entries per item
- if len(self.price_history[item_name]) > 100:
- self.price_history[item_name] = self.price_history[item_name][-100:]
-
- self._save_data()
-
- def get_price_trend(self, item_name, days=7):
- """Get price trend for an item."""
- history = self.price_history.get(item_name, [])
- if not history:
- return None
-
- cutoff = (datetime.now() - timedelta(days=days)).isoformat()
- recent = [h for h in history if h['time'] > cutoff]
-
- if len(recent) < 2:
- return None
-
- prices = [h['buyout'] for h in recent]
- return {
- 'current': prices[-1],
- 'average': sum(prices) / len(prices),
- 'min': min(prices),
- 'max': max(prices),
- 'trend': 'up' if prices[-1] > prices[0] else 'down' if prices[-1] < prices[0] else 'stable'
- }
-
- def get_deals(self, max_markup=102.0):
- """Find items below market price."""
- deals = []
- for item_name, history in self.price_history.items():
- if not history:
- continue
-
- latest = history[-1]
- markup = latest.get('markup')
-
- if markup and markup <= max_markup:
- deals.append({
- 'item': item_name,
- 'price': latest['buyout'],
- 'markup': markup
- })
-
- return sorted(deals, key=lambda x: x['markup'])
diff --git a/plugins/auto_screenshot/__init__.py b/plugins/auto_screenshot/__init__.py
deleted file mode 100644
index ab058c2..0000000
--- a/plugins/auto_screenshot/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-Auto-Screenshot Plugin for EU-Utility
-
-Automatically capture screenshots on Global, HOF, or other
-significant game events.
-"""
-
-from .plugin import AutoScreenshotPlugin
-
-__all__ = ['AutoScreenshotPlugin']
diff --git a/plugins/auto_screenshot/plugin.py b/plugins/auto_screenshot/plugin.py
deleted file mode 100644
index 58974c5..0000000
--- a/plugins/auto_screenshot/plugin.py
+++ /dev/null
@@ -1,735 +0,0 @@
-"""
-EU-Utility - Auto-Screenshot Plugin
-
-Automatically capture screenshots on Global, HOF, or other
-significant game events. Perfect for sharing achievements!
-"""
-
-import os
-from datetime import datetime
-from pathlib import Path
-from typing import Dict, List, Optional, Any
-from dataclasses import dataclass, field
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
- QCheckBox, QSpinBox, QGroupBox, QFileDialog, QLineEdit,
- QComboBox, QListWidget, QListWidgetItem, QTabWidget
-)
-from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject
-from PyQt6.QtGui import QColor
-
-from plugins.base_plugin import BasePlugin
-from core.event_bus import GlobalEvent, LootEvent, SkillGainEvent
-
-
-@dataclass
-class ScreenshotRule:
- """Rule for when to take a screenshot."""
- event_type: str # 'global', 'hof', 'ath', 'skill', 'loot_value', 'custom'
- min_value: float = 0.0 # For value-based triggers (PED, skill gain, etc.)
- enabled: bool = True
- play_sound: bool = True
- show_notification: bool = True
- custom_pattern: str = "" # For custom log patterns
-
-
-class AutoScreenshotSignals(QObject):
- """Signals for thread-safe UI updates."""
- screenshot_taken = pyqtSignal(str) # Path to screenshot
- event_detected = pyqtSignal(str) # Event description
-
-
-class AutoScreenshotPlugin(BasePlugin):
- """
- Plugin for automatic screenshots on game events.
-
- Features:
- - Screenshot on Global/HOF/ATH
- - Screenshot on rare loot
- - Screenshot on significant skill gains
- - Configurable capture delay
- - Custom filename patterns
- - Notification and sound options
- """
-
- name = "Auto-Screenshot"
- version = "1.0.0"
- author = "EU-Utility"
- description = "Capture screenshots on Global/HOF and other events"
- icon = "๐ธ"
- hotkey = "ctrl+shift+c"
-
- def __init__(self, overlay_window, config):
- super().__init__(overlay_window, config)
-
- # Settings
- self.enabled = self.get_config('enabled', True)
- self.capture_delay_ms = self.get_config('capture_delay_ms', 500)
- self.save_directory = self.get_config('save_directory',
- str(Path.home() / "Documents" / "Entropia Universe" / "Screenshots" / "Globals"))
-
- # Filename pattern
- self.filename_pattern = self.get_config('filename_pattern',
- "{event_type}_{timestamp}_{player}")
-
- # Screenshot rules
- self.rules: Dict[str, ScreenshotRule] = {}
- self._load_rules()
-
- # Statistics
- self.screenshots_taken = 0
- self.events_captured: List[Dict] = []
-
- # UI references
- self._ui = None
- self.events_list = None
- self.status_label = None
- self.dir_label = None
-
- # Signals
- self.signals = AutoScreenshotSignals()
- self.signals.screenshot_taken.connect(self._on_screenshot_saved)
- self.signals.event_detected.connect(self._on_event_logged)
-
- # Event subscriptions
- self._subscriptions: List[str] = []
-
- # Pending screenshot timer
- self._pending_screenshot = None
- self._pending_timer = None
-
- def initialize(self) -> None:
- """Initialize plugin."""
- self.log_info("Initializing Auto-Screenshot")
-
- # Ensure save directory exists
- Path(self.save_directory).mkdir(parents=True, exist_ok=True)
-
- # Subscribe to events
- if self.enabled:
- self._subscribe_to_events()
-
- self.log_info(f"Auto-Screenshot initialized (enabled={self.enabled})")
-
- def _subscribe_to_events(self) -> None:
- """Subscribe to game events."""
- # Global/HOF events
- self._subscriptions.append(
- self.subscribe_typed(GlobalEvent, self._on_global_event)
- )
-
- # Loot events (for high-value loot)
- self._subscriptions.append(
- self.subscribe_typed(LootEvent, self._on_loot_event)
- )
-
- # Skill gain events
- self._subscriptions.append(
- self.subscribe_typed(SkillGainEvent, self._on_skill_event)
- )
-
- def get_ui(self) -> QWidget:
- """Return the plugin's UI widget."""
- if self._ui is None:
- self._ui = self._create_ui()
- return self._ui
-
- def _create_ui(self) -> QWidget:
- """Create the plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(12)
- layout.setContentsMargins(16, 16, 16, 16)
-
- # Header
- header = QLabel("๐ธ Auto-Screenshot")
- header.setStyleSheet("""
- font-size: 18px;
- font-weight: bold;
- color: white;
- padding-bottom: 8px;
- """)
- layout.addWidget(header)
-
- # Status
- self.status_label = QLabel(f"Status: {'Enabled' if self.enabled else 'Disabled'} | Captured: {self.screenshots_taken}")
- self.status_label.setStyleSheet("color: rgba(255, 255, 255, 150);")
- layout.addWidget(self.status_label)
-
- # Create tabs
- tabs = QTabWidget()
- tabs.setStyleSheet("""
- QTabWidget::pane {
- background-color: rgba(30, 35, 45, 150);
- border: 1px solid rgba(100, 150, 200, 50);
- border-radius: 8px;
- }
- QTabBar::tab {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- padding: 8px 16px;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- }
- QTabBar::tab:selected {
- background-color: rgba(100, 150, 200, 200);
- }
- """)
-
- # === Rules Tab ===
- rules_widget = QWidget()
- rules_layout = QVBoxLayout(rules_widget)
- rules_layout.setContentsMargins(12, 12, 12, 12)
-
- rules_header = QLabel("Capture Rules")
- rules_header.setStyleSheet("font-weight: bold; color: white;")
- rules_layout.addWidget(rules_header)
-
- # Global checkbox
- self.global_checkbox = QCheckBox("Capture on Global (any value)")
- self.global_checkbox.setChecked(self.rules.get('global', ScreenshotRule('global')).enabled)
- self.global_checkbox.setStyleSheet("color: white;")
- self.global_checkbox.stateChanged.connect(lambda: self._update_rule('global', self.global_checkbox.isChecked()))
- rules_layout.addWidget(self.global_checkbox)
-
- # HOF checkbox
- self.hof_checkbox = QCheckBox("Capture on HOF (50+ PED)")
- self.hof_checkbox.setChecked(self.rules.get('hof', ScreenshotRule('hof', min_value=50)).enabled)
- self.hof_checkbox.setStyleSheet("color: white;")
- self.hof_checkbox.stateChanged.connect(lambda: self._update_rule('hof', self.hof_checkbox.isChecked()))
- rules_layout.addWidget(self.hof_checkbox)
-
- # ATH checkbox
- self.ath_checkbox = QCheckBox("Capture on ATH (All-Time High)")
- self.ath_checkbox.setChecked(self.rules.get('ath', ScreenshotRule('ath')).enabled)
- self.ath_checkbox.setStyleSheet("color: white;")
- self.ath_checkbox.stateChanged.connect(lambda: self._update_rule('ath', self.ath_checkbox.isChecked()))
- rules_layout.addWidget(self.ath_checkbox)
-
- # Discovery checkbox
- self.discovery_checkbox = QCheckBox("Capture on Discovery")
- self.discovery_checkbox.setChecked(self.rules.get('discovery', ScreenshotRule('discovery')).enabled)
- self.discovery_checkbox.setStyleSheet("color: white;")
- self.discovery_checkbox.stateChanged.connect(lambda: self._update_rule('discovery', self.discovery_checkbox.isChecked()))
- rules_layout.addWidget(self.discovery_checkbox)
-
- # High value loot
- loot_layout = QHBoxLayout()
- self.loot_checkbox = QCheckBox("Capture on loot above")
- self.loot_checkbox.setChecked(self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)).enabled)
- self.loot_checkbox.setStyleSheet("color: white;")
- self.loot_checkbox.stateChanged.connect(lambda: self._update_rule('loot_value', self.loot_checkbox.isChecked()))
- loot_layout.addWidget(self.loot_checkbox)
-
- self.loot_spin = QSpinBox()
- self.loot_spin.setRange(1, 10000)
- self.loot_spin.setValue(int(self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)).min_value))
- self.loot_spin.setSuffix(" PED")
- self.loot_spin.setStyleSheet(self._spinbox_style())
- loot_layout.addWidget(self.loot_spin)
- loot_layout.addStretch()
- rules_layout.addLayout(loot_layout)
-
- # Skill gain
- skill_layout = QHBoxLayout()
- self.skill_checkbox = QCheckBox("Capture on skill gain above")
- self.skill_checkbox.setChecked(self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)).enabled)
- self.skill_checkbox.setStyleSheet("color: white;")
- self.skill_checkbox.stateChanged.connect(lambda: self._update_rule('skill', self.skill_checkbox.isChecked()))
- skill_layout.addWidget(self.skill_checkbox)
-
- self.skill_spin = QSpinBox()
- self.skill_spin.setRange(1, 1000)
- self.skill_spin.setValue(int(self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)).min_value * 100))
- self.skill_spin.setSuffix(" points (x0.01)")
- self.skill_spin.setStyleSheet(self._spinbox_style())
- skill_layout.addWidget(self.skill_spin)
- skill_layout.addStretch()
- rules_layout.addLayout(skill_layout)
-
- # Sound/notification options
- options_group = QGroupBox("Notification Options")
- options_layout = QVBoxLayout(options_group)
-
- self.sound_checkbox = QCheckBox("Play sound on capture")
- self.sound_checkbox.setChecked(self.get_config('play_sound', True))
- self.sound_checkbox.setStyleSheet("color: white;")
- options_layout.addWidget(self.sound_checkbox)
-
- self.notification_checkbox = QCheckBox("Show notification on capture")
- self.notification_checkbox.setChecked(self.get_config('show_notification', True))
- self.notification_checkbox.setStyleSheet("color: white;")
- options_layout.addWidget(self.notification_checkbox)
-
- rules_layout.addWidget(options_group)
- rules_layout.addStretch()
-
- tabs.addTab(rules_widget, "๐ Rules")
-
- # === History Tab ===
- history_widget = QWidget()
- history_layout = QVBoxLayout(history_widget)
- history_layout.setContentsMargins(12, 12, 12, 12)
-
- history_header = QLabel("Recent Captures")
- history_header.setStyleSheet("font-weight: bold; color: white;")
- history_layout.addWidget(history_header)
-
- self.events_list = QListWidget()
- self.events_list.setStyleSheet("""
- QListWidget {
- background-color: rgba(30, 35, 45, 150);
- border: 1px solid rgba(100, 150, 200, 50);
- border-radius: 8px;
- color: white;
- }
- QListWidget::item {
- padding: 8px;
- border-bottom: 1px solid rgba(100, 150, 200, 30);
- }
- QListWidget::item:selected {
- background-color: rgba(100, 150, 200, 100);
- }
- """)
- history_layout.addWidget(self.events_list)
-
- # History buttons
- history_btn_layout = QHBoxLayout()
-
- open_folder_btn = QPushButton("๐ Open Folder")
- open_folder_btn.setStyleSheet(self._button_style("#2196f3"))
- open_folder_btn.clicked.connect(self._open_screenshots_folder)
- history_btn_layout.addWidget(open_folder_btn)
-
- clear_history_btn = QPushButton("๐งน Clear History")
- clear_history_btn.setStyleSheet(self._button_style())
- clear_history_btn.clicked.connect(self._clear_history)
- history_btn_layout.addWidget(clear_history_btn)
-
- history_btn_layout.addStretch()
- history_layout.addLayout(history_btn_layout)
-
- tabs.addTab(history_widget, "๐ History")
-
- # === Settings Tab ===
- settings_widget = QWidget()
- settings_layout = QVBoxLayout(settings_widget)
- settings_layout.setContentsMargins(12, 12, 12, 12)
-
- settings_header = QLabel("Capture Settings")
- settings_header.setStyleSheet("font-weight: bold; color: white;")
- settings_layout.addWidget(settings_header)
-
- # Master enable
- self.enable_checkbox = QCheckBox("Enable Auto-Screenshot")
- self.enable_checkbox.setChecked(self.enabled)
- self.enable_checkbox.setStyleSheet("color: #4caf50; font-weight: bold;")
- self.enable_checkbox.stateChanged.connect(self._toggle_enabled)
- settings_layout.addWidget(self.enable_checkbox)
-
- # Capture delay
- delay_layout = QHBoxLayout()
- delay_label = QLabel("Capture Delay:")
- delay_label.setStyleSheet("color: white;")
- delay_layout.addWidget(delay_label)
-
- self.delay_spin = QSpinBox()
- self.delay_spin.setRange(0, 5000)
- self.delay_spin.setValue(self.capture_delay_ms)
- self.delay_spin.setSuffix(" ms")
- self.delay_spin.setSingleStep(100)
- self.delay_spin.setStyleSheet(self._spinbox_style())
- delay_layout.addWidget(self.delay_spin)
-
- delay_info = QLabel("(time to hide overlay)")
- delay_info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
- delay_layout.addWidget(delay_info)
- delay_layout.addStretch()
- settings_layout.addLayout(delay_layout)
-
- # Save directory
- dir_group = QGroupBox("Save Location")
- dir_layout = QVBoxLayout(dir_group)
-
- dir_row = QHBoxLayout()
- self.dir_label = QLabel(self.save_directory)
- self.dir_label.setStyleSheet("color: white;")
- self.dir_label.setWordWrap(True)
- dir_row.addWidget(self.dir_label, 1)
-
- change_dir_btn = QPushButton("๐ Change")
- change_dir_btn.setStyleSheet(self._button_style())
- change_dir_btn.clicked.connect(self._change_save_directory)
- dir_row.addWidget(change_dir_btn)
-
- dir_layout.addLayout(dir_row)
- settings_layout.addWidget(dir_group)
-
- # Filename pattern
- pattern_group = QGroupBox("Filename Pattern")
- pattern_layout = QVBoxLayout(pattern_group)
-
- pattern_info = QLabel("Available variables: {timestamp}, {event_type}, {player}, {value}")
- pattern_info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
- pattern_layout.addWidget(pattern_info)
-
- self.pattern_input = QLineEdit(self.filename_pattern)
- self.pattern_input.setStyleSheet(self._input_style())
- pattern_layout.addWidget(self.pattern_input)
-
- settings_layout.addWidget(pattern_group)
- settings_layout.addStretch()
-
- tabs.addTab(settings_widget, "โ๏ธ Settings")
-
- layout.addWidget(tabs)
-
- # Save button
- save_btn = QPushButton("๐พ Save Settings")
- save_btn.setStyleSheet(self._button_style("#4caf50"))
- save_btn.clicked.connect(self._save_settings)
- layout.addWidget(save_btn)
-
- # Test button
- test_btn = QPushButton("๐ธ Test Screenshot")
- test_btn.setStyleSheet(self._button_style("#ff9800"))
- test_btn.clicked.connect(lambda: self._take_screenshot("test", "TestUser", 0))
- layout.addWidget(test_btn)
-
- return widget
-
- def _button_style(self, color: str = "#607d8b") -> str:
- """Generate button stylesheet."""
- return f"""
- QPushButton {{
- background-color: {color};
- color: white;
- border: none;
- border-radius: 8px;
- padding: 10px 16px;
- font-weight: bold;
- }}
- QPushButton:hover {{
- background-color: {color}dd;
- }}
- QPushButton:pressed {{
- background-color: {color}aa;
- }}
- """
-
- def _input_style(self) -> str:
- """Generate input stylesheet."""
- return """
- QLineEdit {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- border: 1px solid rgba(100, 150, 200, 100);
- border-radius: 8px;
- padding: 8px 12px;
- }
- """
-
- def _spinbox_style(self) -> str:
- """Generate spinbox stylesheet."""
- return """
- QSpinBox {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- border: 1px solid rgba(100, 150, 200, 100);
- border-radius: 4px;
- padding: 4px;
- }
- """
-
- def _on_global_event(self, event: GlobalEvent) -> None:
- """Handle global/HOF events."""
- if not self.enabled:
- return
-
- event_type = event.achievement_type.lower()
-
- # Check if we should capture
- if event_type == 'global' and self.rules.get('global', ScreenshotRule('global')).enabled:
- self._schedule_screenshot('global', event.player_name, event.value)
-
- elif event_type in ['hof', 'hall of fame'] and self.rules.get('hof', ScreenshotRule('hof')).enabled:
- if event.value >= self.rules.get('hof', ScreenshotRule('hof', min_value=50)).min_value:
- self._schedule_screenshot('hof', event.player_name, event.value)
-
- elif event_type in ['ath', 'all time high'] and self.rules.get('ath', ScreenshotRule('ath')).enabled:
- self._schedule_screenshot('ath', event.player_name, event.value)
-
- elif event_type == 'discovery' and self.rules.get('discovery', ScreenshotRule('discovery')).enabled:
- self._schedule_screenshot('discovery', event.player_name, event.value)
-
- def _on_loot_event(self, event: LootEvent) -> None:
- """Handle loot events."""
- if not self.enabled:
- return
-
- rule = self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100))
- if rule.enabled and event.total_tt_value >= rule.min_value:
- self._schedule_screenshot('loot', 'player', event.total_tt_value)
-
- def _on_skill_event(self, event: SkillGainEvent) -> None:
- """Handle skill gain events."""
- if not self.enabled:
- return
-
- rule = self.rules.get('skill', ScreenshotRule('skill', min_value=0.1))
- if rule.enabled and event.gain_amount >= rule.min_value:
- self._schedule_screenshot('skill', 'player', event.gain_amount)
-
- def _schedule_screenshot(self, event_type: str, player_name: str, value: float) -> None:
- """Schedule a screenshot with optional delay."""
- if self._pending_timer:
- self._pending_timer.stop()
-
- self._pending_screenshot = (event_type, player_name, value)
-
- if self.capture_delay_ms > 0:
- self._pending_timer = QTimer()
- self._pending_timer.timeout.connect(lambda: self._execute_screenshot())
- self._pending_timer.setSingleShot(True)
- self._pending_timer.start(self.capture_delay_ms)
- else:
- self._execute_screenshot()
-
- def _execute_screenshot(self) -> None:
- """Execute the pending screenshot."""
- if not self._pending_screenshot:
- return
-
- event_type, player_name, value = self._pending_screenshot
- self._pending_screenshot = None
-
- self._take_screenshot(event_type, player_name, value)
-
- def _take_screenshot(self, event_type: str, player_name: str, value: float) -> None:
- """Take and save a screenshot."""
- try:
- # Generate filename
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- filename = self.filename_pattern.format(
- timestamp=timestamp,
- event_type=event_type.upper(),
- player=player_name,
- value=f"{value:.0f}"
- )
- filename = f"{filename}.png"
-
- # Clean filename
- filename = "".join(c for c in filename if c.isalnum() or c in "._- ")
-
- filepath = Path(self.save_directory) / filename
-
- # Capture screenshot
- screenshot = self.capture_screen(full_screen=True)
-
- # Save
- screenshot.save(str(filepath), 'PNG')
-
- self.screenshots_taken += 1
-
- # Log event
- event_data = {
- 'timestamp': datetime.now().isoformat(),
- 'event_type': event_type,
- 'player': player_name,
- 'value': value,
- 'filepath': str(filepath)
- }
- self.events_captured.append(event_data)
-
- # Trim history
- if len(self.events_captured) > 100:
- self.events_captured = self.events_captured[-100:]
-
- # Emit signals
- self.signals.screenshot_taken.emit(str(filepath))
- self.signals.event_detected.emit(f"{event_type.upper()}: {player_name} - {value:.0f} PED")
-
- # Play sound if enabled
- if self.sound_checkbox.isChecked():
- self.play_sound('global' if event_type in ['global', 'hof', 'ath'] else 'skill_gain')
-
- # Show notification if enabled
- if self.notification_checkbox.isChecked():
- self.notify(
- f"๐ธ {event_type.upper()} Captured!",
- f"Saved to {filename}",
- notification_type='success'
- )
-
- self.log_info(f"Screenshot saved: {filepath}")
-
- except Exception as e:
- self.log_error(f"Screenshot failed: {e}")
- self.notify_error("Screenshot Failed", str(e))
-
- def _on_screenshot_saved(self, filepath: str) -> None:
- """Handle screenshot saved event."""
- self.status_label.setText(f"Status: {'Enabled' if self.enabled else 'Disabled'} | Captured: {self.screenshots_taken}")
-
- def _on_event_logged(self, description: str) -> None:
- """Handle event logged."""
- if self.events_list:
- item = QListWidgetItem(f"[{datetime.now().strftime('%H:%M:%S')}] {description}")
- item.setForeground(QColor("white"))
- self.events_list.insertItem(0, item)
-
- # Trim list
- while self.events_list.count() > 50:
- self.events_list.takeItem(self.events_list.count() - 1)
-
- def _update_rule(self, rule_name: str, enabled: bool) -> None:
- """Update a rule's enabled state."""
- if rule_name not in self.rules:
- # Create default rule
- defaults = {
- 'global': ScreenshotRule('global'),
- 'hof': ScreenshotRule('hof', min_value=50),
- 'ath': ScreenshotRule('ath'),
- 'discovery': ScreenshotRule('discovery'),
- 'loot_value': ScreenshotRule('loot_value', min_value=100),
- 'skill': ScreenshotRule('skill', min_value=0.1)
- }
- self.rules[rule_name] = defaults.get(rule_name, ScreenshotRule(rule_name))
-
- self.rules[rule_name].enabled = enabled
- self._save_rules()
-
- def _toggle_enabled(self, state) -> None:
- """Toggle plugin enabled state."""
- self.enabled = state == Qt.CheckState.Checked.value
-
- if self.enabled:
- self._subscribe_to_events()
- self.status_label.setText(f"Status: Enabled | Captured: {self.screenshots_taken}")
- self.status_label.setStyleSheet("color: #4caf50;")
- else:
- # Unsubscribe
- for sub_id in self._subscriptions:
- self.unsubscribe_typed(sub_id)
- self._subscriptions.clear()
- self.status_label.setText(f"Status: Disabled | Captured: {self.screenshots_taken}")
- self.status_label.setStyleSheet("color: #f44336;")
-
- def _change_save_directory(self) -> None:
- """Change the save directory."""
- new_dir = QFileDialog.getExistingDirectory(
- self._ui,
- "Select Screenshot Directory",
- self.save_directory
- )
-
- if new_dir:
- self.save_directory = new_dir
- Path(self.save_directory).mkdir(parents=True, exist_ok=True)
- if self.dir_label:
- self.dir_label.setText(new_dir)
-
- def _open_screenshots_folder(self) -> None:
- """Open the screenshots folder."""
- import subprocess
- import platform
-
- try:
- if platform.system() == 'Windows':
- subprocess.run(['explorer', self.save_directory], check=True)
- elif platform.system() == 'Darwin':
- subprocess.run(['open', self.save_directory], check=True)
- else:
- subprocess.run(['xdg-open', self.save_directory], check=True)
- except Exception as e:
- self.log_error(f"Failed to open folder: {e}")
-
- def _clear_history(self) -> None:
- """Clear the events history."""
- self.events_captured.clear()
- if self.events_list:
- self.events_list.clear()
-
- def _save_settings(self) -> None:
- """Save all settings."""
- self.capture_delay_ms = self.delay_spin.value()
- self.filename_pattern = self.pattern_input.text()
-
- # Update rules with values
- if 'loot_value' in self.rules:
- self.rules['loot_value'].min_value = self.loot_spin.value()
- if 'skill' in self.rules:
- self.rules['skill'].min_value = self.skill_spin.value() / 100
-
- self.set_config('enabled', self.enabled)
- self.set_config('capture_delay_ms', self.capture_delay_ms)
- self.set_config('save_directory', self.save_directory)
- self.set_config('filename_pattern', self.filename_pattern)
- self.set_config('play_sound', self.sound_checkbox.isChecked())
- self.set_config('show_notification', self.notification_checkbox.isChecked())
-
- self._save_rules()
-
- self.notify_success("Settings Saved", "Auto-screenshot settings updated")
-
- def _save_rules(self) -> None:
- """Save rules to storage."""
- rules_data = {
- name: {
- 'event_type': rule.event_type,
- 'min_value': rule.min_value,
- 'enabled': rule.enabled,
- 'play_sound': rule.play_sound,
- 'show_notification': rule.show_notification
- }
- for name, rule in self.rules.items()
- }
- self.save_data('rules', rules_data)
- self.save_data('events_captured', self.events_captured)
-
- def _load_rules(self) -> None:
- """Load rules from storage."""
- rules_data = self.load_data('rules', {})
- for name, data in rules_data.items():
- self.rules[name] = ScreenshotRule(
- event_type=data['event_type'],
- min_value=data.get('min_value', 0),
- enabled=data.get('enabled', True),
- play_sound=data.get('play_sound', True),
- show_notification=data.get('show_notification', True)
- )
-
- # Set defaults if not loaded
- defaults = {
- 'global': ScreenshotRule('global'),
- 'hof': ScreenshotRule('hof', min_value=50),
- 'ath': ScreenshotRule('ath'),
- 'discovery': ScreenshotRule('discovery'),
- 'loot_value': ScreenshotRule('loot_value', min_value=100),
- 'skill': ScreenshotRule('skill', min_value=0.1)
- }
-
- for name, rule in defaults.items():
- if name not in self.rules:
- self.rules[name] = rule
-
- self.events_captured = self.load_data('events_captured', [])
- self.screenshots_taken = len(self.events_captured)
-
- def on_hotkey(self) -> None:
- """Handle hotkey press - take manual screenshot."""
- self._take_screenshot('manual', 'player', 0)
-
- def shutdown(self) -> None:
- """Cleanup on shutdown."""
- self._save_settings()
-
- if self._pending_timer:
- self._pending_timer.stop()
-
- # Unsubscribe
- for sub_id in self._subscriptions:
- self.unsubscribe_typed(sub_id)
-
- super().shutdown()
diff --git a/plugins/auto_updater/__init__.py b/plugins/auto_updater/__init__.py
deleted file mode 100644
index 9ea3594..0000000
--- a/plugins/auto_updater/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .plugin import AutoUpdaterPlugin
-
-__all__ = ['AutoUpdaterPlugin']
diff --git a/plugins/auto_updater/plugin.py b/plugins/auto_updater/plugin.py
deleted file mode 100644
index 4602b4d..0000000
--- a/plugins/auto_updater/plugin.py
+++ /dev/null
@@ -1,481 +0,0 @@
-# Description: Auto-updater plugin for EU-Utility
-# Checks for updates and installs them automatically
-
-"""
-EU-Utility Auto-Updater
-
-Features:
-- Check for updates from GitHub
-- Download and install updates
-- Changelog display
-- Automatic rollback on failure
-- Scheduled update checks
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QProgressBar, QTextEdit, QMessageBox,
- QCheckBox, QGroupBox, QComboBox
-)
-from PyQt6.QtCore import QThread, pyqtSignal, QTimer, Qt
-from plugins.base_plugin import BasePlugin
-import requests
-import json
-import os
-import shutil
-import zipfile
-import subprocess
-import sys
-from datetime import datetime
-
-
-class UpdateWorker(QThread):
- """Background worker for update operations."""
- progress = pyqtSignal(int)
- status = pyqtSignal(str)
- finished_signal = pyqtSignal(bool, str)
-
- def __init__(self, download_url, install_path, backup_path):
- super().__init__()
- self.download_url = download_url
- self.install_path = install_path
- self.backup_path = backup_path
- self.temp_download = None
-
- def run(self):
- try:
- # Step 1: Download
- self.status.emit("Downloading update...")
- self._download()
- self.progress.emit(33)
-
- # Step 2: Backup
- self.status.emit("Creating backup...")
- self._create_backup()
- self.progress.emit(66)
-
- # Step 3: Install
- self.status.emit("Installing update...")
- self._install()
- self.progress.emit(100)
-
- self.finished_signal.emit(True, "Update installed successfully. Please restart EU-Utility.")
-
- except Exception as e:
- self.status.emit(f"Error: {str(e)}")
- self._rollback()
- self.finished_signal.emit(False, str(e))
- finally:
- # Cleanup temp file
- if self.temp_download and os.path.exists(self.temp_download):
- try:
- os.remove(self.temp_download)
- except:
- pass
-
- def _download(self):
- """Download update package."""
- self.temp_download = os.path.join(os.path.expanduser('~/.eu-utility'), 'update.zip')
- os.makedirs(os.path.dirname(self.temp_download), exist_ok=True)
-
- response = requests.get(self.download_url, stream=True, timeout=120)
- response.raise_for_status()
-
- with open(self.temp_download, 'wb') as f:
- for chunk in response.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
-
- def _create_backup(self):
- """Create backup of current installation."""
- if os.path.exists(self.install_path):
- os.makedirs(self.backup_path, exist_ok=True)
- backup_name = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
- backup_full_path = os.path.join(self.backup_path, backup_name)
- shutil.copytree(self.install_path, backup_full_path, ignore_dangling_symlinks=True)
-
- def _install(self):
- """Install the update."""
- # Extract update
- with zipfile.ZipFile(self.temp_download, 'r') as zip_ref:
- # Extract to temp location first
- temp_extract = os.path.join(os.path.expanduser('~/.eu-utility'), 'update_extract')
- if os.path.exists(temp_extract):
- shutil.rmtree(temp_extract)
- zip_ref.extractall(temp_extract)
-
- # Find the actual content (might be in a subdirectory)
- contents = os.listdir(temp_extract)
- if len(contents) == 1 and os.path.isdir(os.path.join(temp_extract, contents[0])):
- source = os.path.join(temp_extract, contents[0])
- else:
- source = temp_extract
-
- # Copy files to install path
- for item in os.listdir(source):
- s = os.path.join(source, item)
- d = os.path.join(self.install_path, item)
-
- if os.path.isdir(s):
- if os.path.exists(d):
- shutil.rmtree(d)
- shutil.copytree(s, d)
- else:
- shutil.copy2(s, d)
-
- # Cleanup
- shutil.rmtree(temp_extract)
-
- def _rollback(self):
- """Rollback to backup on failure."""
- try:
- # Find most recent backup
- if os.path.exists(self.backup_path):
- backups = sorted(os.listdir(self.backup_path))
- if backups:
- latest_backup = os.path.join(self.backup_path, backups[-1])
-
- # Restore from backup
- if os.path.exists(self.install_path):
- shutil.rmtree(self.install_path)
- shutil.copytree(latest_backup, self.install_path)
- except:
- pass
-
-
-class AutoUpdaterPlugin(BasePlugin):
- """
- Auto-updater for EU-Utility.
-
- Checks for updates from GitHub and installs them automatically.
- """
-
- name = "Auto Updater"
- version = "1.0.0"
- author = "LemonNexus"
- description = "Automatic update checker and installer"
- icon = "refresh"
-
- # GitHub repository info
- GITHUB_REPO = "ImpulsiveFPS/EU-Utility"
- GITHUB_API_URL = "https://api.github.com/repos/{}/releases/latest"
-
- def initialize(self):
- """Initialize auto-updater."""
- self.current_version = "2.0.0" # Should be read from version file
- self.latest_version = None
- self.latest_release = None
- self.worker = None
-
- # Settings
- self.check_on_startup = self.load_data("check_on_startup", True)
- self.auto_install = self.load_data("auto_install", False)
- self.check_interval_hours = self.load_data("check_interval", 24)
-
- # Check for updates if enabled
- if self.check_on_startup:
- QTimer.singleShot(5000, self._check_for_updates) # Check after 5 seconds
-
- def get_ui(self):
- """Create updater UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
-
- # Title
- title = QLabel("Auto Updater")
- title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;")
- layout.addWidget(title)
-
- # Current version
- version_group = QGroupBox("Version Information")
- version_layout = QVBoxLayout(version_group)
-
- self.current_version_label = QLabel(f"Current Version: {self.current_version}")
- version_layout.addWidget(self.current_version_label)
-
- self.latest_version_label = QLabel("Latest Version: Checking...")
- version_layout.addWidget(self.latest_version_label)
-
- self.status_label = QLabel("Status: Ready")
- self.status_label.setStyleSheet("color: #4caf50;")
- version_layout.addWidget(self.status_label)
-
- layout.addWidget(version_group)
-
- # Check for updates button
- check_btn = QPushButton("Check for Updates")
- check_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a9eff;
- color: white;
- padding: 12px;
- font-weight: bold;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #5aafff;
- }
- """)
- check_btn.clicked.connect(self._check_for_updates)
- layout.addWidget(check_btn)
-
- # Changelog
- changelog_group = QGroupBox("Changelog")
- changelog_layout = QVBoxLayout(changelog_group)
-
- self.changelog_text = QTextEdit()
- self.changelog_text.setReadOnly(True)
- self.changelog_text.setPlaceholderText("Check for updates to see changelog...")
- changelog_layout.addWidget(self.changelog_text)
-
- layout.addWidget(changelog_group)
-
- # Progress
- self.progress_bar = QProgressBar()
- self.progress_bar.setVisible(False)
- layout.addWidget(self.progress_bar)
-
- self.progress_status = QLabel("")
- layout.addWidget(self.progress_status)
-
- # Update button
- self.update_btn = QPushButton("Download and Install Update")
- self.update_btn.setStyleSheet("""
- QPushButton {
- background-color: #4caf50;
- color: white;
- padding: 12px;
- font-weight: bold;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #5cbf60;
- }
- QPushButton:disabled {
- background-color: #555;
- }
- """)
- self.update_btn.setEnabled(False)
- self.update_btn.clicked.connect(self._start_update)
- layout.addWidget(self.update_btn)
-
- # Settings
- settings_group = QGroupBox("Settings")
- settings_layout = QVBoxLayout(settings_group)
-
- self.startup_checkbox = QCheckBox("Check for updates on startup")
- self.startup_checkbox.setChecked(self.check_on_startup)
- self.startup_checkbox.toggled.connect(self._on_startup_changed)
- settings_layout.addWidget(self.startup_checkbox)
-
- self.auto_checkbox = QCheckBox("Auto-install updates (not recommended)")
- self.auto_checkbox.setChecked(self.auto_install)
- self.auto_checkbox.toggled.connect(self._on_auto_changed)
- settings_layout.addWidget(self.auto_checkbox)
-
- interval_layout = QHBoxLayout()
- interval_layout.addWidget(QLabel("Check interval:"))
- self.interval_combo = QComboBox()
- self.interval_combo.addItems(["Every hour", "Every 6 hours", "Every 12 hours", "Daily", "Weekly"])
- self.interval_combo.setCurrentIndex(3) # Daily
- self.interval_combo.currentIndexChanged.connect(self._on_interval_changed)
- interval_layout.addWidget(self.interval_combo)
- settings_layout.addLayout(interval_layout)
-
- layout.addWidget(settings_group)
-
- # Manual rollback
- rollback_btn = QPushButton("Rollback to Previous Version")
- rollback_btn.setStyleSheet("color: #ff9800;")
- rollback_btn.clicked.connect(self._rollback_dialog)
- layout.addWidget(rollback_btn)
-
- layout.addStretch()
-
- return widget
-
- def _check_for_updates(self):
- """Check GitHub for updates."""
- self.status_label.setText("Status: Checking...")
- self.status_label.setStyleSheet("color: #ff9800;")
-
- try:
- # Query GitHub API
- url = self.GITHUB_API_URL.format(self.GITHUB_REPO)
- response = requests.get(url, timeout=30)
- response.raise_for_status()
-
- self.latest_release = response.json()
- self.latest_version = self.latest_release['tag_name'].lstrip('v')
-
- self.latest_version_label.setText(f"Latest Version: {self.latest_version}")
-
- # Parse changelog
- changelog = self.latest_release.get('body', 'No changelog available.')
- self.changelog_text.setText(changelog)
-
- # Compare versions
- if self._version_compare(self.latest_version, self.current_version) > 0:
- self.status_label.setText("Status: Update available!")
- self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;")
- self.update_btn.setEnabled(True)
-
- self.notify_info(
- "Update Available",
- f"Version {self.latest_version} is available. Check the Auto Updater to install."
- )
- else:
- self.status_label.setText("Status: Up to date")
- self.status_label.setStyleSheet("color: #4caf50;")
- self.update_btn.setEnabled(False)
-
- except Exception as e:
- self.status_label.setText(f"Status: Check failed")
- self.status_label.setStyleSheet("color: #f44336;")
- self.log_error(f"Update check failed: {e}")
-
- def _version_compare(self, v1, v2):
- """Compare two version strings. Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal."""
- def normalize(v):
- return [int(x) for x in v.split('.')]
-
- n1 = normalize(v1)
- n2 = normalize(v2)
-
- for i in range(max(len(n1), len(n2))):
- x1 = n1[i] if i < len(n1) else 0
- x2 = n2[i] if i < len(n2) else 0
-
- if x1 > x2:
- return 1
- elif x1 < x2:
- return -1
-
- return 0
-
- def _start_update(self):
- """Start the update process."""
- if not self.latest_release:
- QMessageBox.warning(self.get_ui(), "No Update", "Please check for updates first.")
- return
-
- # Get download URL
- assets = self.latest_release.get('assets', [])
- if not assets:
- QMessageBox.critical(self.get_ui(), "Error", "No update package found.")
- return
-
- download_url = assets[0]['browser_download_url']
-
- # Confirm update
- reply = QMessageBox.question(
- self.get_ui(),
- "Confirm Update",
- f"This will update EU-Utility to version {self.latest_version}.\n\n"
- "The application will need to restart after installation.\n\n"
- "Continue?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
-
- if reply != QMessageBox.StandardButton.Yes:
- return
-
- # Start update worker
- install_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- backup_path = os.path.expanduser('~/.eu-utility/backups')
-
- self.worker = UpdateWorker(download_url, install_path, backup_path)
- self.worker.progress.connect(self.progress_bar.setValue)
- self.worker.status.connect(self.progress_status.setText)
- self.worker.finished_signal.connect(self._on_update_finished)
-
- self.progress_bar.setVisible(True)
- self.progress_bar.setValue(0)
- self.update_btn.setEnabled(False)
-
- self.worker.start()
-
- def _on_update_finished(self, success, message):
- """Handle update completion."""
- self.progress_bar.setVisible(False)
-
- if success:
- QMessageBox.information(
- self.get_ui(),
- "Update Complete",
- f"{message}\n\nClick OK to restart EU-Utility."
- )
- self._restart_application()
- else:
- QMessageBox.critical(
- self.get_ui(),
- "Update Failed",
- f"Update failed: {message}\n\nRollback was attempted."
- )
- self.update_btn.setEnabled(True)
-
- def _restart_application(self):
- """Restart the application."""
- python = sys.executable
- script = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'core', 'main.py')
- subprocess.Popen([python, script])
- sys.exit(0)
-
- def _rollback_dialog(self):
- """Show rollback dialog."""
- backup_path = os.path.expanduser('~/.eu-utility/backups')
-
- if not os.path.exists(backup_path):
- QMessageBox.information(self.get_ui(), "No Backups", "No backups found.")
- return
-
- backups = sorted(os.listdir(backup_path))
- if not backups:
- QMessageBox.information(self.get_ui(), "No Backups", "No backups found.")
- return
-
- # Show simple rollback for now
- reply = QMessageBox.question(
- self.get_ui(),
- "Confirm Rollback",
- f"This will restore the most recent backup:\n{backups[-1]}\n\n"
- "The application will restart. Continue?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
-
- if reply == QMessageBox.StandardButton.Yes:
- try:
- backup = os.path.join(backup_path, backups[-1])
- install_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
- # Restore
- if os.path.exists(install_path):
- shutil.rmtree(install_path)
- shutil.copytree(backup, install_path)
-
- QMessageBox.information(
- self.get_ui(),
- "Rollback Complete",
- "Rollback successful. Click OK to restart."
- )
- self._restart_application()
-
- except Exception as e:
- QMessageBox.critical(self.get_ui(), "Rollback Failed", str(e))
-
- def _on_startup_changed(self, checked):
- """Handle startup check toggle."""
- self.check_on_startup = checked
- self.save_data("check_on_startup", checked)
-
- def _on_auto_changed(self, checked):
- """Handle auto-install toggle."""
- self.auto_install = checked
- self.save_data("auto_install", checked)
-
- def _on_interval_changed(self, index):
- """Handle check interval change."""
- intervals = [1, 6, 12, 24, 168] # hours
- self.check_interval_hours = intervals[index]
- self.save_data("check_interval", self.check_interval_hours)
diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py
deleted file mode 100644
index 8934498..0000000
--- a/plugins/base_plugin.py
+++ /dev/null
@@ -1,1193 +0,0 @@
-"""
-EU-Utility - Plugin Base Class
-
-Defines the interface that all plugins must implement.
-Includes PluginAPI integration for cross-plugin communication.
-"""
-
-from abc import ABC, abstractmethod
-from typing import Optional, Dict, Any, TYPE_CHECKING, Callable, List, Type
-
-if TYPE_CHECKING:
- from core.overlay_window import OverlayWindow
- from core.plugin_api import PluginAPI, APIEndpoint, APIType
- from core.event_bus import BaseEvent, EventCategory
-
-
-class BasePlugin(ABC):
- """Base class for all EU-Utility plugins.
-
- To define hotkeys for your plugin, use either:
-
- 1. Legacy single hotkey (simple toggle):
- hotkey = "ctrl+shift+n"
-
- 2. New multi-hotkey format (recommended):
- hotkeys = [
- {
- 'action': 'toggle', # Unique action identifier
- 'description': 'Toggle My Plugin', # Display name in settings
- 'default': 'ctrl+shift+m', # Default hotkey combination
- 'config_key': 'myplugin_toggle' # Settings key (optional)
- },
- {
- 'action': 'quick_action',
- 'description': 'Quick Scan',
- 'default': 'ctrl+shift+s',
- }
- ]
- """
-
- # Plugin metadata - override in subclass
- name: str = "Unnamed Plugin"
- version: str = "1.0.0"
- author: str = "Unknown"
- description: str = "No description provided"
- icon: Optional[str] = None
-
- # Plugin settings
- hotkey: Optional[str] = None # Legacy single hotkey (e.g., "ctrl+shift+n")
- hotkeys: Optional[List[Dict[str, str]]] = None # New multi-hotkey format
- enabled: bool = True
-
- # Dependencies - override in subclass
- # Format: {
- # 'pip': ['package1', 'package2>=1.0'],
- # 'plugins': ['plugin_id1', 'plugin_id2'], # Other plugins this plugin requires
- # 'optional': {'package3': 'description'}
- # }
- dependencies: Dict[str, Any] = {}
-
- def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]):
- self.overlay = overlay_window
- self.config = config
- self._ui = None
- self._api_registered = False
- self._plugin_id = f"{self.__class__.__module__}.{self.__class__.__name__}"
-
- # Track event subscriptions for cleanup
- self._event_subscriptions: List[str] = []
-
- # Get API instance
- try:
- from core.plugin_api import get_api
- self.api = get_api()
- except ImportError:
- self.api = None
-
- @abstractmethod
- def initialize(self) -> None:
- """Called when plugin is loaded. Setup API connections, etc."""
- pass
-
- @abstractmethod
- def get_ui(self) -> Any:
- """Return the plugin's UI widget (QWidget)."""
- return None
-
- def on_show(self) -> None:
- """Called when overlay becomes visible."""
- pass
-
- def on_hide(self) -> None:
- """Called when overlay is hidden."""
- pass
-
- def on_hotkey(self) -> None:
- """Called when plugin's hotkey is pressed."""
- pass
-
- def shutdown(self) -> None:
- """Called when app is closing. Cleanup resources."""
- # Unregister APIs
- if self.api and self._api_registered:
- self.api.unregister_api(self._plugin_id)
-
- # Unsubscribe from all typed events
- self.unsubscribe_all_typed()
-
- # ========== Config Methods ==========
-
- def get_config(self, key: str, default: Any = None) -> Any:
- """Get a config value with default."""
- return self.config.get(key, default)
-
- def set_config(self, key: str, value: Any) -> None:
- """Set a config value."""
- self.config[key] = value
-
- # ========== API Methods ==========
-
- def register_api(self, name: str, handler: Callable, api_type: 'APIType' = None, description: str = "") -> bool:
- """Register an API endpoint for other plugins to use.
-
- Example:
- self.register_api(
- "scan_window",
- self.scan_window,
- APIType.OCR,
- "Scan game window and return text"
- )
- """
- if not self.api:
- print(f"[{self.name}] API not available")
- return False
-
- try:
- from core.plugin_api import APIEndpoint, APIType
-
- if api_type is None:
- api_type = APIType.UTILITY
-
- endpoint = APIEndpoint(
- name=name,
- api_type=api_type,
- description=description,
- handler=handler,
- plugin_id=self._plugin_id,
- version=self.version
- )
-
- success = self.api.register_api(endpoint)
- if success:
- self._api_registered = True
- return success
-
- except Exception as e:
- print(f"[{self.name}] Failed to register API: {e}")
- return False
-
- def call_api(self, plugin_id: str, api_name: str, *args, **kwargs) -> Any:
- """Call another plugin's API.
-
- Example:
- # Call Game Reader's OCR API
- result = self.call_api("plugins.game_reader.plugin", "capture_screen")
- """
- if not self.api:
- raise RuntimeError("API not available")
-
- return self.api.call_api(plugin_id, api_name, *args, **kwargs)
-
- def find_apis(self, api_type: 'APIType' = None) -> list:
- """Find available APIs from other plugins."""
- if not self.api:
- return []
-
- return self.api.find_apis(api_type)
-
- # ========== Shared Services ==========
-
- def ocr_capture(self, region: tuple = None) -> Dict[str, Any]:
- """Capture screen and perform OCR.
-
- Returns:
- {'text': str, 'confidence': float, 'raw_results': list}
- """
- if not self.api:
- return {"text": "", "confidence": 0, "error": "API not available"}
-
- return self.api.ocr_capture(region)
-
- # ========== Screenshot Service Methods ==========
-
- def capture_screen(self, full_screen: bool = True):
- """Capture screenshot.
-
- Args:
- full_screen: If True, capture entire screen
-
- Returns:
- PIL Image object
-
- Example:
- # Capture full screen
- screenshot = self.capture_screen()
-
- # Capture specific region
- region = self.capture_region(100, 100, 800, 600)
- """
- if not self.api:
- raise RuntimeError("API not available")
-
- return self.api.capture_screen(full_screen)
-
- def capture_region(self, x: int, y: int, width: int, height: int):
- """Capture specific screen region.
-
- Args:
- x: Left coordinate
- y: Top coordinate
- width: Region width
- height: Region height
-
- Returns:
- PIL Image object
-
- Example:
- # Capture a 400x200 region starting at (100, 100)
- image = self.capture_region(100, 100, 400, 200)
- """
- if not self.api:
- raise RuntimeError("API not available")
-
- return self.api.capture_region(x, y, width, height)
-
- def get_last_screenshot(self):
- """Get the most recent screenshot.
-
- Returns:
- PIL Image or None if no screenshots taken yet
- """
- if not self.api:
- return None
-
- return self.api.get_last_screenshot()
-
- def read_log(self, lines: int = 50, filter_text: str = None) -> list:
- """Read recent game log lines."""
- if not self.api:
- return []
-
- return self.api.read_log(lines, filter_text)
-
- def get_shared_data(self, key: str, default=None):
- """Get shared data from other plugins."""
- if not self.api:
- return default
-
- return self.api.get_data(key, default)
-
- def set_shared_data(self, key: str, value: Any):
- """Set shared data for other plugins."""
- if self.api:
- self.api.set_data(key, value)
-
- # ========== Legacy Event System ==========
-
- def publish_event(self, event_type: str, data: Dict[str, Any]):
- """Publish an event for other plugins to consume (legacy)."""
- if self.api:
- self.api.publish_event(event_type, data)
-
- def subscribe(self, event_type: str, callback: Callable):
- """Subscribe to events from other plugins (legacy)."""
- if self.api:
- self.api.subscribe(event_type, callback)
-
- # ========== Enhanced Typed Event System ==========
-
- def publish_typed(self, event: 'BaseEvent') -> None:
- """
- Publish a typed event to the Event Bus.
-
- Args:
- event: A typed event instance (SkillGainEvent, LootEvent, etc.)
-
- Example:
- from core.event_bus import LootEvent
-
- self.publish_typed(LootEvent(
- mob_name="Daikiba",
- items=[{"name": "Animal Oil", "value": 0.05}],
- total_tt_value=0.05
- ))
- """
- if self.api:
- self.api.publish_typed(event)
-
- def subscribe_typed(
- self,
- event_class: Type['BaseEvent'],
- callback: Callable,
- **filter_kwargs
- ) -> str:
- """
- Subscribe to a specific event type with optional filtering.
-
- Args:
- event_class: The event class to subscribe to
- callback: Function to call when matching events occur
- **filter_kwargs: Additional filter criteria
- - min_damage: Minimum damage threshold
- - max_damage: Maximum damage threshold
- - mob_types: List of mob names to filter
- - skill_names: List of skill names to filter
- - sources: List of event sources to filter
- - replay_last: Number of recent events to replay
- - predicate: Custom filter function
-
- Returns:
- Subscription ID (store this to unsubscribe later)
-
- Example:
- from core.event_bus import DamageEvent
-
- # Subscribe to all damage events
- self.sub_id = self.subscribe_typed(DamageEvent, self.on_damage)
-
- # Subscribe to high damage events only
- self.sub_id = self.subscribe_typed(
- DamageEvent,
- self.on_big_hit,
- min_damage=100
- )
-
- # Subscribe with replay
- self.sub_id = self.subscribe_typed(
- SkillGainEvent,
- self.on_skill_gain,
- replay_last=10
- )
- """
- if not self.api:
- print(f"[{self.name}] API not available for event subscription")
- return ""
-
- sub_id = self.api.subscribe_typed(event_class, callback, **filter_kwargs)
- if sub_id:
- self._event_subscriptions.append(sub_id)
- return sub_id
-
- def unsubscribe_typed(self, subscription_id: str) -> bool:
- """
- Unsubscribe from a specific typed event subscription.
-
- Args:
- subscription_id: The subscription ID returned by subscribe_typed
-
- Returns:
- True if subscription was found and removed
- """
- if not self.api:
- return False
-
- result = self.api.unsubscribe_typed(subscription_id)
- if result and subscription_id in self._event_subscriptions:
- self._event_subscriptions.remove(subscription_id)
- return result
-
- def unsubscribe_all_typed(self) -> None:
- """Unsubscribe from all typed event subscriptions."""
- if not self.api:
- return
-
- for sub_id in self._event_subscriptions[:]: # Copy list to avoid modification during iteration
- self.api.unsubscribe_typed(sub_id)
- self._event_subscriptions.clear()
-
- def get_recent_events(
- self,
- event_type: Type['BaseEvent'] = None,
- count: int = 100,
- category: 'EventCategory' = None
- ) -> List['BaseEvent']:
- """
- Get recent events from history.
-
- Args:
- event_type: Filter by event class
- count: Maximum number of events to return
- category: Filter by event category
-
- Returns:
- List of matching events
-
- Example:
- from core.event_bus import LootEvent
-
- # Get last 20 loot events
- recent_loot = self.get_recent_events(LootEvent, 20)
- """
- if not self.api:
- return []
-
- return self.api.get_recent_events(event_type, count, category)
-
- def get_event_stats(self) -> Dict[str, Any]:
- """
- Get Event Bus statistics.
-
- Returns:
- Dict with event bus statistics
- """
- if not self.api:
- return {}
-
- return self.api.get_event_stats()
-
- # ========== Utility Methods ==========
-
- def format_ped(self, value: float) -> str:
- """Format PED value."""
- if self.api:
- return self.api.format_ped(value)
- return f"{value:.2f} PED"
-
- def format_pec(self, value: float) -> str:
- """Format PEC value."""
- if self.api:
- return self.api.format_pec(value)
- return f"{value:.0f} PEC"
-
- def calculate_dpp(self, damage: float, ammo: int, decay: float) -> float:
- """Calculate Damage Per PEC."""
- if self.api:
- return self.api.calculate_dpp(damage, ammo, decay)
-
- # Fallback calculation
- if damage <= 0:
- return 0.0
- ammo_cost = ammo * 0.01
- total_cost = ammo_cost + decay
- if total_cost <= 0:
- return 0.0
- return damage / (total_cost / 100)
-
- def calculate_markup(self, price: float, tt: float) -> float:
- """Calculate markup percentage."""
- if self.api:
- return self.api.calculate_markup(price, tt)
-
- if tt <= 0:
- return 0.0
- return (price / tt) * 100
-
- # ========== Audio Service Methods ==========
-
- def play_sound(self, filename_or_key: str, blocking: bool = False) -> bool:
- """Play a sound by key or filename.
-
- Args:
- filename_or_key: Sound key ('global', 'hof', 'skill_gain', 'alert', 'error')
- or path to file
- blocking: If True, wait for sound to complete (default: False)
-
- Returns:
- True if sound was queued/played, False on error or if muted
-
- Examples:
- # Play predefined sounds
- self.play_sound('hof')
- self.play_sound('skill_gain')
- self.play_sound('alert')
-
- # Play custom sound file
- self.play_sound('/path/to/custom.wav')
- """
- if not self.api:
- return False
-
- return self.api.play_sound(filename_or_key, blocking)
-
- def set_volume(self, volume: float) -> None:
- """Set global audio volume.
-
- Args:
- volume: Volume level from 0.0 (mute) to 1.0 (max)
- """
- if self.api:
- self.api.set_volume(volume)
-
- def get_volume(self) -> float:
- """Get current audio volume.
-
- Returns:
- Current volume level (0.0 to 1.0)
- """
- if not self.api:
- return 0.0
-
- return self.api.get_volume()
-
- def mute(self) -> None:
- """Mute all audio."""
- if self.api:
- self.api.mute_audio()
-
- def unmute(self) -> None:
- """Unmute audio."""
- if self.api:
- self.api.unmute_audio()
-
- def toggle_mute(self) -> bool:
- """Toggle audio mute state.
-
- Returns:
- New muted state (True if now muted)
- """
- if not self.api:
- return False
-
- return self.api.toggle_mute_audio()
-
- def is_muted(self) -> bool:
- """Check if audio is muted.
-
- Returns:
- True if audio is muted
- """
- if not self.api:
- return False
-
- return self.api.is_audio_muted()
-
- def is_audio_available(self) -> bool:
- """Check if audio service is available.
-
- Returns:
- True if audio backend is initialized and working
- """
- if not self.api:
- return False
-
- return self.api.is_audio_available()
-
- # ========== Background Task Methods ==========
-
- def run_in_background(self, func: Callable, *args,
- priority: str = 'normal',
- on_complete: Callable = None,
- on_error: Callable = None,
- **kwargs) -> str:
- """Run a function in a background thread.
-
- Use this instead of creating your own QThreads.
-
- Args:
- func: Function to execute in background
- *args: Positional arguments for the function
- priority: 'high', 'normal', or 'low' (default: 'normal')
- on_complete: Called with result when task completes successfully
- on_error: Called with exception when task fails
- **kwargs: Keyword arguments for the function
-
- Returns:
- Task ID for tracking/cancellation
-
- Example:
- def heavy_calculation(data):
- return process(data)
-
- def on_done(result):
- self.update_ui(result)
-
- def on_fail(error):
- self.show_error(str(error))
-
- task_id = self.run_in_background(
- heavy_calculation,
- large_dataset,
- priority='high',
- on_complete=on_done,
- on_error=on_fail
- )
-
- # Or with decorator style:
- @self.run_in_background
- def fetch_remote_data():
- return requests.get(url).json()
- """
- if not self.api:
- raise RuntimeError("API not available")
-
- return self.api.run_in_background(
- func, *args,
- priority=priority,
- on_complete=on_complete,
- on_error=on_error,
- **kwargs
- )
-
- def schedule_task(self, delay_ms: int, func: Callable, *args,
- priority: str = 'normal',
- on_complete: Callable = None,
- on_error: Callable = None,
- periodic: bool = False,
- interval_ms: int = None,
- **kwargs) -> str:
- """Schedule a task for delayed or periodic execution.
-
- Args:
- delay_ms: Milliseconds to wait before first execution
- func: Function to execute
- *args: Positional arguments
- priority: 'high', 'normal', or 'low'
- on_complete: Called with result after each execution
- on_error: Called with exception if execution fails
- periodic: If True, repeat execution at interval_ms
- interval_ms: Milliseconds between periodic executions
- **kwargs: Keyword arguments
-
- Returns:
- Task ID for tracking/cancellation
-
- Example:
- # One-time delayed execution
- task_id = self.schedule_task(
- 5000, # 5 seconds
- lambda: print("Hello after delay!")
- )
-
- # Periodic data refresh (every 30 seconds)
- self.schedule_task(
- 0, # Start immediately
- self.refresh_data,
- periodic=True,
- interval_ms=30000,
- on_complete=lambda data: self.update_display(data)
- )
- """
- if not self.api:
- raise RuntimeError("API not available")
-
- return self.api.schedule_task(
- delay_ms, func, *args,
- priority=priority,
- on_complete=on_complete,
- on_error=on_error,
- periodic=periodic,
- interval_ms=interval_ms,
- **kwargs
- )
-
- def cancel_task(self, task_id: str) -> bool:
- """Cancel a pending or running task.
-
- Args:
- task_id: Task ID returned by run_in_background or schedule_task
-
- Returns:
- True if task was cancelled, False if not found or already done
- """
- if not self.api:
- return False
-
- return self.api.cancel_task(task_id)
-
- def connect_task_signals(self,
- on_completed: Callable = None,
- on_failed: Callable = None,
- on_started: Callable = None,
- on_cancelled: Callable = None) -> bool:
- """Connect to task status signals for UI updates.
-
- Connects Qt signals so UI updates from background threads are thread-safe.
-
- Args:
- on_completed: Called with (task_id, result) when tasks complete
- on_failed: Called with (task_id, error_message) when tasks fail
- on_started: Called with (task_id) when tasks start
- on_cancelled: Called with (task_id) when tasks are cancelled
-
- Returns:
- True if signals were connected
-
- Example:
- class MyPlugin(BasePlugin):
- def initialize(self):
- # Connect task signals for UI updates
- self.connect_task_signals(
- on_completed=self._on_task_done,
- on_failed=self._on_task_error
- )
-
- def _on_task_done(self, task_id, result):
- self.status_label.setText(f"Task {task_id}: Done!")
-
- def _on_task_error(self, task_id, error):
- self.status_label.setText(f"Task {task_id} failed: {error}")
- """
- if not self.api:
- return False
-
- connected = False
-
- if on_completed:
- connected = self.api.connect_task_signal('completed', on_completed) or connected
- if on_failed:
- connected = self.api.connect_task_signal('failed', on_failed) or connected
- if on_started:
- connected = self.api.connect_task_signal('started', on_started) or connected
- if on_cancelled:
- connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected
-
- return connected
-
- # ========== Nexus API Methods ==========
-
- def nexus_search(self, query: str, entity_type: str = "items", limit: int = 20) -> List[Dict[str, Any]]:
- """Search for entities via Entropia Nexus API.
-
- Args:
- query: Search query string
- entity_type: Type of entity to search. Valid types:
- - items, weapons, armors
- - mobs, pets
- - blueprints, materials
- - locations, teleporters, shops, planets, areas
- - skills
- - enhancers, medicaltools, finders, excavators, refiners
- - vehicles, decorations, furniture
- - storagecontainers, strongboxes, vendors
- limit: Maximum number of results (default: 20, max: 100)
-
- Returns:
- List of search result dictionaries
-
- Example:
- # Search for weapons
- results = self.nexus_search("ArMatrix", entity_type="weapons")
-
- # Search for mobs
- mobs = self.nexus_search("Atrox", entity_type="mobs")
-
- # Search for locations
- locations = self.nexus_search("Fort", entity_type="locations")
-
- # Process results
- for item in results:
- print(f"{item['name']} ({item['type']})")
- """
- if not self.api:
- return []
-
- return self.api.nexus_search(query, entity_type, limit)
-
- def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]:
- """Get detailed information about a specific item.
-
- Args:
- item_id: The item's unique identifier (e.g., "armatrix_lp-35")
-
- Returns:
- Dictionary with item details, or None if not found
-
- Example:
- details = self.nexus_get_item_details("armatrix_lp-35")
- if details:
- print(f"Name: {details['name']}")
- print(f"TT Value: {details['tt_value']} PED")
- print(f"Damage: {details.get('damage', 'N/A')}")
- print(f"Range: {details.get('range', 'N/A')}m")
- """
- if not self.api:
- return None
-
- return self.api.nexus_get_item_details(item_id)
-
- def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]:
- """Get market data for a specific item.
-
- Args:
- item_id: The item's unique identifier
-
- Returns:
- Dictionary with market data, or None if not found
-
- Example:
- market = self.nexus_get_market_data("armatrix_lp-35")
- if market:
- print(f"Current markup: {market['current_markup']:.1f}%")
- print(f"7-day avg: {market['avg_markup_7d']:.1f}%")
- print(f"24h Volume: {market['volume_24h']}")
-
- # Check orders
- for buy in market.get('buy_orders', [])[:5]:
- print(f"Buy: {buy['price']} PED x {buy['quantity']}")
- """
- if not self.api:
- return None
-
- return self.api.nexus_get_market_data(item_id)
-
- def nexus_is_available(self) -> bool:
- """Check if Nexus API is available.
-
- Returns:
- True if Nexus API service is ready
- """
- if not self.api:
- return False
-
- return self.api.nexus_is_available()
-
- # ========== HTTP Client Methods ==========
-
- def http_get(self, url: str, cache_ttl: int = 300, headers: Dict[str, str] = None, **kwargs) -> Dict[str, Any]:
- """Make an HTTP GET request with caching.
-
- Args:
- url: The URL to fetch
- cache_ttl: Cache TTL in seconds (default: 300 = 5 minutes)
- headers: Additional headers
- **kwargs: Additional arguments
-
- Returns:
- Dict with 'status_code', 'headers', 'content', 'text', 'json', 'from_cache'
-
- Example:
- response = self.http_get(
- "https://api.example.com/data",
- cache_ttl=600,
- headers={'Accept': 'application/json'}
- )
- if response['status_code'] == 200:
- data = response['json']
- """
- if not self.api:
- raise RuntimeError("API not available")
-
- # Get HTTP client from services
- http_client = self.api.services.get('http')
- if not http_client:
- raise RuntimeError("HTTP client not available")
-
- return http_client.get(url, cache_ttl=cache_ttl, headers=headers, **kwargs)
-
- # ========== DataStore Methods ==========
-
- def save_data(self, key: str, data: Any) -> bool:
- """Save data to persistent storage.
-
- Data is automatically scoped to this plugin and survives app restarts.
-
- Args:
- key: Key to store data under
- data: Data to store (must be JSON serializable)
-
- Returns:
- True if saved successfully
-
- Example:
- # Save plugin settings
- self.save_data("settings", {"theme": "dark", "volume": 0.8})
-
- # Save user progress
- self.save_data("total_loot", {"ped": 150.50, "items": 42})
- """
- if not self.api:
- return False
-
- data_store = self.api.services.get('data_store')
- if not data_store:
- print(f"[{self.name}] DataStore not available")
- return False
-
- return data_store.save(self._plugin_id, key, data)
-
- def load_data(self, key: str, default: Any = None) -> Any:
- """Load data from persistent storage.
-
- Args:
- key: Key to load data from
- default: Default value if key doesn't exist
-
- Returns:
- Stored data or default value
-
- Example:
- # Load settings with defaults
- settings = self.load_data("settings", {"theme": "light", "volume": 1.0})
-
- # Load progress
- progress = self.load_data("total_loot", {"ped": 0, "items": 0})
- print(f"Total loot: {progress['ped']} PED")
- """
- if not self.api:
- return default
-
- data_store = self.api.services.get('data_store')
- if not data_store:
- return default
-
- return data_store.load(self._plugin_id, key, default)
-
- def delete_data(self, key: str) -> bool:
- """Delete data from persistent storage.
-
- Args:
- key: Key to delete
-
- Returns:
- True if deleted (or didn't exist), False on error
- """
- if not self.api:
- return False
-
- data_store = self.api.services.get('data_store')
- if not data_store:
- return False
-
- return data_store.delete(self._plugin_id, key)
-
- def get_all_data_keys(self) -> List[str]:
- """Get all data keys stored by this plugin.
-
- Returns:
- List of key names
- """
- if not self.api:
- return []
-
- data_store = self.api.services.get('data_store')
- if not data_store:
- return []
-
- return data_store.get_all_keys(self._plugin_id)
-
- # ========== Window Manager Methods ==========
-
- def get_eu_window(self) -> Optional[Dict[str, Any]]:
- """Get information about the Entropia Universe game window.
-
- Returns:
- Dict with window info or None if not found:
- - handle: Window handle (int)
- - title: Window title (str)
- - rect: (left, top, right, bottom) tuple
- - width: Window width (int)
- - height: Window height (int)
- - visible: Whether window is visible (bool)
-
- Example:
- window = self.get_eu_window()
- if window:
- print(f"EU window: {window['width']}x{window['height']}")
- print(f"Position: {window['rect']}")
- """
- if not self.api:
- return None
-
- return self.api.get_eu_window()
-
- def is_eu_focused(self) -> bool:
- """Check if Entropia Universe window is currently focused.
-
- Returns:
- True if EU is the active window
-
- Example:
- if self.is_eu_focused():
- # Safe to capture screenshot
- screenshot = self.capture_screen()
- """
- if not self.api:
- return False
-
- return self.api.is_eu_focused()
-
- def is_eu_visible(self) -> bool:
- """Check if Entropia Universe window is visible.
-
- Returns:
- True if EU window is visible (not minimized)
- """
- if not self.api:
- return False
-
- return self.api.is_eu_visible()
-
- def bring_eu_to_front(self) -> bool:
- """Bring Entropia Universe window to front and focus it.
-
- Returns:
- True if successful
- """
- if not self.api:
- return False
-
- return self.api.bring_eu_to_front()
-
- # ========== Clipboard Methods ==========
-
- def copy_to_clipboard(self, text: str) -> bool:
- """Copy text to system clipboard.
-
- Args:
- text: Text to copy
-
- Returns:
- True if successful
-
- Example:
- # Copy coordinates
- self.copy_to_clipboard("12345, 67890")
-
- # Copy calculation result
- result = self.calculate_dpp(50, 100, 2.5)
- self.copy_to_clipboard(f"DPP: {result:.2f}")
- """
- if not self.api:
- return False
-
- return self.api.copy_to_clipboard(text)
-
- def paste_from_clipboard(self) -> str:
- """Paste text from system clipboard.
-
- Returns:
- Clipboard content or empty string
-
- Example:
- # Get pasted coordinates
- coords = self.paste_from_clipboard()
- if coords:
- x, y = map(int, coords.split(","))
- """
- if not self.api:
- return ""
-
- return self.api.paste_from_clipboard()
-
- def get_clipboard_history(self, limit: int = 10) -> List[Dict[str, str]]:
- """Get recent clipboard history.
-
- Args:
- limit: Maximum number of entries to return
-
- Returns:
- List of clipboard entries with 'text', 'timestamp', 'source'
- """
- if not self.api:
- return []
-
- return self.api.get_clipboard_history(limit)
-
- # ========== Notification Methods ==========
-
- def notify(self, title: str, message: str, notification_type: str = 'info', sound: bool = False, duration_ms: int = 5000) -> str:
- """Show a toast notification.
-
- Args:
- title: Notification title
- message: Notification message
- notification_type: 'info', 'warning', 'error', or 'success'
- sound: Play notification sound
- duration_ms: How long to show notification (default: 5000ms)
-
- Returns:
- Notification ID
-
- Example:
- # Info notification
- self.notify("Session Started", "Tracking loot...")
-
- # Success with sound
- self.notify("Global!", "You received 150 PED", notification_type='success', sound=True)
-
- # Warning
- self.notify("Low Ammo", "Only 100 shots remaining", notification_type='warning')
-
- # Error
- self.notify("Connection Failed", "Check your internet", notification_type='error', sound=True)
- """
- if not self.api:
- return ""
-
- return self.api.notify(title, message, notification_type, sound, duration_ms)
-
- def notify_info(self, title: str, message: str, sound: bool = False) -> str:
- """Show info notification (convenience method)."""
- return self.notify(title, message, 'info', sound)
-
- def notify_success(self, title: str, message: str, sound: bool = False) -> str:
- """Show success notification (convenience method)."""
- return self.notify(title, message, 'success', sound)
-
- def notify_warning(self, title: str, message: str, sound: bool = False) -> str:
- """Show warning notification (convenience method)."""
- return self.notify(title, message, 'warning', sound)
-
- def notify_error(self, title: str, message: str, sound: bool = True) -> str:
- """Show error notification (convenience method)."""
- return self.notify(title, message, 'error', sound)
-
- def close_notification(self, notification_id: str) -> bool:
- """Close a specific notification.
-
- Args:
- notification_id: ID returned by notify()
-
- Returns:
- True if closed
- """
- if not self.api:
- return False
-
- return self.api.close_notification(notification_id)
-
- def close_all_notifications(self) -> None:
- """Close all visible notifications."""
- if not self.api:
- return
-
- self.api.close_all_notifications()
-
- # ========== Settings Methods ==========
-
- def get_setting(self, key: str, default: Any = None) -> Any:
- """Get a global EU-Utility setting.
-
- These are user preferences that apply across all plugins.
-
- Args:
- key: Setting key
- default: Default value if not set
-
- Returns:
- Setting value
-
- Available settings:
- - theme: 'dark', 'light', or 'auto'
- - overlay_opacity: float 0.0-1.0
- - icon_size: 'small', 'medium', 'large'
- - minimize_to_tray: bool
- - show_tooltips: bool
- - global_hotkeys: Dict of hotkey mappings
- """
- if not self.api:
- return default
-
- settings = self.api.services.get('settings')
- if not settings:
- return default
-
- return settings.get(key, default)
-
- def set_setting(self, key: str, value: Any) -> bool:
- """Set a global EU-Utility setting.
-
- Args:
- key: Setting key
- value: Value to set
-
- Returns:
- True if saved
- """
- if not self.api:
- return False
-
- settings = self.api.services.get('settings')
- if not settings:
- return False
-
- return settings.set(key, value)
-
- # ========== Logging Methods ==========
-
- def log_debug(self, message: str) -> None:
- """Log debug message (development only)."""
- print(f"[DEBUG][{self.name}] {message}")
-
- def log_info(self, message: str) -> None:
- """Log info message."""
- print(f"[INFO][{self.name}] {message}")
-
- def log_warning(self, message: str) -> None:
- """Log warning message."""
- print(f"[WARNING][{self.name}] {message}")
-
- def log_error(self, message: str) -> None:
- """Log error message."""
- print(f"[ERROR][{self.name}] {message}")
diff --git a/plugins/calculator/__init__.py b/plugins/calculator/__init__.py
deleted file mode 100644
index b5ba8bf..0000000
--- a/plugins/calculator/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Calculator Plugin for EU-Utility
-"""
-
-from .plugin import CalculatorPlugin
-
-__all__ = ["CalculatorPlugin"]
diff --git a/plugins/calculator/plugin.py b/plugins/calculator/plugin.py
deleted file mode 100644
index 2514914..0000000
--- a/plugins/calculator/plugin.py
+++ /dev/null
@@ -1,386 +0,0 @@
-"""
-EU-Utility - Calculator Plugin
-
-Standard calculator with Windows-style layout.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout,
- QLineEdit, QPushButton, QLabel, QGridLayout
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class CalculatorPlugin(BasePlugin):
- """Standard calculator with Windows-style layout."""
-
- name = "Calculator"
- version = "1.1.0"
- author = "ImpulsiveFPS"
- description = "Standard calculator"
- hotkey = "ctrl+shift+c"
-
- def initialize(self):
- """Setup calculator."""
- self.current_value = "0"
- self.stored_value = None
- self.pending_op = None
- self.memory = 0
- self.start_new = True
-
- def get_ui(self):
- """Create calculator UI with Windows layout."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(2)
-
- # Title
- title = QLabel("๐งฎ Calculator")
- title.setStyleSheet("color: #4a9eff; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Display
- self.display = QLineEdit("0")
- self.display.setAlignment(Qt.AlignmentFlag.AlignRight)
- self.display.setStyleSheet("""
- QLineEdit {
- background-color: #1a1a1a;
- color: white;
- font-size: 32px;
- font-family: 'Segoe UI', Arial;
- padding: 15px;
- border: none;
- border-radius: 4px;
- }
- """)
- self.display.setReadOnly(True)
- layout.addWidget(self.display)
-
- # Button grid - Windows Calculator Layout
- grid = QGridLayout()
- grid.setSpacing(2)
-
- # Row 1: Memory buttons
- mem_buttons = ['MC', 'MR', 'M+', 'M-', 'MS', 'M~']
- for i, btn_text in enumerate(mem_buttons):
- btn = self._create_button(btn_text, "#3a3a3a")
- btn.clicked.connect(lambda checked, t=btn_text: self._on_memory(t))
- grid.addWidget(btn, 0, i)
-
- # Row 2: %, CE, C, โซ
- row2 = ['%', 'CE', 'C', 'โซ']
- for i, btn_text in enumerate(row2):
- btn = self._create_button(btn_text, "#3a3a3a")
- btn.clicked.connect(lambda checked, t=btn_text: self._on_special(t))
- grid.addWidget(btn, 1, i)
-
- # Row 3: ยน/โ, xยฒ, ยฒโx, รท
- row3 = [('ยน/โ', '1/x'), ('xยฒ', 'sq'), ('ยฒโx', 'sqrt'), 'รท']
- for i, item in enumerate(row3):
- if isinstance(item, tuple):
- text, op = item
- else:
- text = op = item
- btn = self._create_button(text, "#3a3a3a")
- btn.clicked.connect(lambda checked, o=op: self._on_operator(o))
- grid.addWidget(btn, 2, i)
-
- # Row 4: 7, 8, 9, ร
- row4 = ['7', '8', '9', 'ร']
- for i, btn_text in enumerate(row4):
- btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['ร'])
- if btn_text == 'ร':
- btn.clicked.connect(lambda checked: self._on_operator('*'))
- else:
- btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t))
- grid.addWidget(btn, 3, i)
-
- # Row 5: 4, 5, 6, -
- row5 = ['4', '5', '6', '-']
- for i, btn_text in enumerate(row5):
- btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['-'])
- if btn_text == '-':
- btn.clicked.connect(lambda checked: self._on_operator('-'))
- else:
- btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t))
- grid.addWidget(btn, 4, i)
-
- # Row 6: 1, 2, 3, +
- row6 = ['1', '2', '3', '+']
- for i, btn_text in enumerate(row6):
- btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['+'])
- if btn_text == '+':
- btn.clicked.connect(lambda checked: self._on_operator('+'))
- else:
- btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t))
- grid.addWidget(btn, 5, i)
-
- # Row 7: +/-, 0, ., =
- row7 = [('ยฑ', '+/-'), '0', '.', '=']
- for i, item in enumerate(row7):
- if isinstance(item, tuple):
- text, val = item
- else:
- text = val = item
-
- if val == '=':
- btn = self._create_button(text, "#0078d4", text_color="white") # Blue equals
- else:
- btn = self._create_button(text, "#2a2a2a", is_number=True)
-
- if val == '+/-':
- btn.clicked.connect(self._on_negate)
- elif val == '.':
- btn.clicked.connect(self._on_decimal)
- elif val == '=':
- btn.clicked.connect(self._on_equals)
- else:
- btn.clicked.connect(lambda checked, t=val: self._on_number(t))
-
- grid.addWidget(btn, 6, i)
-
- # Set column stretch
- for i in range(4):
- grid.setColumnStretch(i, 1)
-
- # Set row stretch
- for i in range(7):
- grid.setRowStretch(i, 1)
-
- layout.addLayout(grid)
- layout.addStretch()
-
- return widget
-
- def _create_button(self, text, bg_color, is_number=False, text_color="#ffffff"):
- """Create a calculator button."""
- btn = QPushButton(text)
- btn.setMinimumSize(60, 45)
-
- if is_number:
- font_size = "18px"
- font_weight = "normal"
- else:
- font_size = "14px"
- font_weight = "normal"
-
- btn.setStyleSheet(f"""
- QPushButton {{
- background-color: {bg_color};
- color: {text_color};
- font-size: {font_size};
- font-weight: {font_weight};
- border: none;
- border-radius: 4px;
- }}
- QPushButton:hover {{
- background-color: {self._lighten(bg_color)};
- }}
- QPushButton:pressed {{
- background-color: {self._darken(bg_color)};
- }}
- """)
-
- return btn
-
- def _lighten(self, color):
- """Lighten a hex color slightly."""
- # Simple approximation - increase each component
- if color == "#2a2a2a":
- return "#3a3a3a"
- elif color == "#3a3a3a":
- return "#4a4a4a"
- elif color == "#0078d4":
- return "#1084e0"
- return color
-
- def _darken(self, color):
- """Darken a hex color slightly."""
- if color == "#2a2a2a":
- return "#1a1a1a"
- elif color == "#3a3a3a":
- return "#2a2a2a"
- elif color == "#0078d4":
- return "#006cbd"
- return color
-
- def _on_number(self, num):
- """Handle number button press."""
- if self.start_new:
- self.current_value = num
- self.start_new = False
- else:
- if self.current_value == "0":
- self.current_value = num
- else:
- self.current_value += num
- self._update_display()
-
- def _on_decimal(self):
- """Handle decimal point."""
- if self.start_new:
- self.current_value = "0."
- self.start_new = False
- elif "." not in self.current_value:
- self.current_value += "."
- self._update_display()
-
- def _on_operator(self, op):
- """Handle operator button."""
- try:
- current = float(self.current_value)
-
- if op == "1/x":
- result = 1 / current
- self.current_value = self._format_result(result)
- self.start_new = True
- elif op == "sq":
- result = current ** 2
- self.current_value = self._format_result(result)
- self.start_new = True
- elif op == "sqrt":
- import math
- result = math.sqrt(current)
- self.current_value = self._format_result(result)
- self.start_new = True
- else:
- # Binary operators
- if self.pending_op and not self.start_new:
- self._calculate()
-
- self.stored_value = float(self.current_value)
- self.pending_op = op
- self.start_new = True
-
- self._update_display()
- except Exception:
- self.current_value = "Error"
- self._update_display()
- self.start_new = True
-
- def _on_special(self, op):
- """Handle special buttons (%, CE, C, backspace)."""
- if op == 'C':
- # Clear all
- self.current_value = "0"
- self.stored_value = None
- self.pending_op = None
- self.start_new = True
- elif op == 'CE':
- # Clear entry
- self.current_value = "0"
- self.start_new = True
- elif op == 'โซ':
- # Backspace
- if len(self.current_value) > 1:
- self.current_value = self.current_value[:-1]
- else:
- self.current_value = "0"
- elif op == '%':
- # Percent
- try:
- result = float(self.current_value) / 100
- self.current_value = self._format_result(result)
- self.start_new = True
- except:
- self.current_value = "Error"
- self.start_new = True
-
- self._update_display()
-
- def _on_memory(self, op):
- """Handle memory operations."""
- try:
- current = float(self.current_value)
-
- if op == 'MC':
- self.memory = 0
- elif op == 'MR':
- self.current_value = self._format_result(self.memory)
- self.start_new = True
- elif op == 'M+':
- self.memory += current
- elif op == 'M-':
- self.memory -= current
- elif op == 'MS':
- self.memory = current
- elif op == 'M~':
- # Memory clear (same as MC)
- self.memory = 0
-
- self._update_display()
- except:
- pass
-
- def _on_negate(self):
- """Toggle sign."""
- try:
- current = float(self.current_value)
- result = -current
- self.current_value = self._format_result(result)
- self._update_display()
- except:
- pass
-
- def _on_equals(self):
- """Calculate result."""
- self._calculate()
- self.pending_op = None
- self.stored_value = None
- self.start_new = True
-
- def _calculate(self):
- """Perform pending calculation."""
- if self.pending_op and self.stored_value is not None:
- try:
- current = float(self.current_value)
-
- if self.pending_op == '+':
- result = self.stored_value + current
- elif self.pending_op == '-':
- result = self.stored_value - current
- elif self.pending_op == '*':
- result = self.stored_value * current
- elif self.pending_op == 'รท':
- if current != 0:
- result = self.stored_value / current
- else:
- result = "Error"
- else:
- return
-
- self.current_value = self._format_result(result)
- self._update_display()
- except:
- self.current_value = "Error"
- self._update_display()
-
- def _format_result(self, result):
- """Format calculation result."""
- if isinstance(result, str):
- return result
-
- # Check if it's essentially an integer
- if result == int(result):
- return str(int(result))
-
- # Format with reasonable precision
- formatted = f"{result:.10f}"
- # Remove trailing zeros
- formatted = formatted.rstrip('0').rstrip('.')
-
- # Limit length
- if len(formatted) > 12:
- formatted = f"{result:.6e}"
-
- return formatted
-
- def _update_display(self):
- """Update the display."""
- self.display.setText(self.current_value)
-
- def on_hotkey(self):
- """Focus calculator when hotkey pressed."""
- pass
diff --git a/plugins/chat_logger/__init__.py b/plugins/chat_logger/__init__.py
deleted file mode 100644
index 5e50812..0000000
--- a/plugins/chat_logger/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Chat Logger Plugin
-"""
-
-from .plugin import ChatLoggerPlugin
-
-__all__ = ["ChatLoggerPlugin"]
diff --git a/plugins/chat_logger/plugin.py b/plugins/chat_logger/plugin.py
deleted file mode 100644
index 27278f6..0000000
--- a/plugins/chat_logger/plugin.py
+++ /dev/null
@@ -1,285 +0,0 @@
-"""
-EU-Utility - Chat Logger Plugin
-
-Log and search chat messages with filters.
-"""
-
-import re
-import json
-from datetime import datetime, timedelta
-from pathlib import Path
-from collections import deque
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTextEdit, QLineEdit, QComboBox,
- QCheckBox, QFrame
-)
-from PyQt6.QtCore import Qt, QTimer
-
-from plugins.base_plugin import BasePlugin
-from core.icon_manager import get_icon_manager
-
-
-class ChatLoggerPlugin(BasePlugin):
- """Log and search chat messages."""
-
- name = "Chat Logger"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Log, search, and filter chat messages"
- hotkey = "ctrl+shift+t" # T for chaT
-
- # Chat channels
- CHANNELS = {
- 'main': 'Main',
- 'society': 'Society',
- 'team': 'Team',
- 'local': 'Local',
- 'global': 'Global',
- 'trade': 'Trade',
- 'private': 'Private',
- }
-
- def initialize(self):
- """Setup chat logger."""
- self.data_file = Path("data/chat_log.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- # Keep last 10000 messages in memory
- self.messages = deque(maxlen=10000)
- self.filters = {
- 'show_main': True,
- 'show_society': True,
- 'show_team': True,
- 'show_local': True,
- 'show_global': True,
- 'show_trade': True,
- 'show_private': True,
- 'search_text': '',
- 'show_globals_only': False,
- 'show_loot': False,
- }
-
- self._load_recent()
-
- def _load_recent(self):
- """Load recent messages."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.messages.extend(data.get('messages', [])[-1000:])
- except:
- pass
-
- def _save_messages(self):
- """Save messages to file."""
- # Keep last 24 hours
- cutoff = (datetime.now() - timedelta(hours=24)).isoformat()
- recent = [m for m in self.messages if m['time'] > cutoff]
-
- with open(self.data_file, 'w') as f:
- json.dump({'messages': recent}, f)
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(10)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Get icon manager
- icon_mgr = get_icon_manager()
-
- # Title with icon
- title_layout = QHBoxLayout()
-
- title_icon = QLabel()
- icon_pixmap = icon_mgr.get_pixmap('message-square', size=20)
- title_icon.setPixmap(icon_pixmap)
- title_icon.setFixedSize(20, 20)
- title_layout.addWidget(title_icon)
-
- title = QLabel("Chat Logger")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- title_layout.addWidget(title)
- title_layout.addStretch()
-
- layout.addLayout(title_layout)
-
- # Search bar
- search_layout = QHBoxLayout()
-
- self.search_input = QLineEdit()
- self.search_input.setPlaceholderText("Search messages...")
- self.search_input.setStyleSheet("""
- QLineEdit {
- background-color: rgba(255, 255, 255, 15);
- color: white;
- border: 1px solid rgba(255, 255, 255, 30);
- border-radius: 6px;
- padding: 8px;
- }
- """)
- self.search_input.textChanged.connect(self._update_filter)
- search_layout.addWidget(self.search_input)
-
- search_btn = QPushButton("๐")
- search_btn.setFixedSize(32, 32)
- search_btn.setStyleSheet("""
- QPushButton {
- background-color: rgba(255, 255, 255, 15);
- border: none;
- border-radius: 6px;
- font-size: 14px;
- }
- QPushButton:hover {
- background-color: rgba(255, 255, 255, 30);
- }
- """)
- search_layout.addWidget(search_btn)
-
- layout.addLayout(search_layout)
-
- # Filters
- filters_frame = QFrame()
- filters_frame.setStyleSheet("""
- QFrame {
- background-color: rgba(0, 0, 0, 50);
- border-radius: 8px;
- border: 1px solid rgba(255, 255, 255, 20);
- }
- """)
- filters_layout = QHBoxLayout(filters_frame)
- filters_layout.setContentsMargins(10, 6, 10, 6)
-
- # Channel filters
- self.filter_checks = {}
- for channel_id, channel_name in self.CHANNELS.items():
- cb = QCheckBox(channel_name)
- cb.setChecked(True)
- cb.setStyleSheet("color: rgba(255, 255, 255, 180); font-size: 10px;")
- cb.stateChanged.connect(self._update_filter)
- self.filter_checks[channel_id] = cb
- filters_layout.addWidget(cb)
-
- filters_layout.addStretch()
- layout.addWidget(filters_frame)
-
- # Chat display
- self.chat_display = QTextEdit()
- self.chat_display.setReadOnly(True)
- self.chat_display.setStyleSheet("""
- QTextEdit {
- background-color: rgba(20, 25, 35, 150);
- color: white;
- border: 1px solid rgba(255, 255, 255, 20);
- border-radius: 8px;
- padding: 10px;
- font-family: Consolas, monospace;
- font-size: 11px;
- }
- """)
- layout.addWidget(self.chat_display)
-
- # Stats
- self.stats_label = QLabel("Messages: 0")
- self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;")
- layout.addWidget(self.stats_label)
-
- # Refresh display
- self._refresh_display()
-
- return widget
-
- def _update_filter(self):
- """Update filter settings."""
- self.filters['search_text'] = self.search_input.text().lower()
-
- for channel_id, cb in self.filter_checks.items():
- self.filters[f'show_{channel_id}'] = cb.isChecked()
-
- self._refresh_display()
-
- def _refresh_display(self):
- """Refresh chat display."""
- html = []
-
- for msg in reversed(self.messages):
- # Apply filters
- channel = msg.get('channel', 'main')
- if not self.filters.get(f'show_{channel}', True):
- continue
-
- text = msg.get('text', '')
- if self.filters['search_text']:
- if self.filters['search_text'] not in text.lower():
- continue
-
- # Format message
- time_str = msg['time'][11:16] if msg['time'] else '--:--'
- author = msg.get('author', 'Unknown')
-
- # Color by channel
- colors = {
- 'main': '#ffffff',
- 'society': '#9c27b0',
- 'team': '#4caf50',
- 'local': '#ffc107',
- 'global': '#f44336',
- 'trade': '#ff9800',
- 'private': '#00bcd4',
- }
- color = colors.get(channel, '#ffffff')
-
- html.append(f'''
-
- [{time_str}]
- {author}:
- {text}
-
- ''')
-
- self.chat_display.setHtml(''.join(html[:100])) # Show last 100
- self.stats_label.setText(f"Messages: {len(self.messages)}")
-
- def parse_chat_message(self, message, channel='main', author='Unknown'):
- """Parse and log chat message."""
- entry = {
- 'time': datetime.now().isoformat(),
- 'channel': channel,
- 'author': author,
- 'text': message,
- }
-
- self.messages.append(entry)
- self._refresh_display()
-
- # Auto-save periodically
- if len(self.messages) % 100 == 0:
- self._save_messages()
-
- def search(self, query):
- """Search chat history."""
- results = []
- query_lower = query.lower()
-
- for msg in self.messages:
- if query_lower in msg.get('text', '').lower():
- results.append(msg)
-
- return results
-
- def get_globals(self):
- """Get global messages."""
- return [m for m in self.messages if m.get('channel') == 'global']
-
- def get_loot_messages(self):
- """Get loot-related messages."""
- loot_keywords = ['received', 'loot', 'item', 'ped']
- return [
- m for m in self.messages
- if any(kw in m.get('text', '').lower() for kw in loot_keywords)
- ]
diff --git a/plugins/codex_tracker/__init__.py b/plugins/codex_tracker/__init__.py
deleted file mode 100644
index dcba8fd..0000000
--- a/plugins/codex_tracker/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Codex Tracker Plugin
-"""
-
-from .plugin import CodexTrackerPlugin
-
-__all__ = ["CodexTrackerPlugin"]
diff --git a/plugins/codex_tracker/plugin.py b/plugins/codex_tracker/plugin.py
deleted file mode 100644
index e36aed2..0000000
--- a/plugins/codex_tracker/plugin.py
+++ /dev/null
@@ -1,218 +0,0 @@
-"""
-EU-Utility - Codex Tracker Plugin
-
-Track creature challenge progress from Codex.
-"""
-
-import json
-from pathlib import Path
-from datetime import datetime
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QProgressBar, QTableWidget, QTableWidgetItem,
- QComboBox, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class CodexTrackerPlugin(BasePlugin):
- """Track creature codex progress."""
-
- name = "Codex Tracker"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track creature challenges and codex progress"
- hotkey = "ctrl+shift+x"
-
- # Arkadia creatures from screenshot
- CREATURES = [
- {"name": "Bokol", "rank": 22, "progress": 49.5},
- {"name": "Nusul", "rank": 15, "progress": 24.8},
- {"name": "Wombana", "rank": 10, "progress": 45.2},
- {"name": "Arkadian Hornet", "rank": 1, "progress": 15.0},
- {"name": "Feran", "rank": 4, "progress": 86.0},
- {"name": "Hadraada", "rank": 6, "progress": 0.4},
- {"name": "Halix", "rank": 14, "progress": 0.2},
- {"name": "Huon", "rank": 1, "progress": 45.8},
- {"name": "Kadra", "rank": 2, "progress": 0.7},
- {"name": "Magurg", "rank": 1, "progress": 8.7},
- {"name": "Monura", "rank": 1, "progress": 5.2},
- ]
-
- def initialize(self):
- """Setup codex tracker."""
- self.data_file = Path("data/codex.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.creatures = self.CREATURES.copy()
- self.scanned_data = {}
-
- self._load_data()
-
- def _load_data(self):
- """Load codex data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.creatures = data.get('creatures', self.CREATURES)
- except:
- pass
-
- def _save_data(self):
- """Save codex data."""
- with open(self.data_file, 'w') as f:
- json.dump({'creatures': self.creatures}, f, indent=2)
-
- def get_ui(self):
- """Create codex tracker UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("๐ Codex Tracker")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Summary
- summary = QHBoxLayout()
-
- total_creatures = len(self.creatures)
- completed = sum(1 for c in self.creatures if c.get('progress', 0) >= 100)
-
- self.total_label = QLabel(f"Creatures: {total_creatures}")
- self.total_label.setStyleSheet("color: #4a9eff; font-size: 13px;")
- summary.addWidget(self.total_label)
-
- self.completed_label = QLabel(f"Completed: {completed}")
- self.completed_label.setStyleSheet("color: #4caf50; font-size: 13px;")
- summary.addWidget(self.completed_label)
-
- summary.addStretch()
- layout.addLayout(summary)
-
- # Scan button
- scan_btn = QPushButton("Scan Codex Window")
- scan_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #ffa060;
- }
- """)
- scan_btn.clicked.connect(self._scan_codex)
- layout.addWidget(scan_btn)
-
- # Creatures list
- self.creatures_table = QTableWidget()
- self.creatures_table.setColumnCount(4)
- self.creatures_table.setHorizontalHeaderLabels(["Creature", "Rank", "Progress", "Next Rank"])
- self.creatures_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 8px;
- border: none;
- font-weight: bold;
- font-size: 11px;
- }
- """)
- self.creatures_table.horizontalHeader().setStretchLastSection(True)
- layout.addWidget(self.creatures_table)
-
- self._refresh_table()
-
- layout.addStretch()
- return widget
-
- def _refresh_table(self):
- """Refresh creatures table."""
- self.creatures_table.setRowCount(len(self.creatures))
-
- for i, creature in enumerate(sorted(self.creatures, key=lambda x: -x.get('progress', 0))):
- # Name
- name_item = QTableWidgetItem(creature.get('name', 'Unknown'))
- name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.creatures_table.setItem(i, 0, name_item)
-
- # Rank
- rank = creature.get('rank', 0)
- rank_item = QTableWidgetItem(str(rank))
- rank_item.setFlags(rank_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- rank_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
- self.creatures_table.setItem(i, 1, rank_item)
-
- # Progress bar
- progress = creature.get('progress', 0)
- progress_widget = QWidget()
- progress_layout = QHBoxLayout(progress_widget)
- progress_layout.setContentsMargins(5, 2, 5, 2)
-
- bar = QProgressBar()
- bar.setValue(int(progress))
- bar.setTextVisible(True)
- bar.setFormat(f"{progress:.1f}%")
- bar.setStyleSheet("""
- QProgressBar {
- background-color: rgba(60, 70, 90, 150);
- border: none;
- border-radius: 3px;
- text-align: center;
- color: white;
- font-size: 10px;
- }
- QProgressBar::chunk {
- background-color: #ff8c42;
- border-radius: 3px;
- }
- """)
- progress_layout.addWidget(bar)
-
- self.creatures_table.setCellWidget(i, 2, progress_widget)
-
- # Next rank (estimated kills needed)
- progress_item = QTableWidgetItem(f"~{int((100-progress) * 120)} kills")
- progress_item.setFlags(progress_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- progress_item.setForeground(Qt.GlobalColor.gray)
- self.creatures_table.setItem(i, 3, progress_item)
-
- def _scan_codex(self):
- """Scan codex window with OCR."""
- # TODO: Implement OCR scanning
- # For now, simulate update
- for creature in self.creatures:
- creature['progress'] = min(100, creature.get('progress', 0) + 0.5)
-
- self._save_data()
- self._refresh_table()
-
- def get_closest_to_rankup(self, count=3):
- """Get creatures closest to ranking up."""
- sorted_creatures = sorted(
- self.creatures,
- key=lambda x: -(x.get('progress', 0))
- )
- return [c for c in sorted_creatures[:count] if c.get('progress', 0) < 100]
-
- def get_recommended_hunt(self):
- """Get recommended creature to hunt."""
- close = self.get_closest_to_rankup(1)
- return close[0] if close else None
diff --git a/plugins/crafting_calc/__init__.py b/plugins/crafting_calc/__init__.py
deleted file mode 100644
index 371956a..0000000
--- a/plugins/crafting_calc/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Crafting Calculator Plugin
-"""
-
-from .plugin import CraftingCalculatorPlugin
-
-__all__ = ["CraftingCalculatorPlugin"]
diff --git a/plugins/crafting_calc/plugin.py b/plugins/crafting_calc/plugin.py
deleted file mode 100644
index bd79a00..0000000
--- a/plugins/crafting_calc/plugin.py
+++ /dev/null
@@ -1,219 +0,0 @@
-"""
-EU-Utility - Crafting Calculator Plugin
-
-Calculate crafting success rates and material costs.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QComboBox, QLineEdit, QTableWidget,
- QTableWidgetItem, QFrame, QGroupBox
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class CraftingCalculatorPlugin(BasePlugin):
- """Calculate crafting costs and success rates."""
-
- name = "Crafting Calc"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Crafting success rates and material costs"
- hotkey = "ctrl+shift+b" # B for Blueprint
-
- # Sample blueprints data
- BLUEPRINTS = {
- 'Weapon': [
- 'ArMatrix LP-35 (L)',
- 'ArMatrix BP-25 (L)',
- 'Omegaton M83 Predator',
- ],
- 'Armor': [
- 'Vigiator Harness (M)',
- 'Vigiator Thighs (M)',
- ],
- 'Tool': [
- 'Ziplex Z1 Seeker',
- 'Ziplex Z3 Seeker',
- ],
- 'Material': [
- 'Metal Residue',
- 'Energy Matter Residue',
- ],
- }
-
- def initialize(self):
- """Setup crafting calculator."""
- self.saved_recipes = []
-
- def get_ui(self):
- """Create crafting calculator UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("๐จ Crafting Calculator")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Blueprint selector
- bp_group = QGroupBox("Blueprint")
- bp_group.setStyleSheet(self._group_style())
- bp_layout = QVBoxLayout(bp_group)
-
- # Category
- cat_layout = QHBoxLayout()
- cat_layout.addWidget(QLabel("Category:"))
- self.cat_combo = QComboBox()
- self.cat_combo.addItems(list(self.BLUEPRINTS.keys()))
- self.cat_combo.currentTextChanged.connect(self._update_blueprints)
- cat_layout.addWidget(self.cat_combo)
- bp_layout.addLayout(cat_layout)
-
- # Blueprint
- bp_layout2 = QHBoxLayout()
- bp_layout2.addWidget(QLabel("Blueprint:"))
- self.bp_combo = QComboBox()
- self._update_blueprints(self.cat_combo.currentText())
- bp_layout2.addWidget(self.bp_combo)
- bp_layout.addLayout(bp_layout2)
-
- # QR
- qr_layout = QHBoxLayout()
- qr_layout.addWidget(QLabel("QR:"))
- self.qr_input = QLineEdit()
- self.qr_input.setPlaceholderText("1.0")
- self.qr_input.setText("1.0")
- qr_layout.addWidget(self.qr_input)
- bp_layout.addLayout(qr_layout)
-
- layout.addWidget(bp_group)
-
- # Materials
- mat_group = QGroupBox("Materials")
- mat_group.setStyleSheet(self._group_style())
- mat_layout = QVBoxLayout(mat_group)
-
- self.mat_table = QTableWidget()
- self.mat_table.setColumnCount(4)
- self.mat_table.setHorizontalHeaderLabels(["Material", "Needed", "Have", "Buy"])
- self.mat_table.setRowCount(3)
-
- sample_mats = [
- ("Lysterium Ingot", 50, 0),
- ("Oil", 30, 10),
- ("Meldar Paper", 10, 5),
- ]
-
- for i, (mat, needed, have) in enumerate(sample_mats):
- self.mat_table.setItem(i, 0, QTableWidgetItem(mat))
- self.mat_table.setItem(i, 1, QTableWidgetItem(str(needed)))
- self.mat_table.setItem(i, 2, QTableWidgetItem(str(have)))
- buy = needed - have if needed > have else 0
- self.mat_table.setItem(i, 3, QTableWidgetItem(str(buy)))
-
- self.mat_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: none;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 6px;
- font-size: 10px;
- }
- """)
- mat_layout.addWidget(self.mat_table)
-
- layout.addWidget(mat_group)
-
- # Calculator
- calc_group = QGroupBox("Calculator")
- calc_group.setStyleSheet(self._group_style())
- calc_layout = QVBoxLayout(calc_group)
-
- # Click calculator
- click_layout = QHBoxLayout()
- click_layout.addWidget(QLabel("Clicks:"))
- self.clicks_input = QLineEdit()
- self.clicks_input.setPlaceholderText("10")
- self.clicks_input.setText("10")
- click_layout.addWidget(self.clicks_input)
- calc_layout.addLayout(click_layout)
-
- calc_btn = QPushButton("Calculate")
- calc_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- calc_btn.clicked.connect(self._calculate)
- calc_layout.addWidget(calc_btn)
-
- # Results
- self.result_label = QLabel("Success Rate: ~45%")
- self.result_label.setStyleSheet("color: #4caf50; font-weight: bold;")
- calc_layout.addWidget(self.result_label)
-
- self.cost_label = QLabel("Estimated Cost: 15.50 PED")
- self.cost_label.setStyleSheet("color: #ffc107;")
- calc_layout.addWidget(self.cost_label)
-
- layout.addWidget(calc_group)
- layout.addStretch()
-
- return widget
-
- def _group_style(self):
- return """
- QGroupBox {
- color: rgba(255,255,255,200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- margin-top: 10px;
- font-weight: bold;
- }
- QGroupBox::title {
- subcontrol-origin: margin;
- left: 10px;
- padding: 0 5px;
- }
- """
-
- def _update_blueprints(self, category):
- """Update blueprint list."""
- self.bp_combo.clear()
- self.bp_combo.addItems(self.BLUEPRINTS.get(category, []))
-
- def _calculate(self):
- """Calculate crafting results."""
- try:
- qr = float(self.qr_input.text() or 1.0)
- clicks = int(self.clicks_input.text() or 10)
-
- # Simple formula (real one is more complex)
- base_rate = 0.45
- qr_bonus = (qr - 1.0) * 0.05
- success_rate = min(0.95, base_rate + qr_bonus)
-
- expected_success = int(clicks * success_rate)
-
- self.result_label.setText(
- f"Success Rate: ~{success_rate*100:.1f}% | "
- f"Expected: {expected_success}/{clicks}"
- )
-
- except Exception as e:
- self.result_label.setText(f"Error: {e}")
diff --git a/plugins/dashboard/__init__.py b/plugins/dashboard/__init__.py
deleted file mode 100644
index 448dc14..0000000
--- a/plugins/dashboard/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Dashboard Plugin
-"""
-
-from .plugin import DashboardPlugin
-
-__all__ = ["DashboardPlugin"]
diff --git a/plugins/dashboard/plugin.py b/plugins/dashboard/plugin.py
deleted file mode 100644
index ece6750..0000000
--- a/plugins/dashboard/plugin.py
+++ /dev/null
@@ -1,326 +0,0 @@
-"""
-EU-Utility - Dashboard Plugin with Customizable Widgets
-
-Customizable start page with avatar statistics.
-"""
-
-import json
-from pathlib import Path
-from datetime import datetime, timedelta
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QGridLayout, QFrame, QScrollArea,
- QSizePolicy, QCheckBox, QDialog, QListWidget,
- QListWidgetItem, QDialogButtonBox
-)
-from PyQt6.QtCore import Qt, QTimer
-from PyQt6.QtGui import QColor, QFont
-
-from core.eu_styles import EU_COLORS
-from plugins.base_plugin import BasePlugin
-
-
-class DashboardPlugin(BasePlugin):
- """Customizable dashboard with avatar statistics."""
-
- name = "Dashboard"
- version = "2.0.0"
- author = "ImpulsiveFPS"
- description = "Customizable start page with avatar stats"
- hotkey = "ctrl+shift+home"
-
- # Available widgets
- AVAILABLE_WIDGETS = {
- 'ped_balance': {'name': 'PED Balance', 'icon': 'dollar-sign', 'default': True},
- 'skill_count': {'name': 'Skills Tracked', 'icon': 'trending-up', 'default': True},
- 'inventory_items': {'name': 'Inventory Items', 'icon': 'archive', 'default': True},
- 'current_dpp': {'name': 'Current DPP', 'icon': 'crosshair', 'default': True},
- 'total_gains_today': {'name': "Today's Skill Gains", 'icon': 'zap', 'default': True},
- 'professions_count': {'name': 'Professions', 'icon': 'award', 'default': False},
- 'missions_active': {'name': 'Active Missions', 'icon': 'map', 'default': False},
- 'codex_progress': {'name': 'Codex Progress', 'icon': 'book', 'default': False},
- 'globals_hofs': {'name': 'Globals/HOFs', 'icon': 'package', 'default': False},
- 'play_time': {'name': 'Session Time', 'icon': 'clock', 'default': False},
- }
-
- def initialize(self):
- """Setup dashboard."""
- self.config_file = Path("data/dashboard_config.json")
- self.config_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.enabled_widgets = []
- self.widget_data = {}
-
- self._load_config()
- self._load_data()
-
- # Auto-refresh timer
- self.refresh_timer = QTimer()
- self.refresh_timer.timeout.connect(self._refresh_data)
- self.refresh_timer.start(5000) # Refresh every 5 seconds
-
- def _load_config(self):
- """Load widget configuration."""
- if self.config_file.exists():
- try:
- with open(self.config_file, 'r') as f:
- config = json.load(f)
- self.enabled_widgets = config.get('enabled', [])
- except:
- pass
-
- # Default: enable default widgets
- if not self.enabled_widgets:
- self.enabled_widgets = [
- k for k, v in self.AVAILABLE_WIDGETS.items() if v['default']
- ]
-
- def _save_config(self):
- """Save widget configuration."""
- with open(self.config_file, 'w') as f:
- json.dump({'enabled': self.enabled_widgets}, f)
-
- def _load_data(self):
- """Load data from other plugins."""
- # Try to get data from other plugin files
- data_dir = Path("data")
-
- # PED from inventory
- inv_file = data_dir / "inventory.json"
- if inv_file.exists():
- try:
- with open(inv_file, 'r') as f:
- data = json.load(f)
- items = data.get('items', [])
- total_tt = sum(item.get('tt', 0) for item in items)
- self.widget_data['ped_balance'] = total_tt
- except:
- self.widget_data['ped_balance'] = 0
-
- # Skills
- skills_file = data_dir / "skill_tracker.json"
- if skills_file.exists():
- try:
- with open(skills_file, 'r') as f:
- data = json.load(f)
- self.widget_data['skill_count'] = len(data.get('skills', {}))
- self.widget_data['total_gains_today'] = len([
- g for g in data.get('gains', [])
- if datetime.fromisoformat(g['time']).date() == datetime.now().date()
- ])
- except:
- self.widget_data['skill_count'] = 0
- self.widget_data['total_gains_today'] = 0
-
- # Inventory count
- if inv_file.exists():
- try:
- with open(inv_file, 'r') as f:
- data = json.load(f)
- self.widget_data['inventory_items'] = len(data.get('items', []))
- except:
- self.widget_data['inventory_items'] = 0
-
- # Professions
- prof_file = data_dir / "professions.json"
- if prof_file.exists():
- try:
- with open(prof_file, 'r') as f:
- data = json.load(f)
- self.widget_data['professions_count'] = len(data.get('professions', {}))
- except:
- self.widget_data['professions_count'] = 0
-
- def _refresh_data(self):
- """Refresh widget data."""
- self._load_data()
- if hasattr(self, 'widgets_container'):
- self._update_widgets()
-
- def get_ui(self):
- """Create dashboard UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Header with customize button
- header = QHBoxLayout()
-
- title = QLabel("Dashboard")
- title.setStyleSheet("font-size: 20px; font-weight: bold; color: white;")
- header.addWidget(title)
-
- header.addStretch()
-
- customize_btn = QPushButton("Customize")
- customize_btn.setStyleSheet(f"""
- QPushButton {{
- background-color: {EU_COLORS['bg_secondary']};
- color: {EU_COLORS['text_secondary']};
- border: 1px solid {EU_COLORS['border_default']};
- border-radius: 4px;
- padding: 8px 16px;
- }}
- QPushButton:hover {{
- background-color: {EU_COLORS['bg_hover']};
- border-color: {EU_COLORS['accent_orange']};
- }}
- """)
- customize_btn.clicked.connect(self._show_customize_dialog)
- header.addWidget(customize_btn)
-
- layout.addLayout(header)
-
- # Scroll area for widgets
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setFrameShape(QFrame.Shape.NoFrame)
- scroll.setStyleSheet("background: transparent; border: none;")
-
- self.widgets_container = QWidget()
- self.widgets_layout = QGridLayout(self.widgets_container)
- self.widgets_layout.setSpacing(15)
- self.widgets_layout.setContentsMargins(0, 0, 0, 0)
-
- self._update_widgets()
-
- scroll.setWidget(self.widgets_container)
- layout.addWidget(scroll)
-
- return widget
-
- def _update_widgets(self):
- """Update widget display."""
- # Clear existing
- while self.widgets_layout.count():
- item = self.widgets_layout.takeAt(0)
- if item.widget():
- item.widget().deleteLater()
-
- # Add enabled widgets
- col = 0
- row = 0
- for widget_id in self.enabled_widgets:
- if widget_id in self.AVAILABLE_WIDGETS:
- widget_info = self.AVAILABLE_WIDGETS[widget_id]
- card = self._create_widget_card(
- widget_id,
- widget_info['name'],
- widget_info['icon']
- )
- self.widgets_layout.addWidget(card, row, col)
-
- col += 1
- if col >= 2: # 2 columns
- col = 0
- row += 1
-
- def _create_widget_card(self, widget_id, name, icon_name):
- """Create a stat widget card."""
- card = QFrame()
- card.setStyleSheet(f"""
- QFrame {{
- background-color: {EU_COLORS['bg_secondary']};
- border: 1px solid {EU_COLORS['border_default']};
- border-radius: 8px;
- }}
- """)
-
- layout = QVBoxLayout(card)
- layout.setContentsMargins(15, 15, 15, 15)
- layout.setSpacing(8)
-
- # Title
- title = QLabel(name)
- title.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 11px;")
- layout.addWidget(title)
-
- # Value
- value = self.widget_data.get(widget_id, 0)
-
- if widget_id == 'ped_balance':
- value_text = f"{value:.2f} PED"
- elif widget_id == 'play_time':
- value_text = "2h 34m" # Placeholder
- elif widget_id == 'current_dpp':
- value_text = "3.45"
- else:
- value_text = str(value)
-
- value_label = QLabel(value_text)
- value_label.setStyleSheet(f"""
- color: {EU_COLORS['accent_orange']};
- font-size: 24px;
- font-weight: bold;
- """)
- layout.addWidget(value_label)
-
- layout.addStretch()
- return card
-
- def _show_customize_dialog(self):
- """Show widget customization dialog."""
- dialog = QDialog()
- dialog.setWindowTitle("Customize Dashboard")
- dialog.setStyleSheet(f"""
- QDialog {{
- background-color: {EU_COLORS['bg_secondary']};
- color: white;
- }}
- QLabel {{
- color: white;
- }}
- """)
-
- layout = QVBoxLayout(dialog)
-
- # Instructions
- info = QLabel("Check widgets to display on dashboard:")
- info.setStyleSheet(f"color: {EU_COLORS['text_secondary']};")
- layout.addWidget(info)
-
- # Widget list
- list_widget = QListWidget()
- list_widget.setStyleSheet(f"""
- QListWidget {{
- background-color: {EU_COLORS['bg_secondary']};
- color: white;
- border: 1px solid {EU_COLORS['border_default']};
- }}
- QListWidget::item {{
- padding: 10px;
- }}
- """)
-
- for widget_id, widget_info in self.AVAILABLE_WIDGETS.items():
- item = QListWidgetItem(widget_info['name'])
- item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
- item.setCheckState(
- Qt.CheckState.Checked if widget_id in self.enabled_widgets
- else Qt.CheckState.Unchecked
- )
- item.setData(Qt.ItemDataRole.UserRole, widget_id)
- list_widget.addItem(item)
-
- layout.addWidget(list_widget)
-
- # Buttons
- buttons = QDialogButtonBox(
- QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
- )
- buttons.accepted.connect(dialog.accept)
- buttons.rejected.connect(dialog.reject)
- layout.addWidget(buttons)
-
- if dialog.exec() == QDialog.DialogCode.Accepted:
- # Save selection
- self.enabled_widgets = []
- for i in range(list_widget.count()):
- item = list_widget.item(i)
- if item.checkState() == Qt.CheckState.Checked:
- self.enabled_widgets.append(item.data(Qt.ItemDataRole.UserRole))
-
- self._save_config()
- self._update_widgets()
diff --git a/plugins/discord_presence/__init__.py b/plugins/discord_presence/__init__.py
deleted file mode 100644
index 977afb9..0000000
--- a/plugins/discord_presence/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .plugin import DiscordPresencePlugin
-
-__all__ = ['DiscordPresencePlugin']
diff --git a/plugins/discord_presence/plugin.py b/plugins/discord_presence/plugin.py
deleted file mode 100644
index b834e66..0000000
--- a/plugins/discord_presence/plugin.py
+++ /dev/null
@@ -1,217 +0,0 @@
-# Description: Discord Rich Presence plugin for EU-Utility
-# Author: LemonNexus
-# Version: 1.0.0
-
-"""
-Discord Rich Presence plugin for EU-Utility.
-Shows current Entropia Universe activity in Discord.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QLineEdit, QCheckBox, QComboBox
-)
-from PyQt6.QtCore import QTimer
-from plugins.base_plugin import BasePlugin
-import time
-import threading
-
-
-class DiscordPresencePlugin(BasePlugin):
- """Discord Rich Presence integration for EU-Utility."""
-
- name = "Discord Presence"
- version = "1.0.0"
- author = "LemonNexus"
- description = "Show EU activity in Discord status"
- icon = "message-square"
-
- def initialize(self):
- """Initialize Discord RPC."""
- self.enabled = self.load_data("enabled", False)
- self.client_id = self.load_data("client_id", "")
- self.show_details = self.load_data("show_details", True)
- self.rpc = None
- self.connected = False
-
- # Try to import pypresence
- try:
- from pypresence import Presence
- self.Presence = Presence
- self.available = True
- except ImportError:
- self.log_warning("pypresence not installed. Run: pip install pypresence")
- self.available = False
- return
-
- # Auto-connect if enabled
- if self.enabled and self.client_id:
- self._connect()
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
-
- # Title
- title = QLabel("Discord Rich Presence")
- title.setStyleSheet("font-size: 18px; font-weight: bold; color: #5865F2;")
- layout.addWidget(title)
-
- # Status
- self.status_label = QLabel("Status: " + ("Connected" if self.connected else "Disconnected"))
- self.status_label.setStyleSheet(
- f"color: {'#43b581' if self.connected else '#f04747'}; font-weight: bold;"
- )
- layout.addWidget(self.status_label)
-
- # Enable checkbox
- self.enable_checkbox = QCheckBox("Enable Discord Presence")
- self.enable_checkbox.setChecked(self.enabled)
- self.enable_checkbox.toggled.connect(self._on_enable_toggled)
- layout.addWidget(self.enable_checkbox)
-
- # Client ID input
- id_layout = QHBoxLayout()
- id_layout.addWidget(QLabel("Client ID:"))
- self.id_input = QLineEdit(self.client_id)
- self.id_input.setPlaceholderText("Your Discord Application Client ID")
- id_layout.addWidget(self.id_input)
- layout.addLayout(id_layout)
-
- # Show details
- self.details_checkbox = QCheckBox("Show detailed activity")
- self.details_checkbox.setChecked(self.show_details)
- self.details_checkbox.toggled.connect(self._on_details_toggled)
- layout.addWidget(self.details_checkbox)
-
- # Activity type
- layout.addWidget(QLabel("Activity Type:"))
- self.activity_combo = QComboBox()
- self.activity_combo.addItems(["Playing", "Hunting", "Mining", "Crafting", "Trading"])
- self.activity_combo.currentTextChanged.connect(self._update_presence)
- layout.addWidget(self.activity_combo)
-
- # Buttons
- btn_layout = QHBoxLayout()
-
- connect_btn = QPushButton("Connect")
- connect_btn.clicked.connect(self._connect)
- btn_layout.addWidget(connect_btn)
-
- disconnect_btn = QPushButton("Disconnect")
- disconnect_btn.clicked.connect(self._disconnect)
- btn_layout.addWidget(disconnect_btn)
-
- update_btn = QPushButton("Update Presence")
- update_btn.clicked.connect(self._update_presence)
- btn_layout.addWidget(update_btn)
-
- layout.addLayout(btn_layout)
-
- # Info
- info = QLabel(
- "To use this feature:\n"
- "1. Create a Discord app at discord.com/developers\n"
- "2. Copy the Client ID\n"
- "3. Paste it above and click Connect"
- )
- info.setStyleSheet("color: rgba(255,255,255,150); font-size: 11px;")
- layout.addWidget(info)
-
- layout.addStretch()
- return widget
-
- def _on_enable_toggled(self, checked):
- """Handle enable toggle."""
- self.enabled = checked
- self.save_data("enabled", checked)
- if checked:
- self._connect()
- else:
- self._disconnect()
-
- def _on_details_toggled(self, checked):
- """Handle details toggle."""
- self.show_details = checked
- self.save_data("show_details", checked)
- self._update_presence()
-
- def _connect(self):
- """Connect to Discord."""
- if not self.available:
- self.log_error("pypresence not installed")
- return
-
- client_id = self.id_input.text().strip()
- if not client_id:
- self.log_warning("No Client ID provided")
- return
-
- self.client_id = client_id
- self.save_data("client_id", client_id)
-
- try:
- self.rpc = self.Presence(client_id)
- self.rpc.connect()
- self.connected = True
- self.status_label.setText("Status: Connected")
- self.status_label.setStyleSheet("color: #43b581; font-weight: bold;")
- self._update_presence()
- self.log_info("Discord RPC connected")
- except Exception as e:
- self.log_error(f"Failed to connect: {e}")
- self.connected = False
-
- def _disconnect(self):
- """Disconnect from Discord."""
- if self.rpc:
- try:
- self.rpc.close()
- except:
- pass
- self.rpc = None
- self.connected = False
- self.status_label.setText("Status: Disconnected")
- self.status_label.setStyleSheet("color: #f04747; font-weight: bold;")
- self.log_info("Discord RPC disconnected")
-
- def _update_presence(self):
- """Update Discord presence."""
- if not self.connected or not self.rpc:
- return
-
- activity = self.activity_combo.currentText()
-
- try:
- if self.show_details:
- self.rpc.update(
- state=f"In Entropia Universe",
- details=f"Currently {activity}",
- large_image="eu_logo",
- large_text="Entropia Universe",
- small_image="playing",
- small_text=activity,
- start=time.time()
- )
- else:
- self.rpc.update(
- state="Playing Entropia Universe",
- large_image="eu_logo",
- large_text="Entropia Universe"
- )
- self.log_info("Presence updated")
- except Exception as e:
- self.log_error(f"Failed to update presence: {e}")
-
- def on_show(self):
- """Update UI when shown."""
- self.status_label.setText("Status: " + ("Connected" if self.connected else "Disconnected"))
- self.status_label.setStyleSheet(
- f"color: {'#43b581' if self.connected else '#f04747'}; font-weight: bold;"
- )
-
- def shutdown(self):
- """Cleanup on shutdown."""
- self._disconnect()
diff --git a/plugins/dpp_calculator/__init__.py b/plugins/dpp_calculator/__init__.py
deleted file mode 100644
index 1130b97..0000000
--- a/plugins/dpp_calculator/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-DPP Calculator Plugin
-"""
-
-from .plugin import DPPCalculatorPlugin
-
-__all__ = ["DPPCalculatorPlugin"]
diff --git a/plugins/dpp_calculator/plugin.py b/plugins/dpp_calculator/plugin.py
deleted file mode 100644
index 2d1bfb2..0000000
--- a/plugins/dpp_calculator/plugin.py
+++ /dev/null
@@ -1,230 +0,0 @@
-"""
-EU-Utility - DPP Calculator Plugin
-
-Calculate Damage Per PEC for weapons and setups.
-"""
-
-from decimal import Decimal, ROUND_HALF_UP
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QLineEdit, QPushButton, QComboBox, QTableWidget,
- QTableWidgetItem, QFrame, QGroupBox
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-from core.icon_manager import get_icon_manager
-
-
-class DPPCalculatorPlugin(BasePlugin):
- """Calculate weapon efficiency and DPP."""
-
- name = "DPP Calculator"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Calculate Damage Per PEC and weapon efficiency"
- hotkey = "ctrl+shift+d"
-
- def initialize(self):
- """Setup DPP calculator."""
- self.saved_setups = []
-
- def get_ui(self):
- """Create DPP calculator UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Get icon manager
- icon_mgr = get_icon_manager()
-
- # Title with icon
- title_layout = QHBoxLayout()
-
- title_icon = QLabel()
- icon_pixmap = icon_mgr.get_pixmap('target', size=20)
- title_icon.setPixmap(icon_pixmap)
- title_icon.setFixedSize(20, 20)
- title_layout.addWidget(title_icon)
-
- title = QLabel("DPP Calculator")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- title_layout.addWidget(title)
- title_layout.addStretch()
-
- layout.addLayout(title_layout)
-
- # Calculator section
- calc_group = QGroupBox("Weapon Setup")
- calc_group.setStyleSheet("""
- QGroupBox {
- color: rgba(255,255,255,200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- margin-top: 10px;
- font-weight: bold;
- }
- QGroupBox::title {
- subcontrol-origin: margin;
- left: 10px;
- padding: 0 5px;
- }
- """)
- calc_layout = QVBoxLayout(calc_group)
-
- # Weapon damage
- dmg_layout = QHBoxLayout()
- dmg_layout.addWidget(QLabel("Damage:"))
- self.dmg_input = QLineEdit()
- self.dmg_input.setPlaceholderText("e.g., 45.2")
- dmg_layout.addWidget(self.dmg_input)
- calc_layout.addLayout(dmg_layout)
-
- # Ammo cost
- ammo_layout = QHBoxLayout()
- ammo_layout.addWidget(QLabel("Ammo per shot:"))
- self.ammo_input = QLineEdit()
- self.ammo_input.setPlaceholderText("e.g., 100")
- ammo_layout.addWidget(self.ammo_input)
- calc_layout.addLayout(ammo_layout)
-
- # Weapon decay
- decay_layout = QHBoxLayout()
- decay_layout.addWidget(QLabel("Decay (PEC):"))
- self.decay_input = QLineEdit()
- self.decay_input.setPlaceholderText("e.g., 2.5")
- decay_layout.addWidget(self.decay_input)
- calc_layout.addLayout(decay_layout)
-
- # Amp/scope
- amp_layout = QHBoxLayout()
- amp_layout.addWidget(QLabel("Amp decay:"))
- self.amp_input = QLineEdit()
- self.amp_input.setPlaceholderText("0")
- self.amp_input.setText("0")
- amp_layout.addWidget(self.amp_input)
- calc_layout.addLayout(amp_layout)
-
- # Calculate button
- calc_btn = QPushButton("Calculate DPP")
- calc_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #ffa060;
- }
- """)
- calc_btn.clicked.connect(self._calculate)
- calc_layout.addWidget(calc_btn)
-
- # Results
- self.result_label = QLabel("DPP: -")
- self.result_label.setStyleSheet("""
- color: #4caf50;
- font-size: 20px;
- font-weight: bold;
- """)
- calc_layout.addWidget(self.result_label)
-
- self.cost_label = QLabel("Cost per shot: -")
- self.cost_label.setStyleSheet("color: rgba(255,255,255,150);")
- calc_layout.addWidget(self.cost_label)
-
- layout.addWidget(calc_group)
-
- # Reference table
- ref_group = QGroupBox("DPP Reference")
- ref_group.setStyleSheet(calc_group.styleSheet())
- ref_layout = QVBoxLayout(ref_group)
-
- ref_table = QTableWidget()
- ref_table.setColumnCount(3)
- ref_table.setHorizontalHeaderLabels(["Rating", "DPP Range", "Efficiency"])
- ref_table.setRowCount(5)
-
- ratings = [
- ("Excellent", "4.0+", "95-100%"),
- ("Very Good", "3.5-4.0", "85-95%"),
- ("Good", "3.0-3.5", "75-85%"),
- ("Average", "2.5-3.0", "60-75%"),
- ("Poor", "< 2.5", "< 60%"),
- ]
-
- for i, (rating, dpp, eff) in enumerate(ratings):
- ref_table.setItem(i, 0, QTableWidgetItem(rating))
- ref_table.setItem(i, 1, QTableWidgetItem(dpp))
- ref_table.setItem(i, 2, QTableWidgetItem(eff))
-
- ref_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: none;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 6px;
- font-size: 10px;
- }
- """)
- ref_layout.addWidget(ref_table)
-
- layout.addWidget(ref_group)
- layout.addStretch()
-
- return widget
-
- def _calculate(self):
- """Calculate DPP."""
- try:
- damage = Decimal(self.dmg_input.text() or 0)
- ammo = Decimal(self.ammo_input.text() or 0)
- decay = Decimal(self.decay_input.text() or 0)
- amp = Decimal(self.amp_input.text() or 0)
-
- # Convert ammo to PEC (1 ammo = 0.0001 PED = 0.01 PEC)
- ammo_cost = ammo * Decimal('0.01')
-
- # Total cost in PEC
- total_cost = ammo_cost + decay + amp
-
- if total_cost > 0:
- dpp = damage / (total_cost / Decimal('100')) # Convert to PED for DPP
-
- self.result_label.setText(f"DPP: {dpp:.3f}")
- self.result_label.setStyleSheet(f"""
- color: {'#4caf50' if dpp >= Decimal('3.5') else '#ffc107' if dpp >= Decimal('2.5') else '#f44336'};
- font-size: 20px;
- font-weight: bold;
- """)
-
- cost_ped = total_cost / Decimal('100')
- self.cost_label.setText(f"Cost per shot: {cost_ped:.4f} PED ({total_cost:.2f} PEC)")
- else:
- self.result_label.setText("DPP: Enter values")
-
- except Exception as e:
- self.result_label.setText(f"Error: {e}")
-
- def calculate_from_api(self, weapon_data):
- """Calculate DPP from Nexus API weapon data."""
- damage = Decimal(str(weapon_data.get('damage', 0)))
- decay = Decimal(str(weapon_data.get('decay', 0)))
- ammo = Decimal(str(weapon_data.get('ammo', 0)))
-
- # Calculate
- ammo_cost = ammo * Decimal('0.01')
- total_cost = ammo_cost + decay
-
- if total_cost > 0:
- return damage / (total_cost / Decimal('100'))
- return Decimal('0')
diff --git a/plugins/enhancer_calc/__init__.py b/plugins/enhancer_calc/__init__.py
deleted file mode 100644
index 8758bb8..0000000
--- a/plugins/enhancer_calc/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Enhancer Calculator Plugin
-"""
-
-from .plugin import EnhancerCalculatorPlugin
-
-__all__ = ["EnhancerCalculatorPlugin"]
diff --git a/plugins/enhancer_calc/plugin.py b/plugins/enhancer_calc/plugin.py
deleted file mode 100644
index a5cc975..0000000
--- a/plugins/enhancer_calc/plugin.py
+++ /dev/null
@@ -1,160 +0,0 @@
-"""
-EU-Utility - Enhancer Calculator Plugin
-
-Calculate enhancer break rates and costs.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QLineEdit, QPushButton, QComboBox, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class EnhancerCalculatorPlugin(BasePlugin):
- """Calculate enhancer usage and costs."""
-
- name = "Enhancer Calc"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Calculate enhancer break rates and costs"
- hotkey = "ctrl+shift+e"
-
- # Break rates (approximate)
- BREAK_RATES = {
- 'Accuracy': 0.0012, # 0.12%
- 'Damage': 0.0015, # 0.15%
- 'Economy': 0.0010, # 0.10%
- 'Range': 0.0013, # 0.13%
- }
-
- def initialize(self):
- """Setup enhancer calculator."""
- self.saved_calculations = []
-
- def get_ui(self):
- """Create enhancer calculator UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel(" Enhancer Calculator")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Calculator
- calc_frame = QFrame()
- calc_frame.setStyleSheet("""
- QFrame {
- background-color: rgba(30, 35, 45, 200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- """)
- calc_layout = QVBoxLayout(calc_frame)
- calc_layout.setSpacing(10)
-
- # Enhancer type
- type_layout = QHBoxLayout()
- type_layout.addWidget(QLabel("Type:"))
- self.type_combo = QComboBox()
- self.type_combo.addItems(list(self.BREAK_RATES.keys()))
- self.type_combo.setStyleSheet("""
- QComboBox {
- background-color: rgba(20, 25, 35, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- padding: 5px;
- }
- """)
- type_layout.addWidget(self.type_combo)
- calc_layout.addLayout(type_layout)
-
- # TT value
- tt_layout = QHBoxLayout()
- tt_layout.addWidget(QLabel("TT Value:"))
- self.tt_input = QLineEdit()
- self.tt_input.setPlaceholderText("e.g., 40.00")
- tt_layout.addWidget(self.tt_input)
- calc_layout.addLayout(tt_layout)
-
- # Number of shots
- shots_layout = QHBoxLayout()
- shots_layout.addWidget(QLabel("Shots/hour:"))
- self.shots_input = QLineEdit()
- self.shots_input.setPlaceholderText("e.g., 3600")
- self.shots_input.setText("3600")
- shots_layout.addWidget(self.shots_input)
- calc_layout.addLayout(shots_layout)
-
- # Calculate button
- calc_btn = QPushButton("Calculate")
- calc_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- calc_btn.clicked.connect(self._calculate)
- calc_layout.addWidget(calc_btn)
-
- # Results
- self.break_rate_label = QLabel("Break rate: -")
- self.break_rate_label.setStyleSheet("color: rgba(255,255,255,180);")
- calc_layout.addWidget(self.break_rate_label)
-
- self.breaks_label = QLabel("Expected breaks/hour: -")
- self.breaks_label.setStyleSheet("color: #f44336;")
- calc_layout.addWidget(self.breaks_label)
-
- self.cost_label = QLabel("Cost/hour: -")
- self.cost_label.setStyleSheet("color: #ffc107;")
- calc_layout.addWidget(self.cost_label)
-
- layout.addWidget(calc_frame)
-
- # Info
- info = QLabel("""
- Typical break rates:
- โข Damage: ~0.15% per shot
- โข Accuracy: ~0.12% per shot
- โข Economy: ~0.10% per shot
- โข Range: ~0.13% per shot
- """)
- info.setStyleSheet("color: rgba(255,255,255,120); font-size: 10px;")
- info.setWordWrap(True)
- layout.addWidget(info)
-
- layout.addStretch()
- return widget
-
- def _calculate(self):
- """Calculate enhancer costs."""
- try:
- enhancer_type = self.type_combo.currentText()
- tt_value = float(self.tt_input.text() or 0)
- shots = int(self.shots_input.text() or 3600)
-
- break_rate = self.BREAK_RATES.get(enhancer_type, 0.001)
-
- # Expected breaks
- expected_breaks = shots * break_rate
-
- # Cost
- hourly_cost = expected_breaks * tt_value
-
- self.break_rate_label.setText(f"Break rate: {break_rate*100:.3f}% per shot")
- self.breaks_label.setText(f"Expected breaks/hour: {expected_breaks:.2f}")
- self.cost_label.setText(f"Cost/hour: {hourly_cost:.2f} PED")
-
- except Exception as e:
- self.break_rate_label.setText(f"Error: {e}")
diff --git a/plugins/event_bus_example/__init__.py b/plugins/event_bus_example/__init__.py
deleted file mode 100644
index 887ba92..0000000
--- a/plugins/event_bus_example/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""Event Bus Example Plugin."""
-from .plugin import EventBusExamplePlugin
-
-__all__ = ['EventBusExamplePlugin']
diff --git a/plugins/event_bus_example/plugin.py b/plugins/event_bus_example/plugin.py
deleted file mode 100644
index 1b3081e..0000000
--- a/plugins/event_bus_example/plugin.py
+++ /dev/null
@@ -1,211 +0,0 @@
-"""
-Example plugin demonstrating Enhanced Event Bus usage.
-
-This plugin shows how to use:
-- publish_typed() - Publish typed events
-- subscribe_typed() - Subscribe with filtering
-- get_recent_events() - Retrieve event history
-- Event filtering (min_damage, mob_types, etc.)
-- Event replay for new subscribers
-"""
-
-from plugins.base_plugin import BasePlugin
-from core.event_bus import (
- SkillGainEvent, LootEvent, DamageEvent, GlobalEvent,
- EventCategory
-)
-
-
-class EventBusExamplePlugin(BasePlugin):
- """Example plugin showing Enhanced Event Bus usage."""
-
- name = "Event Bus Example"
- version = "1.0.0"
- author = "EU-Utility"
- description = "Demonstrates Enhanced Event Bus features"
-
- def __init__(self, overlay_window, config):
- super().__init__(overlay_window, config)
- self.big_hits = []
- self.skill_gains = []
- self.dragon_loot = []
- self._subscriptions = []
-
- def initialize(self) -> None:
- """Setup event subscriptions."""
- print(f"[{self.name}] Initializing...")
-
- # 1. Subscribe to ALL damage events
- sub_id = self.subscribe_typed(
- DamageEvent,
- self.on_any_damage,
- replay_last=5 # Replay last 5 damage events on subscribe
- )
- self._subscriptions.append(sub_id)
- print(f"[{self.name}] Subscribed to all damage events")
-
- # 2. Subscribe to HIGH damage events only (filtering)
- sub_id = self.subscribe_typed(
- DamageEvent,
- self.on_big_damage,
- min_damage=100, # Only events with damage >= 100
- replay_last=3
- )
- self._subscriptions.append(sub_id)
- print(f"[{self.name}] Subscribed to high damage events (โฅ100)")
-
- # 3. Subscribe to skill gains for specific skills
- sub_id = self.subscribe_typed(
- SkillGainEvent,
- self.on_combat_skill_gain,
- skill_names=["Rifle", "Handgun", "Sword", "Knife"],
- replay_last=10
- )
- self._subscriptions.append(sub_id)
- print(f"[{self.name}] Subscribed to combat skill gains")
-
- # 4. Subscribe to loot from specific mobs
- sub_id = self.subscribe_typed(
- LootEvent,
- self.on_dragon_loot,
- mob_types=["Dragon", "Drake", "Dragon Old"],
- replay_last=5
- )
- self._subscriptions.append(sub_id)
- print(f"[{self.name}] Subscribed to Dragon/Drake loot")
-
- # 5. Subscribe to ALL globals
- sub_id = self.subscribe_typed(
- GlobalEvent,
- self.on_global_announcement
- )
- self._subscriptions.append(sub_id)
- print(f"[{self.name}] Subscribed to global announcements")
-
- # 6. Demonstrate publishing events
- self._publish_example_events()
-
- # 7. Show event stats
- stats = self.get_event_stats()
- print(f"[{self.name}] Event Bus Stats:")
- print(f" - Total published: {stats.get('total_published', 0)}")
- print(f" - Active subs: {stats.get('active_subscriptions', 0)}")
-
- def _publish_example_events(self):
- """Publish some example events to demonstrate."""
- # Publish a skill gain
- self.publish_typed(SkillGainEvent(
- skill_name="Rifle",
- skill_value=25.5,
- gain_amount=0.01,
- source="example_plugin"
- ))
-
- # Publish some damage events
- self.publish_typed(DamageEvent(
- damage_amount=50.5,
- damage_type="impact",
- is_outgoing=True,
- target_name="Berycled Young",
- source="example_plugin"
- ))
-
- self.publish_typed(DamageEvent(
- damage_amount=150.0,
- damage_type="penetration",
- is_critical=True,
- is_outgoing=True,
- target_name="Daikiba",
- source="example_plugin"
- ))
-
- # Publish loot
- self.publish_typed(LootEvent(
- mob_name="Dragon",
- items=[
- {"name": "Dragon Scale", "value": 15.0},
- {"name": "Animal Oil", "value": 0.05}
- ],
- total_tt_value=15.05,
- source="example_plugin"
- ))
-
- print(f"[{self.name}] Published example events")
-
- # ========== Event Handlers ==========
-
- def on_any_damage(self, event: DamageEvent):
- """Handle all damage events."""
- direction = "dealt" if event.is_outgoing else "received"
- crit = " CRITICAL" if event.is_critical else ""
- print(f"[{self.name}] Damage {direction}: {event.damage_amount:.1f}{crit} to {event.target_name}")
-
- def on_big_damage(self, event: DamageEvent):
- """Handle only high damage events (filtered)."""
- self.big_hits.append(event)
- print(f"[{self.name}] ๐ฅ BIG HIT! {event.damage_amount:.1f} damage to {event.target_name}")
- print(f"[{self.name}] Total big hits recorded: {len(self.big_hits)}")
-
- def on_combat_skill_gain(self, event: SkillGainEvent):
- """Handle combat skill gains."""
- self.skill_gains.append(event)
- print(f"[{self.name}] โ๏ธ Skill up: {event.skill_name} +{event.gain_amount:.4f} = {event.skill_value:.4f}")
-
- def on_dragon_loot(self, event: LootEvent):
- """Handle Dragon/Drake loot."""
- self.dragon_loot.append(event)
- items_str = ", ".join(event.get_item_names())
- print(f"[{self.name}] ๐ Dragon loot from {event.mob_name}: {items_str} (TT: {event.total_tt_value:.2f} PED)")
-
- def on_global_announcement(self, event: GlobalEvent):
- """Handle global announcements."""
- item_str = f" with {event.item_name}" if event.item_name else ""
- print(f"[{self.name}] ๐ GLOBAL: {event.player_name} - {event.achievement_type.upper()}{item_str} ({event.value:.2f} PED)")
-
- def get_ui(self):
- """Return simple info panel."""
- from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
-
- widget = QWidget()
- layout = QVBoxLayout()
-
- # Title
- title = QLabel(f"{self.name}
")
- layout.addWidget(title)
-
- # Stats
- stats_text = f"""
- Recorded Events:
- โข Big Hits (โฅ100 dmg): {len(self.big_hits)}
- โข Combat Skill Gains: {len(self.skill_gains)}
- โข Dragon Loot: {len(self.dragon_loot)}
-
- Active Subscriptions: {len(self._subscriptions)}
- """
- stats_label = QLabel(stats_text)
- stats_label.setWordWrap(True)
- layout.addWidget(stats_label)
-
- # Recent events
- layout.addWidget(QLabel("Recent Combat Events:"))
- text_area = QTextEdit()
- text_area.setReadOnly(True)
- text_area.setMaximumHeight(200)
-
- # Get recent damage events from event bus
- recent = self.get_recent_events(DamageEvent, count=10)
- events_text = "\\n".join([
- f"โข {e.damage_amount:.1f} dmg to {e.target_name}"
- for e in reversed(recent)
- ]) or "No recent damage events"
- text_area.setText(events_text)
- layout.addWidget(text_area)
-
- widget.setLayout(layout)
- return widget
-
- def shutdown(self) -> None:
- """Cleanup."""
- print(f"[{self.name}] Shutting down...")
- # Unsubscribe from all typed events (handled by base class)
- super().shutdown()
diff --git a/plugins/game_reader/__init__.py b/plugins/game_reader/__init__.py
deleted file mode 100644
index 0d896f8..0000000
--- a/plugins/game_reader/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Game Reader Plugin for EU-Utility
-"""
-
-from .plugin import GameReaderPlugin
-
-__all__ = ["GameReaderPlugin"]
diff --git a/plugins/game_reader/plugin.py b/plugins/game_reader/plugin.py
deleted file mode 100644
index 093fd10..0000000
--- a/plugins/game_reader/plugin.py
+++ /dev/null
@@ -1,261 +0,0 @@
-"""
-EU-Utility - OCR Scanner Plugin
-
-Reads text from in-game menus using OCR.
-"""
-
-import subprocess
-import platform
-import tempfile
-from pathlib import Path
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout,
- QLabel, QPushButton, QTextEdit, QComboBox,
- QFrame, QScrollArea, QGroupBox
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
-from PyQt6.QtGui import QPixmap, QImage
-
-from plugins.base_plugin import BasePlugin
-
-
-class OCRScannerThread(QThread):
- """Background thread for OCR scanning."""
- result_ready = pyqtSignal(str)
- error_occurred = pyqtSignal(str)
-
- def __init__(self, region=None):
- super().__init__()
- self.region = region # (x, y, width, height)
-
- def run(self):
- """Capture screen and perform OCR."""
- try:
- system = platform.system()
-
- # Create temp file for screenshot
- with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
- screenshot_path = tmp.name
-
- # Capture screenshot
- if system == "Windows":
- # Use PowerShell to capture screen
- ps_cmd = f'''
- Add-Type -AssemblyName System.Windows.Forms
- Add-Type -AssemblyName System.Drawing
- $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
- $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
- $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
- $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
- $bitmap.Save("{screenshot_path}")
- $graphics.Dispose()
- $bitmap.Dispose()
- '''
- subprocess.run(['powershell', '-Command', ps_cmd], capture_output=True, timeout=10)
-
- elif system == "Linux":
- # Use gnome-screenshot or import
- try:
- subprocess.run(['gnome-screenshot', '-f', screenshot_path],
- capture_output=True, timeout=10)
- except:
- subprocess.run(['import', '-window', 'root', screenshot_path],
- capture_output=True, timeout=10)
-
- # Perform OCR
- text = self._perform_ocr(screenshot_path)
-
- # Clean up
- Path(screenshot_path).unlink(missing_ok=True)
-
- self.result_ready.emit(text)
-
- except Exception as e:
- self.error_occurred.emit(str(e))
-
- def _perform_ocr(self, image_path):
- """Perform OCR on image."""
- try:
- # Try easyocr first
- import easyocr
- reader = easyocr.Reader(['en'])
- results = reader.readtext(image_path)
- text = '\n'.join([result[1] for result in results])
- return text if text else "No text detected"
- except:
- pass
-
- try:
- # Try pytesseract
- import pytesseract
- from PIL import Image
- image = Image.open(image_path)
- text = pytesseract.image_to_string(image)
- return text if text.strip() else "No text detected"
- except:
- pass
-
- return "OCR not available. Install: pip install easyocr or pytesseract"
-
-
-class GameReaderPlugin(BasePlugin):
- """Read in-game menus and text using OCR."""
-
- name = "Game Reader"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "OCR scanner for in-game menus and text"
- hotkey = "ctrl+shift+r" # R for Read
-
- def initialize(self):
- """Setup game reader."""
- self.scan_thread = None
- self.last_result = ""
-
- def get_ui(self):
- """Create game reader UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("๐ท Game Reader (OCR)")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Info
- info = QLabel("Capture in-game menus and read the text")
- info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
- layout.addWidget(info)
-
- # Scan button
- scan_btn = QPushButton("Capture Screen")
- scan_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a9eff;
- color: white;
- padding: 15px;
- border: none;
- border-radius: 10px;
- font-size: 14px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #5aafff;
- }
- QPushButton:pressed {
- background-color: #3a8eef;
- }
- """)
- scan_btn.clicked.connect(self._capture_screen)
- layout.addWidget(scan_btn)
-
- # Status
- self.status_label = QLabel("Ready to capture")
- self.status_label.setStyleSheet("color: #666; font-size: 11px;")
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- layout.addWidget(self.status_label)
-
- # Results area
- results_frame = QFrame()
- results_frame.setStyleSheet("""
- QFrame {
- background-color: rgba(0, 0, 0, 50);
- border-radius: 10px;
- border: 1px solid rgba(255, 255, 255, 20);
- }
- """)
- results_layout = QVBoxLayout(results_frame)
- results_layout.setContentsMargins(10, 10, 10, 10)
-
- results_label = QLabel("Captured Text:")
- results_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
- results_layout.addWidget(results_label)
-
- self.result_text = QTextEdit()
- self.result_text.setPlaceholderText("Captured text will appear here...")
- self.result_text.setStyleSheet("""
- QTextEdit {
- background-color: rgba(30, 30, 30, 100);
- color: white;
- border: none;
- border-radius: 6px;
- padding: 8px;
- font-size: 13px;
- }
- """)
- self.result_text.setMaximumHeight(150)
- results_layout.addWidget(self.result_text)
-
- # Copy button
- copy_btn = QPushButton("Copy Text")
- copy_btn.setStyleSheet("""
- QPushButton {
- background-color: rgba(255, 255, 255, 20);
- color: white;
- padding: 8px;
- border: none;
- border-radius: 6px;
- }
- QPushButton:hover {
- background-color: rgba(255, 255, 255, 30);
- }
- """)
- copy_btn.clicked.connect(self._copy_text)
- results_layout.addWidget(copy_btn)
-
- layout.addWidget(results_frame)
-
- # Common uses
- uses_label = QLabel("Common Uses:")
- uses_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 11px; margin-top: 10px;")
- layout.addWidget(uses_label)
-
- uses_text = QLabel(
- "โข Read NPC dialogue\n"
- "โข Capture mission text\n"
- "โข Extract item stats\n"
- "โข Read shop prices"
- )
- uses_text.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
- layout.addWidget(uses_text)
-
- layout.addStretch()
-
- return widget
-
- def _capture_screen(self):
- """Capture screen and perform OCR."""
- self.status_label.setText("Capturing...")
- self.status_label.setStyleSheet("color: #4a9eff;")
-
- # Start scan thread
- self.scan_thread = OCRScannerThread()
- self.scan_thread.result_ready.connect(self._on_result)
- self.scan_thread.error_occurred.connect(self._on_error)
- self.scan_thread.start()
-
- def _on_result(self, text):
- """Handle OCR result."""
- self.result_text.setText(text)
- self.last_result = text
- self.status_label.setText(f"โ
Captured {len(text)} characters")
- self.status_label.setStyleSheet("color: #4caf50;")
-
- def _on_error(self, error):
- """Handle OCR error."""
- self.status_label.setText(f"โ Error: {error}")
- self.status_label.setStyleSheet("color: #f44336;")
-
- def _copy_text(self):
- """Copy text to clipboard."""
- from PyQt6.QtWidgets import QApplication
- clipboard = QApplication.clipboard()
- clipboard.setText(self.result_text.toPlainText())
- self.status_label.setText("๐ Copied to clipboard!")
-
- def on_hotkey(self):
- """Capture on hotkey."""
- self._capture_screen()
diff --git a/plugins/game_reader_test/__init__.py b/plugins/game_reader_test/__init__.py
deleted file mode 100644
index 401c3cf..0000000
--- a/plugins/game_reader_test/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""Game Reader Test Plugin."""
-from .plugin import GameReaderTestPlugin
diff --git a/plugins/game_reader_test/plugin.py b/plugins/game_reader_test/plugin.py
deleted file mode 100644
index c47532f..0000000
--- a/plugins/game_reader_test/plugin.py
+++ /dev/null
@@ -1,1197 +0,0 @@
-"""
-EU-Utility - Game Reader Test Plugin
-
-Debug and test tool for OCR and game reading functionality.
-Tests screen capture, OCR accuracy, and text extraction.
-"""
-
-import re
-from pathlib import Path
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
- QLabel, QPushButton, QComboBox, QCheckBox,
- QSpinBox, QGroupBox, QSplitter, QFrame,
- QTabWidget, QLineEdit, QProgressBar,
- QFileDialog, QMessageBox, QTableWidget,
- QTableWidgetItem, QHeaderView
-)
-from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
-from PyQt6.QtGui import QPixmap, QImage, QColor
-
-from plugins.base_plugin import BasePlugin
-
-
-class OCRTestThread(QThread):
- """Background thread for OCR testing."""
- result_ready = pyqtSignal(dict)
- progress_update = pyqtSignal(int, str)
-
- def __init__(self, region=None, backend='auto'):
- super().__init__()
- self.region = region
- self.backend = backend
-
- def run(self):
- """Run OCR test."""
- import time
- results = {
- 'success': False,
- 'text': '',
- 'backend_used': '',
- 'processing_time': 0,
- 'error': None
- }
-
- try:
- start_time = time.time()
- self.progress_update.emit(10, "Capturing screen...")
-
- # Capture screen
- from PIL import Image, ImageGrab
- if self.region:
- screenshot = ImageGrab.grab(bbox=self.region)
- else:
- screenshot = ImageGrab.grab()
-
- self.progress_update.emit(30, "Running OCR...")
-
- # Try OCR backends
- text = ""
- backend_used = "none"
-
- if self.backend in ('auto', 'easyocr'):
- try:
- import easyocr
- import numpy as np
- self.progress_update.emit(50, "Loading EasyOCR...")
- reader = easyocr.Reader(['en'], gpu=False, verbose=False)
- self.progress_update.emit(70, "Processing with EasyOCR...")
- # Convert PIL Image to numpy array
- screenshot_np = np.array(screenshot)
- ocr_result = reader.readtext(
- screenshot_np,
- detail=0,
- paragraph=True
- )
- text = '\n'.join(ocr_result)
- backend_used = "easyocr"
- except Exception as e:
- if self.backend == 'easyocr':
- raise e
-
- if not text and self.backend in ('auto', 'tesseract'):
- try:
- import pytesseract
- self.progress_update.emit(70, "Processing with Tesseract...")
- text = pytesseract.image_to_string(screenshot)
- backend_used = "tesseract"
- except Exception as e:
- if self.backend == 'tesseract':
- raise e
-
- if not text and self.backend in ('auto', 'paddle'):
- try:
- from paddleocr import PaddleOCR
- import numpy as np
- self.progress_update.emit(70, "Processing with PaddleOCR...")
- # Try with show_log, fall back without for compatibility
- try:
- ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False)
- except TypeError:
- ocr = PaddleOCR(use_angle_cls=True, lang='en')
- # Convert PIL to numpy
- screenshot_np = np.array(screenshot)
- result = ocr.ocr(screenshot_np, cls=True)
- if result and result[0]:
- texts = [line[1][0] for line in result[0]]
- text = '\n'.join(texts)
- backend_used = "paddleocr"
- except Exception as e:
- if self.backend == 'paddle':
- raise e
-
- processing_time = time.time() - start_time
-
- results.update({
- 'success': True,
- 'text': text or "No text detected",
- 'backend_used': backend_used,
- 'processing_time': processing_time
- })
-
- self.progress_update.emit(100, "Complete!")
-
- except Exception as e:
- results['error'] = str(e)
- self.progress_update.emit(100, f"Error: {e}")
-
- self.result_ready.emit(results)
-
-
-class GameReaderTestPlugin(BasePlugin):
- """Test and debug tool for game reading/OCR functionality."""
-
- name = "Game Reader Test"
- version = "1.0.0"
- author = "EU-Utility"
- description = "Debug tool for testing OCR and screen reading"
-
- # Dependencies for OCR functionality
- dependencies = {
- 'pip': ['pillow', 'numpy'],
- 'optional': {
- 'easyocr': 'Best OCR accuracy, auto-downloads models',
- 'pytesseract': 'Alternative OCR engine',
- 'paddleocr': 'Advanced OCR with layout detection'
- }
- }
-
- def __init__(self, overlay_window, config):
- super().__init__(overlay_window, config)
- self.test_history = []
- self.max_history = 50
- self.ocr_thread = None
-
- def initialize(self):
- """Initialize plugin."""
- self.log_info("Game Reader Test initialized")
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(12)
-
- # Header
- header = QLabel("๐ท Game Reader Test & Debug Tool")
- header.setStyleSheet("font-size: 16px; font-weight: bold; color: #ff8c42;")
- layout.addWidget(header)
-
- # Create tabs
- tabs = QTabWidget()
-
- # Tab 1: Quick Test
- tabs.addTab(self._create_quick_test_tab(), "Quick Test")
-
- # Tab 2: File Test (NEW)
- tabs.addTab(self._create_file_test_tab(), "File Test")
-
- # Tab 3: Region Test
- tabs.addTab(self._create_region_test_tab(), "Region Test")
-
- # Tab 3: History
- tabs.addTab(self._create_history_tab(), "History")
-
- # Tab 5: Skills Parser (NEW)
- tabs.addTab(self._create_skills_parser_tab(), "Skills Parser")
-
- # Tab 6: Calibration
- tabs.addTab(self._create_calibration_tab(), "Calibration")
-
- layout.addWidget(tabs, 1)
-
- # Status bar
- status_frame = QFrame()
- status_frame.setStyleSheet("background-color: #1a1f2e; border-radius: 6px; padding: 8px;")
- status_layout = QHBoxLayout(status_frame)
-
- self.status_label = QLabel("Ready - Select a tab to begin testing")
- self.status_label.setStyleSheet("color: #4ecdc4;")
- status_layout.addWidget(self.status_label)
-
- layout.addWidget(status_frame)
-
- return widget
-
- def _create_quick_test_tab(self):
- """Create quick test tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # Info
- info = QLabel("Quick OCR Test - Captures full screen and extracts text")
- info.setStyleSheet("color: #888;")
- layout.addWidget(info)
-
- # Backend selection
- backend_layout = QHBoxLayout()
- backend_layout.addWidget(QLabel("OCR Backend:"))
- self.backend_combo = QComboBox()
- self.backend_combo.addItems(["Auto (try all)", "EasyOCR", "Tesseract", "PaddleOCR"])
- backend_layout.addWidget(self.backend_combo)
- backend_layout.addStretch()
- layout.addLayout(backend_layout)
-
- # Progress bar
- self.progress_bar = QProgressBar()
- self.progress_bar.setRange(0, 100)
- self.progress_bar.setValue(0)
- self.progress_bar.setStyleSheet("""
- QProgressBar {
- border: 1px solid #333;
- border-radius: 4px;
- text-align: center;
- }
- QProgressBar::chunk {
- background-color: #ff8c42;
- }
- """)
- layout.addWidget(self.progress_bar)
-
- # Test button
- self.test_btn = QPushButton("โถ Run OCR Test")
- self.test_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: #141f23;
- font-weight: bold;
- padding: 12px;
- font-size: 14px;
- }
- QPushButton:hover {
- background-color: #ffa05c;
- }
- """)
- self.test_btn.clicked.connect(self._run_quick_test)
- layout.addWidget(self.test_btn)
-
- # Results
- results_group = QGroupBox("OCR Results")
- results_layout = QVBoxLayout(results_group)
-
- self.results_text = QTextEdit()
- self.results_text.setReadOnly(True)
- self.results_text.setPlaceholderText("OCR results will appear here...")
- self.results_text.setStyleSheet("""
- QTextEdit {
- background-color: #0d1117;
- color: #c9d1d9;
- font-family: Consolas, monospace;
- font-size: 12px;
- }
- """)
- results_layout.addWidget(self.results_text)
-
- # Stats
- self.stats_label = QLabel("Backend: - | Time: - | Status: Waiting")
- self.stats_label.setStyleSheet("color: #888;")
- results_layout.addWidget(self.stats_label)
-
- layout.addWidget(results_group)
-
- # Save buttons
- btn_layout = QHBoxLayout()
-
- save_text_btn = QPushButton("๐พ Save Text")
- save_text_btn.clicked.connect(self._save_text)
- btn_layout.addWidget(save_text_btn)
-
- copy_btn = QPushButton("๐ Copy to Clipboard")
- copy_btn.clicked.connect(self._copy_to_clipboard)
- btn_layout.addWidget(copy_btn)
-
- clear_btn = QPushButton("๐ Clear")
- clear_btn.clicked.connect(self._clear_results)
- btn_layout.addWidget(clear_btn)
-
- btn_layout.addStretch()
- layout.addLayout(btn_layout)
-
- layout.addStretch()
- return tab
-
- def _create_file_test_tab(self):
- """Create file-based OCR test tab for testing with saved screenshots."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- # Info
- info = QLabel("Test OCR on an image file (PNG, JPG, BMP)")
- info.setStyleSheet("color: #888;")
- layout.addWidget(info)
-
- # File selection
- file_layout = QHBoxLayout()
-
- self.file_path_label = QLabel("No file selected")
- self.file_path_label.setStyleSheet("color: #aaa; padding: 8px; background-color: #1a1f2e; border-radius: 4px;")
- file_layout.addWidget(self.file_path_label, 1)
-
- browse_btn = QPushButton("Browse...")
- browse_btn.setStyleSheet("""
- QPushButton {
- background-color: #4ecdc4;
- color: #141f23;
- font-weight: bold;
- padding: 8px 16px;
- }
- """)
- browse_btn.clicked.connect(self._browse_image_file)
- file_layout.addWidget(browse_btn)
-
- layout.addLayout(file_layout)
-
- # Backend selection
- backend_layout = QHBoxLayout()
- backend_layout.addWidget(QLabel("OCR Backend:"))
- self.file_backend_combo = QComboBox()
- self.file_backend_combo.addItems(["Auto (try all)", "EasyOCR", "Tesseract", "PaddleOCR"])
- backend_layout.addWidget(self.file_backend_combo)
- backend_layout.addStretch()
- layout.addLayout(backend_layout)
-
- # Test button
- file_test_btn = QPushButton("โถ Run OCR on File")
- file_test_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: #141f23;
- font-weight: bold;
- padding: 12px;
- font-size: 14px;
- }
- """)
- file_test_btn.clicked.connect(self._run_file_test)
- layout.addWidget(file_test_btn)
-
- # Results
- results_group = QGroupBox("OCR Results")
- results_layout = QVBoxLayout(results_group)
-
- self.file_results_text = QTextEdit()
- self.file_results_text.setReadOnly(True)
- self.file_results_text.setPlaceholderText("OCR results will appear here...")
- self.file_results_text.setStyleSheet("""
- QTextEdit {
- background-color: #0d1117;
- color: #c9d1d9;
- font-family: Consolas, monospace;
- font-size: 12px;
- }
- """)
- results_layout.addWidget(self.file_results_text)
-
- # Stats
- self.file_stats_label = QLabel("File: - | Backend: - | Status: Waiting")
- self.file_stats_label.setStyleSheet("color: #888;")
- results_layout.addWidget(self.file_stats_label)
-
- layout.addWidget(results_group, 1)
-
- layout.addStretch()
- return tab
-
- def _create_region_test_tab(self):
- """Create region test tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- info = QLabel("Test OCR on specific screen region")
- info.setStyleSheet("color: #888;")
- layout.addWidget(info)
-
- # Region input
- region_group = QGroupBox("Screen Region (pixels)")
- region_layout = QHBoxLayout(region_group)
-
- self.x_input = QSpinBox()
- self.x_input.setRange(0, 10000)
- self.x_input.setValue(100)
- region_layout.addWidget(QLabel("X:"))
- region_layout.addWidget(self.x_input)
-
- self.y_input = QSpinBox()
- self.y_input.setRange(0, 10000)
- self.y_input.setValue(100)
- region_layout.addWidget(QLabel("Y:"))
- region_layout.addWidget(self.y_input)
-
- self.w_input = QSpinBox()
- self.w_input.setRange(100, 10000)
- self.w_input.setValue(400)
- region_layout.addWidget(QLabel("Width:"))
- region_layout.addWidget(self.w_input)
-
- self.h_input = QSpinBox()
- self.h_input.setRange(100, 10000)
- self.h_input.setValue(300)
- region_layout.addWidget(QLabel("Height:"))
- region_layout.addWidget(self.h_input)
-
- layout.addWidget(region_group)
-
- # Presets
- preset_layout = QHBoxLayout()
- preset_layout.addWidget(QLabel("Quick Presets:"))
-
- presets = [
- ("Chat Window", 10, 800, 600, 200),
- ("Skills Window", 100, 100, 500, 400),
- ("Inventory", 1200, 200, 600, 500),
- ("Mission Tracker", 1600, 100, 300, 600),
- ]
-
- for name, x, y, w, h in presets:
- btn = QPushButton(name)
- btn.clicked.connect(lambda checked, px=x, py=y, pw=w, ph=h: self._set_region(px, py, pw, ph))
- preset_layout.addWidget(btn)
-
- preset_layout.addStretch()
- layout.addLayout(preset_layout)
-
- # Test button
- region_test_btn = QPushButton("โถ Test Region OCR")
- region_test_btn.setStyleSheet("""
- QPushButton {
- background-color: #4ecdc4;
- color: #141f23;
- font-weight: bold;
- padding: 10px;
- }
- """)
- region_test_btn.clicked.connect(self._run_region_test)
- layout.addWidget(region_test_btn)
-
- # Results
- self.region_results = QTextEdit()
- self.region_results.setReadOnly(True)
- self.region_results.setPlaceholderText("Region OCR results will appear here...")
- self.region_results.setStyleSheet("""
- QTextEdit {
- background-color: #0d1117;
- color: #c9d1d9;
- font-family: Consolas, monospace;
- }
- """)
- layout.addWidget(self.region_results)
-
- layout.addStretch()
- return tab
-
- def _create_history_tab(self):
- """Create history tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- info = QLabel("History of OCR tests")
- info.setStyleSheet("color: #888;")
- layout.addWidget(info)
-
- self.history_text = QTextEdit()
- self.history_text.setReadOnly(True)
- self.history_text.setStyleSheet("""
- QTextEdit {
- background-color: #0d1117;
- color: #c9d1d9;
- }
- """)
- layout.addWidget(self.history_text)
-
- # Controls
- btn_layout = QHBoxLayout()
-
- refresh_btn = QPushButton("๐ Refresh")
- refresh_btn.clicked.connect(self._update_history)
- btn_layout.addWidget(refresh_btn)
-
- clear_hist_btn = QPushButton("๐ Clear History")
- clear_hist_btn.clicked.connect(self._clear_history)
- btn_layout.addWidget(clear_hist_btn)
-
- btn_layout.addStretch()
- layout.addLayout(btn_layout)
-
- return tab
-
- def _create_skills_parser_tab(self):
- """Create skills parser tab for testing skill window OCR."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- info = QLabel("๐ Skills Window Parser - Extract skills from the EU Skills window")
- info.setStyleSheet("color: #888;")
- layout.addWidget(info)
-
- # Instructions
- instructions = QLabel(
- "1. Open your Skills window in Entropia Universe\n"
- "2. Click 'Capture Skills Window' below\n"
- "3. View parsed skills in the table"
- )
- instructions.setStyleSheet("color: #666; font-size: 11px;")
- layout.addWidget(instructions)
-
- # Capture button
- capture_btn = QPushButton("๐ท Capture Skills Window")
- capture_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: #141f23;
- font-weight: bold;
- padding: 12px;
- font-size: 14px;
- }
- """)
- capture_btn.clicked.connect(self._capture_and_parse_skills)
- layout.addWidget(capture_btn)
-
- # Results table
- from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView
-
- self.skills_table = QTableWidget()
- self.skills_table.setColumnCount(3)
- self.skills_table.setHorizontalHeaderLabels(["Skill Name", "Rank", "Points"])
- self.skills_table.horizontalHeader().setStretchLastSection(True)
- self.skills_table.setStyleSheet("""
- QTableWidget {
- background-color: #0d1117;
- border: 1px solid #333;
- }
- QTableWidget::item {
- padding: 6px;
- color: #c9d1d9;
- }
- QHeaderView::section {
- background-color: #1a1f2e;
- color: #ff8c42;
- padding: 8px;
- font-weight: bold;
- }
- """)
- layout.addWidget(self.skills_table, 1)
-
- # Stats label
- self.skills_stats_label = QLabel("No skills captured yet")
- self.skills_stats_label.setStyleSheet("color: #888;")
- layout.addWidget(self.skills_stats_label)
-
- # Raw text view
- raw_group = QGroupBox("Raw OCR Text (for debugging)")
- raw_layout = QVBoxLayout(raw_group)
-
- self.skills_raw_text = QTextEdit()
- self.skills_raw_text.setReadOnly(True)
- self.skills_raw_text.setMaximumHeight(150)
- self.skills_raw_text.setStyleSheet("""
- QTextEdit {
- background-color: #0d1117;
- color: #666;
- font-family: Consolas, monospace;
- font-size: 10px;
- }
- """)
- raw_layout.addWidget(self.skills_raw_text)
- layout.addWidget(raw_group)
-
- layout.addStretch()
- return tab
-
- def _capture_and_parse_skills(self):
- """Capture screen and parse skills."""
- from PyQt6.QtCore import Qt
-
- self.skills_stats_label.setText("Capturing...")
- self.skills_stats_label.setStyleSheet("color: #4ecdc4;")
-
- # Run in thread to not block UI
- from threading import Thread
-
- def capture_and_parse():
- try:
- from PIL import ImageGrab
- import re
- from datetime import datetime
-
- # Capture screen
- screenshot = ImageGrab.grab()
-
- # Run OCR
- text = ""
- try:
- import easyocr
- reader = easyocr.Reader(['en'], gpu=False, verbose=False)
- import numpy as np
- result = reader.readtext(np.array(screenshot), detail=0, paragraph=False)
- text = '\n'.join(result)
- except Exception as e:
- # Fallback to raw OCR
- text = str(e)
-
- # Parse skills
- skills = self._parse_skills_from_text(text)
-
- # Update UI
- from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
- QMetaObject.invokeMethod(
- self.skills_raw_text,
- "setPlainText",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, text)
- )
-
- # Update table
- QMetaObject.invokeMethod(
- self, "_update_skills_table",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(object, skills)
- )
-
- # Update stats
- stats_text = f"Found {len(skills)} skills"
- QMetaObject.invokeMethod(
- self.skills_stats_label,
- "setText",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, stats_text)
- )
- QMetaObject.invokeMethod(
- self.skills_stats_label,
- "setStyleSheet",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, "color: #4ecdc4;")
- )
-
- except Exception as e:
- from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
- QMetaObject.invokeMethod(
- self.skills_stats_label,
- "setText",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, f"Error: {str(e)}")
- )
- QMetaObject.invokeMethod(
- self.skills_stats_label,
- "setStyleSheet",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, "color: #ff6b6b;")
- )
-
- thread = Thread(target=capture_and_parse)
- thread.daemon = True
- thread.start()
-
- def _parse_skills_from_text(self, text):
- """Parse skills from OCR text."""
- skills = {}
-
- # Ranks in Entropia Universe - multi-word first for proper matching
- SINGLE_RANKS = [
- 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
- 'Skilled', 'Expert', 'Professional', 'Master',
- 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
- 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
- ]
- MULTI_RANKS = ['Arch Master', 'Grand Master']
- ALL_RANKS = MULTI_RANKS + SINGLE_RANKS
- rank_pattern = '|'.join(ALL_RANKS)
-
- # Clean text
- text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
- text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
-
- lines = text.split('\n')
-
- for line in lines:
- line = line.strip()
- if not line or len(line) < 10:
- continue
-
- # Pattern: SkillName Rank Points
- match = re.search(
- rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)',
- line, re.IGNORECASE
- )
-
- if match:
- skill_name = match.group(1).strip()
- rank = match.group(2)
- points = int(match.group(3))
-
- # Clean skill name - remove "Skill" prefix
- skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
-
- if points > 0 and skill_name:
- skills[skill_name] = {'rank': rank, 'points': points}
-
- return skills
-
- def _update_skills_table(self, skills):
- """Update the skills table with parsed data."""
- self.skills_table.setRowCount(len(skills))
-
- for i, (skill_name, data) in enumerate(sorted(skills.items())):
- self.skills_table.setItem(i, 0, QTableWidgetItem(skill_name))
- self.skills_table.setItem(i, 1, QTableWidgetItem(data['rank']))
- self.skills_table.setItem(i, 2, QTableWidgetItem(str(data['points'])))
-
- self.skills_table.resizeColumnsToContents()
-
- def _create_calibration_tab(self):
- """Create calibration tab."""
- tab = QWidget()
- layout = QVBoxLayout(tab)
-
- info = QLabel("Calibration tools for optimizing OCR accuracy")
- info.setStyleSheet("color: #888;")
- layout.addWidget(info)
-
- # DPI awareness
- dpi_group = QGroupBox("Display Settings")
- dpi_layout = QVBoxLayout(dpi_group)
-
- self.dpi_label = QLabel("Detecting display DPI...")
- dpi_layout.addWidget(self.dpi_label)
-
- detect_dpi_btn = QPushButton("Detect Display Settings")
- detect_dpi_btn.clicked.connect(self._detect_display_settings)
- dpi_layout.addWidget(detect_dpi_btn)
-
- layout.addWidget(dpi_group)
-
- # Backend status
- backend_group = QGroupBox("OCR Backend Status")
- backend_layout = QVBoxLayout(backend_group)
-
- self.backend_status = QTextEdit()
- self.backend_status.setReadOnly(True)
- self.backend_status.setMaximumHeight(200)
- self._check_backends()
- backend_layout.addWidget(self.backend_status)
-
- # Install buttons
- install_layout = QHBoxLayout()
-
- install_easyocr_btn = QPushButton("๐ฆ Install EasyOCR")
- install_easyocr_btn.setStyleSheet("""
- QPushButton {
- background-color: #4ecdc4;
- color: #141f23;
- font-weight: bold;
- padding: 8px;
- }
- """)
- install_easyocr_btn.clicked.connect(self._install_easyocr)
- install_layout.addWidget(install_easyocr_btn)
-
- install_tesseract_btn = QPushButton("๐ฆ Install Tesseract Package")
- install_tesseract_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: #141f23;
- font-weight: bold;
- padding: 8px;
- }
- """)
- install_tesseract_btn.clicked.connect(self._install_pytesseract)
- install_layout.addWidget(install_tesseract_btn)
-
- detect_tesseract_btn = QPushButton("๐ Auto-Detect Tesseract")
- detect_tesseract_btn.setStyleSheet("""
- QPushButton {
- background-color: #a0aec0;
- color: #141f23;
- font-weight: bold;
- padding: 8px;
- }
- """)
- detect_tesseract_btn.clicked.connect(self._auto_detect_tesseract)
- install_layout.addWidget(detect_tesseract_btn)
-
- install_paddle_btn = QPushButton("๐ฆ Install PaddleOCR")
- install_paddle_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a5568;
- color: white;
- font-weight: bold;
- padding: 8px;
- }
- """)
- install_paddle_btn.clicked.connect(self._install_paddleocr)
- install_layout.addWidget(install_paddle_btn)
-
- backend_layout.addLayout(install_layout)
- layout.addWidget(backend_group)
-
- # Tips
- tips_group = QGroupBox("Tips for Best Results")
- tips_layout = QVBoxLayout(tips_group)
-
- tips_text = QLabel("""
- โข Make sure text is clearly visible and not blurry
- โข Use region selection to focus on specific text areas
- โข Higher resolution screens work better for OCR
- โข Close other windows to reduce background noise
- โข Ensure good contrast between text and background
- """)
- tips_text.setWordWrap(True)
- tips_text.setStyleSheet("color: #aaa;")
- tips_layout.addWidget(tips_text)
-
- layout.addWidget(tips_group)
-
- layout.addStretch()
- return tab
-
- def _set_region(self, x, y, w, h):
- """Set region values."""
- self.x_input.setValue(x)
- self.y_input.setValue(y)
- self.w_input.setValue(w)
- self.h_input.setValue(h)
-
- def _browse_image_file(self):
- """Browse for an image file to test OCR on."""
- file_path, _ = QFileDialog.getOpenFileName(
- None,
- "Select Image File",
- "",
- "Images (*.png *.jpg *.jpeg *.bmp *.tiff);;All Files (*)"
- )
- if file_path:
- self.selected_file_path = file_path
- self.file_path_label.setText(Path(file_path).name)
- self.file_path_label.setStyleSheet("color: #4ecdc4; padding: 8px; background-color: #1a1f2e; border-radius: 4px;")
-
- def _run_file_test(self):
- """Run OCR on the selected image file."""
- if not hasattr(self, 'selected_file_path') or not self.selected_file_path:
- QMessageBox.warning(None, "No File", "Please select an image file first!")
- return
-
- backend_map = {
- 0: 'auto',
- 1: 'easyocr',
- 2: 'tesseract',
- 3: 'paddle'
- }
- backend = backend_map.get(self.file_backend_combo.currentIndex(), 'auto')
-
- self.file_results_text.setPlainText("Processing...")
- self.file_stats_label.setText("Processing...")
-
- # Run OCR in a thread
- from threading import Thread
-
- def process_file():
- try:
- from PIL import Image
- import time
-
- start_time = time.time()
-
- # Load image
- image = Image.open(self.selected_file_path)
-
- # Try OCR backends
- text = ""
- backend_used = "none"
-
- if backend in ('auto', 'easyocr'):
- try:
- import easyocr
- import numpy as np
- reader = easyocr.Reader(['en'], gpu=False, verbose=False)
- # Convert PIL Image to numpy array for EasyOCR
- image_np = np.array(image)
- ocr_result = reader.readtext(
- image_np,
- detail=0,
- paragraph=True
- )
- text = '\n'.join(ocr_result)
- backend_used = "easyocr"
- except Exception as e:
- if backend == 'easyocr':
- raise e
-
- if not text and backend in ('auto', 'tesseract'):
- try:
- import pytesseract
- text = pytesseract.image_to_string(image)
- backend_used = "tesseract"
- except Exception as e:
- if backend == 'tesseract':
- error_msg = str(e)
- if "tesseract is not installed" in error_msg.lower() or "not in your path" in error_msg.lower():
- raise Exception(
- "Tesseract is not installed.\n\n"
- "To use Tesseract OCR:\n"
- "1. Download from: https://github.com/UB-Mannheim/tesseract/wiki\n"
- "2. Install to C:\\Program Files\\Tesseract-OCR\\\n"
- "3. Add to PATH or restart EU-Utility\n\n"
- "Alternatively, use EasyOCR (auto-installs): pip install easyocr"
- )
- raise e
-
- if not text and backend in ('auto', 'paddle'):
- try:
- from paddleocr import PaddleOCR
- # Try without show_log argument for compatibility
- try:
- ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False)
- except TypeError:
- # Older version without show_log
- ocr = PaddleOCR(use_angle_cls=True, lang='en')
- # Convert PIL to numpy for PaddleOCR
- import numpy as np
- image_np = np.array(image)
- result = ocr.ocr(image_np, cls=True)
- if result and result[0]:
- texts = [line[1][0] for line in result[0]]
- text = '\n'.join(texts)
- backend_used = "paddleocr"
- except Exception as e:
- if backend == 'paddle':
- raise e
-
- processing_time = time.time() - start_time
-
- # Update UI (thread-safe via Qt signals would be better, but this works for simple case)
- from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
- QMetaObject.invokeMethod(
- self.file_results_text,
- "setPlainText",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, text if text else "No text detected")
- )
- QMetaObject.invokeMethod(
- self.file_stats_label,
- "setText",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, f"File: {Path(self.selected_file_path).name} | Backend: {backend_used} | Time: {processing_time:.2f}s")
- )
-
- except Exception as e:
- from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
- QMetaObject.invokeMethod(
- self.file_results_text,
- "setPlainText",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, f"Error: {str(e)}")
- )
-
- thread = Thread(target=process_file)
- thread.daemon = True
- thread.start()
-
- def _run_quick_test(self):
- """Run quick OCR test."""
- if self.ocr_thread and self.ocr_thread.isRunning():
- return
-
- backend_map = {
- 0: 'auto',
- 1: 'easyocr',
- 2: 'tesseract',
- 3: 'paddle'
- }
- backend = backend_map.get(self.backend_combo.currentIndex(), 'auto')
-
- self.test_btn.setEnabled(False)
- self.test_btn.setText("โณ Running...")
- self.progress_bar.setValue(0)
-
- self.ocr_thread = OCRTestThread(backend=backend)
- self.ocr_thread.progress_update.connect(self._update_progress)
- self.ocr_thread.result_ready.connect(self._on_ocr_complete)
- self.ocr_thread.start()
-
- def _update_progress(self, value, message):
- """Update progress bar."""
- self.progress_bar.setValue(value)
- self.status_label.setText(message)
-
- def _on_ocr_complete(self, results):
- """Handle OCR completion."""
- self.test_btn.setEnabled(True)
- self.test_btn.setText("โถ Run OCR Test")
-
- if results['success']:
- self.results_text.setPlainText(results['text'])
- self.stats_label.setText(
- f"Backend: {results['backend_used']} | "
- f"Time: {results['processing_time']:.2f}s | "
- f"Status: โ
Success"
- )
-
- # Add to history
- self._add_to_history(results)
- else:
- self.results_text.setPlainText(f"Error: {results.get('error', 'Unknown error')}")
- self.stats_label.setText(f"Backend: {results.get('backend_used', '-')} | Status: โ Failed")
-
- def _run_region_test(self):
- """Run region OCR test."""
- region = (
- self.x_input.value(),
- self.y_input.value(),
- self.w_input.value(),
- self.h_input.value()
- )
-
- self.region_results.setPlainText("Running OCR on region...")
-
- # Use api's ocr_capture if available
- try:
- result = self.ocr_capture(region=region)
- self.region_results.setPlainText(result.get('text', 'No text detected'))
- except Exception as e:
- self.region_results.setPlainText(f"Error: {e}")
-
- def _save_text(self):
- """Save OCR text to file."""
- text = self.results_text.toPlainText()
- if not text:
- QMessageBox.warning(None, "Save Error", "No text to save!")
- return
-
- file_path, _ = QFileDialog.getSaveFileName(
- None, "Save OCR Text", "ocr_result.txt", "Text Files (*.txt)"
- )
- if file_path:
- with open(file_path, 'w', encoding='utf-8') as f:
- f.write(text)
- self.status_label.setText(f"Saved to {file_path}")
-
- def _copy_to_clipboard(self):
- """Copy text to clipboard."""
- text = self.results_text.toPlainText()
- if text:
- self.copy_to_clipboard(text)
- self.status_label.setText("Copied to clipboard!")
-
- def _clear_results(self):
- """Clear results."""
- self.results_text.clear()
- self.stats_label.setText("Backend: - | Time: - | Status: Waiting")
- self.progress_bar.setValue(0)
-
- def _add_to_history(self, results):
- """Add result to history."""
- from datetime import datetime
- entry = {
- 'time': datetime.now().strftime("%H:%M:%S"),
- 'backend': results['backend_used'],
- 'time_taken': results['processing_time'],
- 'text_preview': results['text'][:100] + "..." if len(results['text']) > 100 else results['text']
- }
- self.test_history.insert(0, entry)
- if len(self.test_history) > self.max_history:
- self.test_history = self.test_history[:self.max_history]
- self._update_history()
-
- def _update_history(self):
- """Update history display."""
- lines = []
- for entry in self.test_history:
- lines.append(f"[{entry['time']}] {entry['backend']} ({entry['time_taken']:.2f}s)")
- lines.append(f" {entry['text_preview']}")
- lines.append("")
- self.history_text.setPlainText('\n'.join(lines) if lines else "No history yet")
-
- def _clear_history(self):
- """Clear history."""
- self.test_history.clear()
- self._update_history()
-
- def _detect_display_settings(self):
- """Detect display settings."""
- try:
- from PyQt6.QtWidgets import QApplication
- from PyQt6.QtGui import QScreen
-
- app = QApplication.instance()
- if app:
- screens = app.screens()
- info = []
- for i, screen in enumerate(screens):
- geo = screen.geometry()
- dpi = screen.logicalDotsPerInch()
- info.append(f"Screen {i+1}: {geo.width()}x{geo.height()} @ {dpi:.0f} DPI")
- self.dpi_label.setText('\n'.join(info))
- except Exception as e:
- self.dpi_label.setText(f"Error: {e}")
-
- def _install_easyocr(self):
- """Install EasyOCR backend."""
- from core.ocr_backend_manager import get_ocr_backend_manager
- manager = get_ocr_backend_manager()
- success, message = manager.install_backend('easyocr')
- if success:
- self.notify_success("Installation Complete", message)
- else:
- self.notify_error("Installation Failed", message)
- self._check_backends()
-
- def _install_pytesseract(self):
- """Install pytesseract Python package."""
- from core.ocr_backend_manager import get_ocr_backend_manager
- manager = get_ocr_backend_manager()
- success, message = manager.install_backend('tesseract')
- if success:
- self.notify_success("Installation Complete", message + "\n\nNote: You also need to install the Tesseract binary from:\nhttps://github.com/UB-Mannheim/tesseract/wiki")
- else:
- self.notify_error("Installation Failed", message)
- self._check_backends()
-
- def _install_paddleocr(self):
- """Install PaddleOCR backend."""
- from core.ocr_backend_manager import get_ocr_backend_manager
- manager = get_ocr_backend_manager()
- success, message = manager.install_backend('paddleocr')
- if success:
- self.notify_success("Installation Complete", message)
- else:
- self.notify_error("Installation Failed", message)
- self._check_backends()
-
- def _auto_detect_tesseract(self):
- """Auto-detect Tesseract from registry/paths."""
- from core.ocr_backend_manager import get_ocr_backend_manager
- manager = get_ocr_backend_manager()
- if manager.auto_configure_tesseract():
- self.notify_success("Tesseract Found", f"Auto-configured Tesseract at:\n{manager.backends['tesseract']['path']}")
- else:
- self.notify_warning("Not Found", "Could not find Tesseract installation.\n\nPlease install from:\nhttps://github.com/UB-Mannheim/tesseract/wiki")
- self._check_backends()
-
- def _check_backends(self):
- """Check OCR backend availability with install buttons."""
- from core.ocr_backend_manager import get_ocr_backend_manager
-
- manager = get_ocr_backend_manager()
- statuses = []
-
- # EasyOCR
- easyocr_status = manager.get_backend_status('easyocr')
- if easyocr_status['available']:
- statuses.append("โ
EasyOCR - Available (recommended)")
- else:
- statuses.append("โ EasyOCR - Not installed\n Click 'Install EasyOCR' button below")
-
- # Tesseract
- tesseract_status = manager.get_backend_status('tesseract')
- if tesseract_status['available']:
- path_info = f" at {tesseract_status['path']}" if tesseract_status['path'] else ""
- statuses.append(f"โ
Tesseract - Available{path_info}")
- elif tesseract_status['installed']:
- statuses.append("โ ๏ธ Tesseract - Python package installed but binary not found\n Click 'Auto-Detect Tesseract' or install binary from:\n https://github.com/UB-Mannheim/tesseract/wiki")
- else:
- statuses.append("โ Tesseract - Not installed\n Click 'Install Tesseract Package' then install binary")
-
- # PaddleOCR
- paddle_status = manager.get_backend_status('paddleocr')
- if paddle_status['available']:
- statuses.append("โ
PaddleOCR - Available")
- else:
- statuses.append("โ PaddleOCR - Not installed\n Click 'Install PaddleOCR' button below")
-
- statuses.append("\n๐ก Recommendation: EasyOCR is easiest (auto-downloads models)")
-
- self.backend_status.setPlainText('\n'.join(statuses))
-
- def shutdown(self):
- """Clean up."""
- if self.ocr_thread and self.ocr_thread.isRunning():
- self.ocr_thread.wait(1000)
- super().shutdown()
diff --git a/plugins/global_tracker/__init__.py b/plugins/global_tracker/__init__.py
deleted file mode 100644
index 4d0572d..0000000
--- a/plugins/global_tracker/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Global Tracker Plugin
-"""
-
-from .plugin import GlobalTrackerPlugin
-
-__all__ = ["GlobalTrackerPlugin"]
diff --git a/plugins/global_tracker/plugin.py b/plugins/global_tracker/plugin.py
deleted file mode 100644
index 935d09a..0000000
--- a/plugins/global_tracker/plugin.py
+++ /dev/null
@@ -1,257 +0,0 @@
-"""
-EU-Utility - Global Tracker Plugin
-
-Track globals, HOFs, with notifications.
-"""
-
-import json
-from datetime import datetime, timedelta
-from pathlib import Path
-from collections import deque
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem,
- QCheckBox, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-from core.icon_manager import get_icon_manager
-from PyQt6.QtGui import QPixmap
-
-
-class GlobalTrackerPlugin(BasePlugin):
- """Track globals and HOFs with stats."""
-
- name = "Global Tracker"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track globals, HOFs, and ATHs"
- hotkey = "ctrl+shift+g"
-
- def initialize(self):
- """Setup global tracker."""
- self.data_file = Path("data/globals.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.globals = deque(maxlen=1000)
- self.my_globals = []
- self.notifications_enabled = True
-
- self._load_data()
-
- def _load_data(self):
- """Load global data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.globals.extend(data.get('globals', []))
- self.my_globals = data.get('my_globals', [])
- except:
- pass
-
- def _save_data(self):
- """Save global data."""
- with open(self.data_file, 'w') as f:
- json.dump({
- 'globals': list(self.globals),
- 'my_globals': self.my_globals
- }, f, indent=2)
-
- def get_ui(self):
- """Create global tracker UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Get icon manager
- icon_mgr = get_icon_manager()
-
- # Title with icon
- title_layout = QHBoxLayout()
-
- title_icon = QLabel()
- icon_pixmap = icon_mgr.get_pixmap('dollar-sign', size=20)
- title_icon.setPixmap(icon_pixmap)
- title_icon.setFixedSize(20, 20)
- title_layout.addWidget(title_icon)
-
- title = QLabel("Global Tracker")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- title_layout.addWidget(title)
- title_layout.addStretch()
-
- layout.addLayout(title_layout)
-
- # Stats
- stats_frame = QFrame()
- stats_frame.setStyleSheet("""
- QFrame {
- background-color: rgba(30, 35, 45, 200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- """)
- stats_layout = QHBoxLayout(stats_frame)
-
- total = len(self.globals)
- hofs = sum(1 for g in self.globals if g.get('value', 0) >= 1000)
-
- self.total_label = QLabel(f"Globals: {total}")
- self.total_label.setStyleSheet("color: #4caf50; font-weight: bold;")
- stats_layout.addWidget(self.total_label)
-
- self.hof_label = QLabel(f"HOFs: {hofs}")
- self.hof_label.setStyleSheet("color: #ffc107; font-weight: bold;")
- stats_layout.addWidget(self.hof_label)
-
- self.my_label = QLabel(f"My Globals: {len(self.my_globals)}")
- self.my_label.setStyleSheet("color: #ff8c42; font-weight: bold;")
- stats_layout.addWidget(self.my_label)
-
- stats_layout.addStretch()
- layout.addWidget(stats_frame)
-
- # Filters
- filters = QHBoxLayout()
- self.show_mine_cb = QCheckBox("Show only my globals")
- self.show_mine_cb.setStyleSheet("color: white;")
- filters.addWidget(self.show_mine_cb)
-
- self.show_hof_cb = QCheckBox("HOFs only")
- self.show_hof_cb.setStyleSheet("color: white;")
- filters.addWidget(self.show_hof_cb)
-
- filters.addStretch()
- layout.addLayout(filters)
-
- # Globals table
- self.globals_table = QTableWidget()
- self.globals_table.setColumnCount(5)
- self.globals_table.setHorizontalHeaderLabels(["Time", "Player", "Mob/Item", "Value", "Type"])
- self.globals_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 8px;
- font-weight: bold;
- font-size: 11px;
- }
- """)
- self.globals_table.horizontalHeader().setStretchLastSection(True)
- layout.addWidget(self.globals_table)
-
- self._refresh_globals()
-
- # Test buttons
- test_layout = QHBoxLayout()
-
- test_global = QPushButton("Test Global")
- test_global.setStyleSheet("""
- QPushButton {
- background-color: #4caf50;
- color: white;
- padding: 8px;
- border: none;
- border-radius: 4px;
- }
- """)
- test_global.clicked.connect(self._test_global)
- test_layout.addWidget(test_global)
-
- test_hof = QPushButton("Test HOF")
- test_hof.setStyleSheet("""
- QPushButton {
- background-color: #ffc107;
- color: black;
- padding: 8px;
- border: none;
- border-radius: 4px;
- }
- """)
- test_hof.clicked.connect(self._test_hof)
- test_layout.addWidget(test_hof)
-
- test_layout.addStretch()
- layout.addLayout(test_layout)
-
- layout.addStretch()
- return widget
-
- def _refresh_globals(self):
- """Refresh globals table."""
- recent = list(self.globals)[-50:] # Last 50
-
- self.globals_table.setRowCount(len(recent))
- for i, g in enumerate(reversed(recent)):
- self.globals_table.setItem(i, 0, QTableWidgetItem(g.get('time', '-')[-8:]))
- self.globals_table.setItem(i, 1, QTableWidgetItem(g.get('player', 'Unknown')))
- self.globals_table.setItem(i, 2, QTableWidgetItem(g.get('target', 'Unknown')))
-
- value_item = QTableWidgetItem(f"{g.get('value', 0):.2f} PED")
- value = g.get('value', 0)
- if value >= 10000:
- value_item.setForeground(Qt.GlobalColor.magenta)
- elif value >= 1000:
- value_item.setForeground(Qt.GlobalColor.yellow)
- elif value >= 50:
- value_item.setForeground(Qt.GlobalColor.green)
- self.globals_table.setItem(i, 3, value_item)
-
- g_type = "ATH" if value >= 10000 else "HOF" if value >= 1000 else "Global"
- self.globals_table.setItem(i, 4, QTableWidgetItem(g_type))
-
- def _test_global(self):
- """Add test global."""
- self.add_global("TestPlayer", "Argo Scout", 45.23, is_mine=True)
- self._refresh_globals()
-
- def _test_hof(self):
- """Add test HOF."""
- self.add_global("TestPlayer", "Oratan Miner", 1250.00, is_mine=True)
- self._refresh_globals()
-
- def add_global(self, player, target, value, is_mine=False):
- """Add a global."""
- entry = {
- 'time': datetime.now().isoformat(),
- 'player': player,
- 'target': target,
- 'value': value,
- 'is_mine': is_mine
- }
-
- self.globals.append(entry)
-
- if is_mine:
- self.my_globals.append(entry)
-
- self._save_data()
- self._refresh_globals()
-
- def parse_chat_message(self, message):
- """Parse global from chat."""
- # Look for global patterns
- import re
-
- # Example: "Player killed Argo Scout worth 45.23 PED!"
- pattern = r'(\w+)\s+(?:killed|mined|crafted)\s+(.+?)\s+worth\s+([\d.]+)\s+PED'
- match = re.search(pattern, message, re.IGNORECASE)
-
- if match:
- player = match.group(1)
- target = match.group(2)
- value = float(match.group(3))
-
- is_mine = player == "You" or player == "Your avatar"
- self.add_global(player, target, value, is_mine)
diff --git a/plugins/import_export/__init__.py b/plugins/import_export/__init__.py
deleted file mode 100644
index e1ed8ef..0000000
--- a/plugins/import_export/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .plugin import ImportExportPlugin
-
-__all__ = ['ImportExportPlugin']
diff --git a/plugins/import_export/plugin.py b/plugins/import_export/plugin.py
deleted file mode 100644
index ef72401..0000000
--- a/plugins/import_export/plugin.py
+++ /dev/null
@@ -1,333 +0,0 @@
-# Description: Universal Import/Export tool for EU-Utility
-# Author: LemonNexus
-# Version: 1.0.0
-
-"""
-Universal Import/Export tool for EU-Utility.
-Export and import all plugin data for backup and migration.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QFileDialog, QProgressBar,
- QListWidget, QListWidgetItem, QMessageBox, QComboBox
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal
-import json
-import zipfile
-import os
-from datetime import datetime
-from plugins.base_plugin import BasePlugin
-
-
-class ExportImportWorker(QThread):
- """Background worker for export/import operations."""
- progress = pyqtSignal(int)
- status = pyqtSignal(str)
- finished_signal = pyqtSignal(bool, str)
-
- def __init__(self, operation, data_store, path, selected_plugins=None):
- super().__init__()
- self.operation = operation
- self.data_store = data_store
- self.path = path
- self.selected_plugins = selected_plugins or []
-
- def run(self):
- try:
- if self.operation == "export":
- self._do_export()
- else:
- self._do_import()
- except Exception as e:
- self.finished_signal.emit(False, str(e))
-
- def _do_export(self):
- """Export data to zip file."""
- self.status.emit("Gathering plugin data...")
-
- # Get all plugin data
- all_data = {}
- plugin_keys = self.data_store.get_all_keys("*")
-
- total = len(plugin_keys)
- for i, key in enumerate(plugin_keys):
- # Check if filtered
- plugin_name = key.split('.')[0] if '.' in key else key
- if self.selected_plugins and plugin_name not in self.selected_plugins:
- continue
-
- data = self.data_store.load(key, None)
- if data is not None:
- all_data[key] = data
-
- progress = int((i + 1) / total * 50)
- self.progress.emit(progress)
-
- self.status.emit("Creating export archive...")
-
- # Create zip file
- with zipfile.ZipFile(self.path, 'w', zipfile.ZIP_DEFLATED) as zf:
- # Add metadata
- metadata = {
- 'version': '2.0.0',
- 'exported_at': datetime.now().isoformat(),
- 'plugin_count': len(set(k.split('.')[0] for k in all_data.keys())),
- }
- zf.writestr('metadata.json', json.dumps(metadata, indent=2))
-
- # Add data
- zf.writestr('data.json', json.dumps(all_data, indent=2))
-
- self.progress.emit(75)
-
- self.progress.emit(100)
- self.finished_signal.emit(True, f"Exported {len(all_data)} data items")
-
- def _do_import(self):
- """Import data from zip file."""
- self.status.emit("Reading archive...")
-
- with zipfile.ZipFile(self.path, 'r') as zf:
- # Verify metadata
- if 'metadata.json' not in zf.namelist():
- raise ValueError("Invalid export file: missing metadata")
-
- metadata = json.loads(zf.read('metadata.json'))
- self.status.emit(f"Importing from version {metadata.get('version', 'unknown')}...")
-
- # Load data
- data = json.loads(zf.read('data.json'))
- self.progress.emit(25)
-
- # Import
- total = len(data)
- for i, (key, value) in enumerate(data.items()):
- # Check if filtered
- plugin_name = key.split('.')[0] if '.' in key else key
- if self.selected_plugins and plugin_name not in self.selected_plugins:
- continue
-
- self.data_store.save_raw(key, value)
- progress = 25 + int((i + 1) / total * 75)
- self.progress.emit(progress)
-
- self.finished_signal.emit(True, f"Imported {len(data)} data items")
-
-
-class ImportExportPlugin(BasePlugin):
- """Universal import/export tool for EU-Utility data."""
-
- name = "Import/Export"
- version = "1.0.0"
- author = "LemonNexus"
- description = "Backup and restore all plugin data"
- icon = "archive"
-
- def initialize(self):
- """Initialize plugin."""
- self.worker = None
- self.default_export_dir = os.path.expanduser("~/Documents/EU-Utility/Backups")
- os.makedirs(self.default_export_dir, exist_ok=True)
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
-
- # Title
- title = QLabel("Import / Export Data")
- title.setStyleSheet("font-size: 18px; font-weight: bold;")
- layout.addWidget(title)
-
- # Description
- desc = QLabel("Backup and restore all your EU-Utility data")
- desc.setStyleSheet("color: rgba(255,255,255,150);")
- layout.addWidget(desc)
-
- # Plugin selection
- layout.addWidget(QLabel("Select plugins to export/import:"))
- self.plugin_list = QListWidget()
- self.plugin_list.setSelectionMode(QListWidget.SelectionMode.MultiSelection)
- self._populate_plugin_list()
- layout.addWidget(self.plugin_list)
-
- # Select all/none buttons
- select_layout = QHBoxLayout()
- select_all_btn = QPushButton("Select All")
- select_all_btn.clicked.connect(lambda: self.plugin_list.selectAll())
- select_layout.addWidget(select_all_btn)
-
- select_none_btn = QPushButton("Select None")
- select_none_btn.clicked.connect(lambda: self.plugin_list.clearSelection())
- select_layout.addWidget(select_none_btn)
- layout.addLayout(select_layout)
-
- # Progress
- self.progress_bar = QProgressBar()
- self.progress_bar.setVisible(False)
- layout.addWidget(self.progress_bar)
-
- self.status_label = QLabel("")
- self.status_label.setStyleSheet("color: #4a9eff;")
- layout.addWidget(self.status_label)
-
- # Export/Import buttons
- btn_layout = QHBoxLayout()
-
- export_btn = QPushButton("Export to File...")
- export_btn.setStyleSheet("""
- QPushButton {
- background-color: #4caf50;
- color: white;
- padding: 10px;
- font-weight: bold;
- }
- """)
- export_btn.clicked.connect(self._export)
- btn_layout.addWidget(export_btn)
-
- import_btn = QPushButton("Import from File...")
- import_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 10px;
- font-weight: bold;
- }
- """)
- import_btn.clicked.connect(self._import)
- btn_layout.addWidget(import_btn)
-
- layout.addLayout(btn_layout)
-
- # Info
- info = QLabel(
- "Exports include:\n"
- "- All plugin settings\n"
- "- Session history\n"
- "- Price alerts\n"
- "- Custom configurations\n\n"
- "Files are saved as .zip archives with JSON data."
- )
- info.setStyleSheet("color: rgba(255,255,255,150); font-size: 11px;")
- layout.addWidget(info)
-
- layout.addStretch()
- return widget
-
- def _populate_plugin_list(self):
- """Populate the plugin list."""
- # Get available plugins
- plugins = [
- "loot_tracker",
- "skill_scanner",
- "price_alerts",
- "session_exporter",
- "mining_helper",
- "mission_tracker",
- "codex_tracker",
- "auction_tracker",
- "settings"
- ]
-
- for plugin in plugins:
- item = QListWidgetItem(plugin.replace('_', ' ').title())
- item.setData(Qt.ItemDataRole.UserRole, plugin)
- self.plugin_list.addItem(item)
-
- def _get_selected_plugins(self):
- """Get list of selected plugin names."""
- selected = []
- for item in self.plugin_list.selectedItems():
- selected.append(item.data(Qt.ItemDataRole.UserRole))
- return selected
-
- def _export(self):
- """Export data to file."""
- selected = self._get_selected_plugins()
- if not selected:
- QMessageBox.warning(self.get_ui(), "No Selection", "Please select at least one plugin to export.")
- return
-
- # Get save location
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- default_name = f"eu-utility-backup-{timestamp}.zip"
-
- path, _ = QFileDialog.getSaveFileName(
- self.get_ui(),
- "Export Data",
- os.path.join(self.default_export_dir, default_name),
- "Zip Files (*.zip)"
- )
-
- if not path:
- return
-
- # Start export
- self._start_worker("export", path, selected)
-
- def _import(self):
- """Import data from file."""
- selected = self._get_selected_plugins()
- if not selected:
- QMessageBox.warning(self.get_ui(), "No Selection", "Please select at least one plugin to import.")
- return
-
- # Confirm import
- reply = QMessageBox.question(
- self.get_ui(),
- "Confirm Import",
- "This will overwrite existing data for selected plugins. Continue?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
-
- if reply != QMessageBox.StandardButton.Yes:
- return
-
- # Get file location
- path, _ = QFileDialog.getOpenFileName(
- self.get_ui(),
- "Import Data",
- self.default_export_dir,
- "Zip Files (*.zip)"
- )
-
- if not path:
- return
-
- # Start import
- self._start_worker("import", path, selected)
-
- def _start_worker(self, operation, path, selected):
- """Start background worker."""
- # Show progress
- self.progress_bar.setVisible(True)
- self.progress_bar.setValue(0)
-
- # Get data store from API
- data_store = self.api.services.get('data_store') if self.api else None
- if not data_store:
- QMessageBox.critical(self.get_ui(), "Error", "Data store not available")
- return
-
- # Create and start worker
- self.worker = ExportImportWorker(operation, data_store, path, selected)
- self.worker.progress.connect(self.progress_bar.setValue)
- self.worker.status.connect(self.status_label.setText)
- self.worker.finished_signal.connect(self._on_worker_finished)
- self.worker.start()
-
- def _on_worker_finished(self, success, message):
- """Handle worker completion."""
- self.progress_bar.setVisible(False)
-
- if success:
- self.status_label.setText(f"โ {message}")
- self.status_label.setStyleSheet("color: #4caf50;")
- QMessageBox.information(self.get_ui(), "Success", message)
- else:
- self.status_label.setText(f"โ {message}")
- self.status_label.setStyleSheet("color: #f44336;")
- QMessageBox.critical(self.get_ui(), "Error", message)
diff --git a/plugins/inventory_manager/__init__.py b/plugins/inventory_manager/__init__.py
deleted file mode 100644
index 64fccac..0000000
--- a/plugins/inventory_manager/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Inventory Manager Plugin
-"""
-
-from .plugin import InventoryManagerPlugin
-
-__all__ = ["InventoryManagerPlugin"]
diff --git a/plugins/inventory_manager/plugin.py b/plugins/inventory_manager/plugin.py
deleted file mode 100644
index 656aa90..0000000
--- a/plugins/inventory_manager/plugin.py
+++ /dev/null
@@ -1,219 +0,0 @@
-"""
-EU-Utility - Inventory Manager Plugin
-
-Track inventory items, value, and weight.
-"""
-
-import json
-from pathlib import Path
-from collections import defaultdict
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem,
- QLineEdit, QProgressBar, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class InventoryManagerPlugin(BasePlugin):
- """Track and manage inventory."""
-
- name = "Inventory"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track items, TT value, and weight"
- hotkey = "ctrl+shift+i"
-
- def initialize(self):
- """Setup inventory manager."""
- self.data_file = Path("data/inventory.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.items = []
- self.max_slots = 200
- self.max_weight = 355.0
-
- self._load_data()
-
- def _load_data(self):
- """Load inventory data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.items = data.get('items', [])
- except:
- pass
-
- def _save_data(self):
- """Save inventory data."""
- with open(self.data_file, 'w') as f:
- json.dump({'items': self.items}, f, indent=2)
-
- def get_ui(self):
- """Create inventory UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("๐ Inventory Manager")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Stats
- stats_frame = QFrame()
- stats_frame.setStyleSheet("""
- QFrame {
- background-color: rgba(30, 35, 45, 200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- """)
- stats_layout = QVBoxLayout(stats_frame)
-
- # Slots
- slots_used = len(self.items)
- slots_layout = QHBoxLayout()
- slots_layout.addWidget(QLabel("Slots:"))
- self.slots_progress = QProgressBar()
- self.slots_progress.setMaximum(self.max_slots)
- self.slots_progress.setValue(slots_used)
- self.slots_progress.setStyleSheet("""
- QProgressBar {
- background-color: rgba(60, 70, 90, 150);
- border: none;
- border-radius: 3px;
- text-align: center;
- color: white;
- }
- QProgressBar::chunk {
- background-color: #4a9eff;
- border-radius: 3px;
- }
- """)
- slots_layout.addWidget(self.slots_progress)
- self.slots_label = QLabel(f"{slots_used}/{self.max_slots}")
- self.slots_label.setStyleSheet("color: white;")
- slots_layout.addWidget(self.slots_label)
- stats_layout.addLayout(slots_layout)
-
- # Weight
- total_weight = sum(item.get('weight', 0) for item in self.items)
- weight_layout = QHBoxLayout()
- weight_layout.addWidget(QLabel("Weight:"))
- self.weight_progress = QProgressBar()
- self.weight_progress.setMaximum(int(self.max_weight))
- self.weight_progress.setValue(int(total_weight))
- self.weight_progress.setStyleSheet("""
- QProgressBar {
- background-color: rgba(60, 70, 90, 150);
- border: none;
- border-radius: 3px;
- text-align: center;
- color: white;
- }
- QProgressBar::chunk {
- background-color: #ffc107;
- border-radius: 3px;
- }
- """)
- weight_layout.addWidget(self.weight_progress)
- self.weight_label = QLabel(f"{total_weight:.1f}/{self.max_weight} kg")
- self.weight_label.setStyleSheet("color: white;")
- weight_layout.addWidget(self.weight_label)
- stats_layout.addLayout(weight_layout)
-
- # PED value
- total_tt = sum(item.get('tt', 0) for item in self.items)
- ped_label = QLabel(f"๐ฐ Total TT: {total_tt:.2f} PED")
- ped_label.setStyleSheet("color: #ffc107; font-weight: bold;")
- stats_layout.addWidget(ped_label)
-
- layout.addWidget(stats_frame)
-
- # Items table
- self.items_table = QTableWidget()
- self.items_table.setColumnCount(5)
- self.items_table.setHorizontalHeaderLabels(["Item", "Qty", "TT", "Weight", "Container"])
- self.items_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 8px;
- font-weight: bold;
- font-size: 11px;
- }
- """)
- self.items_table.horizontalHeader().setStretchLastSection(True)
- layout.addWidget(self.items_table)
-
- self._refresh_items()
-
- # Scan button
- scan_btn = QPushButton("Scan Inventory")
- scan_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- scan_btn.clicked.connect(self._scan_inventory)
- layout.addWidget(scan_btn)
-
- layout.addStretch()
- return widget
-
- def _refresh_items(self):
- """Refresh items table."""
- self.items_table.setRowCount(len(self.items))
-
- for i, item in enumerate(self.items):
- self.items_table.setItem(i, 0, QTableWidgetItem(item.get('name', 'Unknown')))
- self.items_table.setItem(i, 1, QTableWidgetItem(str(item.get('quantity', 1))))
- self.items_table.setItem(i, 2, QTableWidgetItem(f"{item.get('tt', 0):.2f}"))
- self.items_table.setItem(i, 3, QTableWidgetItem(f"{item.get('weight', 0):.2f}"))
- self.items_table.setItem(i, 4, QTableWidgetItem(item.get('container', 'Main')))
-
- def _scan_inventory(self):
- """Scan inventory with OCR."""
- # TODO: Implement OCR
- pass
-
- def add_item(self, name, quantity=1, tt=0.0, weight=0.0, container='Main'):
- """Add item to inventory."""
- # Check if exists
- for item in self.items:
- if item['name'] == name and item['container'] == container:
- item['quantity'] += quantity
- item['tt'] += tt
- item['weight'] += weight
- self._save_data()
- self._refresh_items()
- return
-
- # New item
- self.items.append({
- 'name': name,
- 'quantity': quantity,
- 'tt': tt,
- 'weight': weight,
- 'container': container
- })
- self._save_data()
- self._refresh_items()
diff --git a/plugins/log_parser_test/__init__.py b/plugins/log_parser_test/__init__.py
deleted file mode 100644
index eeaca4c..0000000
--- a/plugins/log_parser_test/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""Log Parser Test Plugin."""
-from .plugin import LogParserTestPlugin
diff --git a/plugins/log_parser_test/plugin.py b/plugins/log_parser_test/plugin.py
deleted file mode 100644
index bbfffe8..0000000
--- a/plugins/log_parser_test/plugin.py
+++ /dev/null
@@ -1,384 +0,0 @@
-"""
-EU-Utility - Log Parser Test Plugin
-
-Debug and test tool for the Log Reader core service.
-Monitors log parsing in real-time with detailed output.
-"""
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
- QLabel, QPushButton, QComboBox, QCheckBox,
- QSpinBox, QGroupBox, QSplitter, QFrame,
- QTableWidget, QTableWidgetItem, QHeaderView
-)
-from PyQt6.QtCore import Qt, QTimer
-from PyQt6.QtGui import QColor, QFont
-
-from plugins.base_plugin import BasePlugin
-from core.event_bus import SkillGainEvent, LootEvent, DamageEvent, GlobalEvent
-
-
-class LogParserTestPlugin(BasePlugin):
- """Test and debug tool for log parsing functionality."""
-
- name = "Log Parser Test"
- version = "1.0.0"
- author = "EU-Utility"
- description = "Debug tool for testing log parsing and event detection"
-
- # Example: This plugin could depend on Game Reader Test for shared OCR utilities
- # dependencies = {
- # 'plugins': ['plugins.game_reader_test.plugin.GameReaderTestPlugin']
- # }
-
- def __init__(self, overlay_window, config):
- super().__init__(overlay_window, config)
- self.event_counts = {
- 'skill_gain': 0,
- 'loot': 0,
- 'global': 0,
- 'damage': 0,
- 'damage_taken': 0,
- 'heal': 0,
- 'mission_complete': 0,
- 'tier_increase': 0,
- 'enhancer_break': 0,
- 'unknown': 0
- }
- self.recent_events = []
- self.max_events = 100
- self.auto_scroll = True
-
- def initialize(self):
- """Initialize plugin and subscribe to events."""
- # Subscribe to all event types for testing
- self.subscribe_typed(SkillGainEvent, self._on_skill_gain)
- self.subscribe_typed(LootEvent, self._on_loot)
- self.subscribe_typed(DamageEvent, self._on_damage)
- self.subscribe_typed(GlobalEvent, self._on_global)
-
- # Also subscribe to raw log events via API
- api = self._get_api()
- if api:
- api.subscribe('log_event', self._on_raw_log_event)
-
- self.log_info("Log Parser Test initialized - monitoring events...")
-
- def _get_api(self):
- """Get PluginAPI instance."""
- try:
- from core.plugin_api import get_api
- return get_api()
- except:
- return None
-
- def _on_skill_gain(self, event: SkillGainEvent):
- """Handle skill gain events."""
- self.event_counts['skill_gain'] += 1
- self._add_event("Skill Gain", f"{event.skill_name}: +{event.gain_amount} pts", "#4ecdc4")
-
- def _on_loot(self, event: LootEvent):
- """Handle loot events."""
- self.event_counts['loot'] += 1
- # LootEvent has items list, extract first item for display
- if event.items:
- item = event.items[0]
- item_name = item.get('name', 'Unknown')
- quantity = item.get('quantity', 1)
- value = item.get('value', 0)
- value_str = f" ({value:.2f} PED)" if value else ""
- self._add_event("Loot", f"{item_name} x{quantity}{value_str} from {event.mob_name}", "#ff8c42")
- else:
- self._add_event("Loot", f"Loot from {event.mob_name}", "#ff8c42")
-
- def _on_damage(self, event: DamageEvent):
- """Handle damage events."""
- if event.is_outgoing:
- self.event_counts['damage'] += 1
- self._add_event("Damage", f"{event.damage_amount} damage ({event.damage_type})", "#ff6b6b")
- else:
- self.event_counts['damage_taken'] += 1
- self._add_event("Damage Taken", f"{event.damage_amount} damage from {event.attacker_name}", "#ff4757")
-
- def _on_global(self, event: GlobalEvent):
- """Handle global events."""
- self.event_counts['global'] += 1
- item_str = f" with {event.item_name}" if event.item_name else ""
- self._add_event("Global", f"{event.player_name} got {event.achievement_type}{item_str} worth {event.value} PED!", "#ffd93d")
-
- def _on_raw_log_event(self, event_data):
- """Handle raw log events."""
- event_type = event_data.get('event_type', 'unknown')
- self.event_counts[event_type] = self.event_counts.get(event_type, 0) + 1
-
- def _add_event(self, event_type: str, details: str, color: str):
- """Add event to recent events list."""
- from datetime import datetime
- timestamp = datetime.now().strftime("%H:%M:%S")
-
- self.recent_events.insert(0, {
- 'time': timestamp,
- 'type': event_type,
- 'details': details,
- 'color': color
- })
-
- # Trim to max
- if len(self.recent_events) > self.max_events:
- self.recent_events = self.recent_events[:self.max_events]
-
- # Update UI if available
- if hasattr(self, 'events_table'):
- self._update_events_table()
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(12)
-
- # Header
- header = QLabel("๐ Log Parser Test & Debug Tool")
- header.setStyleSheet("font-size: 16px; font-weight: bold; color: #ff8c42;")
- layout.addWidget(header)
-
- # Status bar
- status_frame = QFrame()
- status_frame.setStyleSheet("background-color: #1a1f2e; border-radius: 6px; padding: 8px;")
- status_layout = QHBoxLayout(status_frame)
-
- self.status_label = QLabel("Status: Monitoring...")
- self.status_label.setStyleSheet("color: #4ecdc4;")
- status_layout.addWidget(self.status_label)
- status_layout.addStretch()
-
- self.log_path_label = QLabel()
- self._update_log_path()
- self.log_path_label.setStyleSheet("color: #666;")
- status_layout.addWidget(self.log_path_label)
-
- layout.addWidget(status_frame)
-
- # Event counters
- counters_group = QGroupBox("Event Counters")
- counters_layout = QHBoxLayout(counters_group)
-
- self.counter_labels = {}
- for event_type, color in [
- ('skill_gain', '#4ecdc4'),
- ('loot', '#ff8c42'),
- ('global', '#ffd93d'),
- ('damage', '#ff6b6b'),
- ('damage_taken', '#ff4757')
- ]:
- frame = QFrame()
- frame.setStyleSheet(f"background-color: #1a1f2e; border-left: 3px solid {color}; padding: 8px;")
- frame_layout = QVBoxLayout(frame)
-
- name_label = QLabel(event_type.replace('_', ' ').title())
- name_label.setStyleSheet("color: #888; font-size: 10px;")
- frame_layout.addWidget(name_label)
-
- count_label = QLabel("0")
- count_label.setStyleSheet(f"color: {color}; font-size: 20px; font-weight: bold;")
- frame_layout.addWidget(count_label)
-
- self.counter_labels[event_type] = count_label
- counters_layout.addWidget(frame)
-
- layout.addWidget(counters_group)
-
- # Recent events table
- events_group = QGroupBox("Recent Events (last 100)")
- events_layout = QVBoxLayout(events_group)
-
- self.events_table = QTableWidget()
- self.events_table.setColumnCount(3)
- self.events_table.setHorizontalHeaderLabels(["Time", "Type", "Details"])
- self.events_table.horizontalHeader().setStretchLastSection(True)
- self.events_table.setStyleSheet("""
- QTableWidget {
- background-color: #141f23;
- border: 1px solid #333;
- }
- QTableWidget::item {
- padding: 6px;
- }
- """)
- events_layout.addWidget(self.events_table)
-
- # Auto-scroll checkbox
- self.auto_scroll_cb = QCheckBox("Auto-scroll to new events")
- self.auto_scroll_cb.setChecked(True)
- events_layout.addWidget(self.auto_scroll_cb)
-
- layout.addWidget(events_group, 1) # Stretch factor 1
-
- # Test controls
- controls_group = QGroupBox("Test Controls")
- controls_layout = QHBoxLayout(controls_group)
-
- # Simulate event buttons
- btn_layout = QHBoxLayout()
-
- test_skill_btn = QPushButton("Test: Skill Gain")
- test_skill_btn.clicked.connect(self._simulate_skill_gain)
- btn_layout.addWidget(test_skill_btn)
-
- test_loot_btn = QPushButton("Test: Loot")
- test_loot_btn.clicked.connect(self._simulate_loot)
- btn_layout.addWidget(test_loot_btn)
-
- test_damage_btn = QPushButton("Test: Damage")
- test_damage_btn.clicked.connect(self._simulate_damage)
- btn_layout.addWidget(test_damage_btn)
-
- clear_btn = QPushButton("Clear Events")
- clear_btn.clicked.connect(self._clear_events)
- btn_layout.addWidget(clear_btn)
-
- controls_layout.addLayout(btn_layout)
- controls_layout.addStretch()
-
- layout.addWidget(controls_group)
-
- # Raw log view
- raw_group = QGroupBox("Raw Log Lines (last 50)")
- raw_layout = QVBoxLayout(raw_group)
-
- self.raw_log_text = QTextEdit()
- self.raw_log_text.setReadOnly(True)
- self.raw_log_text.setStyleSheet("""
- QTextEdit {
- background-color: #0d1117;
- color: #c9d1d9;
- font-family: Consolas, monospace;
- font-size: 11px;
- }
- """)
- raw_layout.addWidget(self.raw_log_text)
-
- layout.addWidget(raw_group)
-
- # Start update timer
- self.update_timer = QTimer()
- self.update_timer.timeout.connect(self._update_counters)
- self.update_timer.start(1000) # Update every second
-
- # Subscribe to raw log lines
- self.read_log(lines=50) # Pre-fill with recent log
-
- return widget
-
- def _update_log_path(self):
- """Update log path display."""
- try:
- from core.log_reader import LogReader
- reader = LogReader()
- if reader.log_path:
- self.log_path_label.setText(f"Log: {reader.log_path}")
- else:
- self.log_path_label.setText("Log: Not found")
- except Exception as e:
- self.log_path_label.setText(f"Log: Error - {e}")
-
- def _update_counters(self):
- """Update counter displays."""
- for event_type, label in self.counter_labels.items():
- count = self.event_counts.get(event_type, 0)
- label.setText(str(count))
-
- def _update_events_table(self):
- """Update events table with recent events."""
- self.events_table.setRowCount(len(self.recent_events))
-
- for i, event in enumerate(self.recent_events):
- # Time
- time_item = QTableWidgetItem(event['time'])
- time_item.setForeground(QColor("#888"))
- self.events_table.setItem(i, 0, time_item)
-
- # Type
- type_item = QTableWidgetItem(event['type'])
- type_item.setForeground(QColor(event['color']))
- type_item.setFont(QFont("Segoe UI", weight=QFont.Weight.Bold))
- self.events_table.setItem(i, 1, type_item)
-
- # Details
- details_item = QTableWidgetItem(event['details'])
- details_item.setForeground(QColor("#c9d1d9"))
- self.events_table.setItem(i, 2, details_item)
-
- # Auto-scroll
- if self.auto_scroll_cb.isChecked() and self.recent_events:
- self.events_table.scrollToTop()
-
- def _simulate_skill_gain(self):
- """Simulate a skill gain event for testing."""
- from datetime import datetime
- event = SkillGainEvent(
- timestamp=datetime.now(),
- skill_name="Rifle",
- gain_amount=0.1234,
- skill_value=1234.56
- )
- self._on_skill_gain(event)
- self.log_info("Simulated skill gain event")
-
- def _simulate_loot(self):
- """Simulate a loot event for testing."""
- from datetime import datetime
- event = LootEvent(
- timestamp=datetime.now(),
- mob_name="Atrox",
- items=[{'name': 'Shrapnel', 'quantity': 100, 'value': 1.0}],
- total_tt_value=1.0,
- position=None
- )
- self._on_loot(event)
- self.log_info("Simulated loot event")
-
- def _simulate_damage(self):
- """Simulate a damage event for testing."""
- from datetime import datetime
- event = DamageEvent(
- timestamp=datetime.now(),
- damage_amount=45,
- damage_type="Impact",
- is_critical=True,
- target_name="Atrox",
- attacker_name="Player",
- is_outgoing=True
- )
- self._on_damage(event)
- self.log_info("Simulated damage event")
-
- def _clear_events(self):
- """Clear all events."""
- self.recent_events.clear()
- for key in self.event_counts:
- self.event_counts[key] = 0
- self._update_events_table()
- self._update_counters()
- self.log_info("Cleared all events")
-
- def read_log(self, lines=50):
- """Read recent log lines."""
- try:
- # Call parent class method (BasePlugin.read_log)
- log_lines = super().read_log(lines=lines)
- if log_lines:
- self.raw_log_text.setPlainText('\n'.join(log_lines))
- except Exception as e:
- self.raw_log_text.setPlainText(f"Error reading log: {e}")
-
- def on_show(self):
- """Called when plugin is shown."""
- self._update_events_table()
- self._update_counters()
-
- def shutdown(self):
- """Clean up."""
- if hasattr(self, 'update_timer'):
- self.update_timer.stop()
- super().shutdown()
diff --git a/plugins/loot_tracker/__init__.py b/plugins/loot_tracker/__init__.py
deleted file mode 100644
index b7024fd..0000000
--- a/plugins/loot_tracker/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Loot Tracker Plugin
-"""
-
-from .plugin import LootTrackerPlugin
-
-__all__ = ["LootTrackerPlugin"]
diff --git a/plugins/loot_tracker/plugin.py b/plugins/loot_tracker/plugin.py
deleted file mode 100644
index 695350b..0000000
--- a/plugins/loot_tracker/plugin.py
+++ /dev/null
@@ -1,224 +0,0 @@
-"""
-EU-Utility - Loot Tracker Plugin
-
-Track and analyze hunting loot with statistics and ROI.
-"""
-
-import re
-from datetime import datetime
-from collections import defaultdict
-from pathlib import Path
-import json
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem,
- QComboBox, QLineEdit, QTabWidget, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class LootTrackerPlugin(BasePlugin):
- """Track hunting loot and calculate ROI."""
-
- name = "Loot Tracker"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track hunting loot with stats and ROI analysis"
- hotkey = "ctrl+shift+l" # L for Loot
-
- def initialize(self):
- """Setup loot tracker."""
- self.data_file = Path("data/loot_tracker.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.sessions = []
- self.current_session = {
- 'start_time': None,
- 'kills': 0,
- 'loot_items': [],
- 'total_tt': 0.0,
- 'ammo_cost': 0.0,
- 'weapon_decay': 0.0,
- }
-
- self._load_data()
-
- def _load_data(self):
- """Load historical data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- self.sessions = json.load(f)
- except:
- self.sessions = []
-
- def _save_data(self):
- """Save data to file."""
- with open(self.data_file, 'w') as f:
- json.dump(self.sessions, f, indent=2)
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("Loot Tracker")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Stats summary
- stats_frame = QFrame()
- stats_frame.setStyleSheet("""
- QFrame {
- background-color: rgba(0, 0, 0, 50);
- border-radius: 10px;
- border: 1px solid rgba(255, 255, 255, 20);
- }
- """)
- stats_layout = QHBoxLayout(stats_frame)
-
- self.kills_label = QLabel("Kills: 0")
- self.kills_label.setStyleSheet("color: #4caf50; font-size: 14px; font-weight: bold;")
- stats_layout.addWidget(self.kills_label)
-
- self.tt_label = QLabel("TT: 0.00 PED")
- self.tt_label.setStyleSheet("color: #ffc107; font-size: 14px; font-weight: bold;")
- stats_layout.addWidget(self.tt_label)
-
- self.roi_label = QLabel("ROI: 0%")
- self.roi_label.setStyleSheet("color: #4a9eff; font-size: 14px; font-weight: bold;")
- stats_layout.addWidget(self.roi_label)
-
- layout.addWidget(stats_frame)
-
- # Session controls
- controls = QHBoxLayout()
-
- self.start_btn = QPushButton("โถ Start Session")
- self.start_btn.setStyleSheet("""
- QPushButton {
- background-color: #4caf50;
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #5cbf60;
- }
- """)
- self.start_btn.clicked.connect(self._start_session)
- controls.addWidget(self.start_btn)
-
- self.stop_btn = QPushButton("โน Stop Session")
- self.stop_btn.setStyleSheet("""
- QPushButton {
- background-color: #f44336;
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #f55a4e;
- }
- """)
- self.stop_btn.clicked.connect(self._stop_session)
- self.stop_btn.setEnabled(False)
- controls.addWidget(self.stop_btn)
-
- layout.addLayout(controls)
-
- # Loot table
- self.loot_table = QTableWidget()
- self.loot_table.setColumnCount(4)
- self.loot_table.setHorizontalHeaderLabels(["Item", "Qty", "TT Value", "Time"])
- self.loot_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 30, 30, 100);
- color: white;
- border: none;
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(74, 158, 255, 100);
- color: white;
- padding: 6px;
- }
- """)
- self.loot_table.horizontalHeader().setStretchLastSection(True)
- layout.addWidget(self.loot_table)
-
- layout.addStretch()
- return widget
-
- def _start_session(self):
- """Start new hunting session."""
- self.current_session = {
- 'start_time': datetime.now().isoformat(),
- 'kills': 0,
- 'loot_items': [],
- 'total_tt': 0.0,
- 'ammo_cost': 0.0,
- 'weapon_decay': 0.0,
- }
- self.start_btn.setEnabled(False)
- self.stop_btn.setEnabled(True)
-
- def _stop_session(self):
- """Stop current session and save."""
- self.current_session['end_time'] = datetime.now().isoformat()
- self.sessions.append(self.current_session)
- self._save_data()
-
- self.start_btn.setEnabled(True)
- self.stop_btn.setEnabled(False)
- self._update_stats()
-
- def _update_stats(self):
- """Update statistics display."""
- kills = self.current_session.get('kills', 0)
- tt = self.current_session.get('total_tt', 0.0)
-
- self.kills_label.setText(f"Kills: {kills}")
- self.tt_label.setText(f"TT: {tt:.2f} PED")
-
- def parse_chat_message(self, message):
- """Parse loot from chat."""
- # Look for loot patterns
- # Example: "You received Animal Hide (0.03 PED)"
- loot_pattern = r'You received (.+?) \(([\d.]+) PED\)'
- match = re.search(loot_pattern, message)
-
- if match:
- item_name = match.group(1)
- tt_value = float(match.group(2))
-
- self.current_session['loot_items'].append({
- 'item': item_name,
- 'tt': tt_value,
- 'time': datetime.now().isoformat()
- })
- self.current_session['total_tt'] += tt_value
- self._update_stats()
-
- # Check for new kill
- # Multiple items in quick succession = one kill
- # Longer gap = new kill
- self.current_session['kills'] += 1
-
- def on_hotkey(self):
- """Toggle session on hotkey."""
- if self.start_btn.isEnabled():
- self._start_session()
- else:
- self._stop_session()
diff --git a/plugins/mining_helper/__init__.py b/plugins/mining_helper/__init__.py
deleted file mode 100644
index ac46eea..0000000
--- a/plugins/mining_helper/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Mining Helper Plugin
-"""
-
-from .plugin import MiningHelperPlugin
-
-__all__ = ["MiningHelperPlugin"]
diff --git a/plugins/mining_helper/plugin.py b/plugins/mining_helper/plugin.py
deleted file mode 100644
index 13004e8..0000000
--- a/plugins/mining_helper/plugin.py
+++ /dev/null
@@ -1,273 +0,0 @@
-"""
-EU-Utility - Mining Helper Plugin
-
-Track mining finds, claims, and locations.
-"""
-
-import json
-from datetime import datetime
-from pathlib import Path
-from collections import defaultdict
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem,
- QComboBox, QTextEdit
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class MiningHelperPlugin(BasePlugin):
- """Track mining activities and claims."""
-
- name = "Mining Helper"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track mining finds, claims, and hotspot locations"
- hotkey = "ctrl+shift+n" # N for miNiNg
-
- # Resource types
- RESOURCES = [
- "Alicenies Liquid",
- "Ares Powder",
- "Blausariam",
- "Caldorite",
- "Cobalt",
- "Copper",
- "Dianthus",
- "Erdorium",
- "Frigulite",
- "Ganganite",
- "Himi",
- "Ignisium",
- "Iron",
- "Kaz Ingot",
- "Lysterium",
- "Maganite",
- "Niksarium",
- "Oil",
- "Platinum",
- "Redulite",
- "Rubio",
- "Sopur",
- "Titan",
- "Typonolic Steam",
- "Uranium",
- "Veldspar",
- "Xantium",
- "Zinc",
- ]
-
- def initialize(self):
- """Setup mining helper."""
- self.data_file = Path("data/mining_helper.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.claims = []
- self.current_run = {
- 'start_time': None,
- 'drops': 0,
- 'finds': [],
- 'total_tt': 0.0,
- }
-
- self._load_data()
-
- def _load_data(self):
- """Load historical data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.claims = data.get('claims', [])
- except:
- self.claims = []
-
- def _save_data(self):
- """Save data."""
- with open(self.data_file, 'w') as f:
- json.dump({'claims': self.claims}, f, indent=2)
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("โ๏ธ Mining Helper")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Stats
- stats = QHBoxLayout()
-
- self.drops_label = QLabel("Drops: 0")
- self.drops_label.setStyleSheet("color: #9c27b0; font-size: 14px; font-weight: bold;")
- stats.addWidget(self.drops_label)
-
- self.finds_label = QLabel("Finds: 0")
- self.finds_label.setStyleSheet("color: #4caf50; font-size: 14px; font-weight: bold;")
- stats.addWidget(self.finds_label)
-
- self.hit_rate_label = QLabel("Hit Rate: 0%")
- self.hit_rate_label.setStyleSheet("color: #ffc107; font-size: 14px; font-weight: bold;")
- stats.addWidget(self.hit_rate_label)
-
- layout.addLayout(stats)
-
- # Quick add claim
- add_frame = QWidget()
- add_frame.setStyleSheet("""
- QWidget {
- background-color: rgba(0, 0, 0, 50);
- border-radius: 8px;
- border: 1px solid rgba(255, 255, 255, 20);
- }
- """)
- add_layout = QHBoxLayout(add_frame)
- add_layout.setContentsMargins(10, 10, 10, 10)
-
- self.resource_combo = QComboBox()
- self.resource_combo.addItems(self.RESOURCES)
- self.resource_combo.setStyleSheet("""
- QComboBox {
- background-color: rgba(255, 255, 255, 15);
- color: white;
- border: none;
- padding: 5px;
- border-radius: 4px;
- }
- """)
- add_layout.addWidget(self.resource_combo)
-
- self.size_combo = QComboBox()
- self.size_combo.addItems(["Tiny", "Small", "Medium", "Large", "Huge", "Massive"])
- self.size_combo.setStyleSheet(self.resource_combo.styleSheet())
- add_layout.addWidget(self.size_combo)
-
- add_btn = QPushButton("+ Add Claim")
- add_btn.setStyleSheet("""
- QPushButton {
- background-color: #9c27b0;
- color: white;
- padding: 8px 15px;
- border: none;
- border-radius: 6px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #ab47bc;
- }
- """)
- add_btn.clicked.connect(self._add_claim)
- add_layout.addWidget(add_btn)
-
- layout.addWidget(add_frame)
-
- # Claims table
- self.claims_table = QTableWidget()
- self.claims_table.setColumnCount(4)
- self.claims_table.setHorizontalHeaderLabels(["Resource", "Size", "TT", "Time"])
- self.claims_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 30, 30, 100);
- color: white;
- border: none;
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(156, 39, 176, 100);
- color: white;
- padding: 6px;
- }
- """)
- self.claims_table.horizontalHeader().setStretchLastSection(True)
- layout.addWidget(self.claims_table)
-
- layout.addStretch()
- return widget
-
- def _add_claim(self):
- """Add a claim manually."""
- resource = self.resource_combo.currentText()
- size = self.size_combo.currentText()
-
- claim = {
- 'resource': resource,
- 'size': size,
- 'tt_value': self._estimate_tt(size),
- 'time': datetime.now().isoformat(),
- 'location': None, # Could get from game
- }
-
- self.claims.append(claim)
- self._save_data()
- self._update_table()
- self._update_stats()
-
- def _estimate_tt(self, size):
- """Estimate TT value based on claim size."""
- estimates = {
- 'Tiny': 0.05,
- 'Small': 0.25,
- 'Medium': 1.00,
- 'Large': 5.00,
- 'Huge': 25.00,
- 'Massive': 100.00,
- }
- return estimates.get(size, 0.05)
-
- def _update_table(self):
- """Update claims table."""
- recent = self.claims[-20:] # Show last 20
- self.claims_table.setRowCount(len(recent))
-
- for i, claim in enumerate(recent):
- self.claims_table.setItem(i, 0, QTableWidgetItem(claim['resource']))
- self.claims_table.setItem(i, 1, QTableWidgetItem(claim['size']))
- self.claims_table.setItem(i, 2, QTableWidgetItem(f"{claim['tt_value']:.2f}"))
- time_str = claim['time'][11:16] if claim['time'] else '-'
- self.claims_table.setItem(i, 3, QTableWidgetItem(time_str))
-
- def _update_stats(self):
- """Update statistics."""
- drops = len(self.claims) + 10 # Estimate
- finds = len(self.claims)
- hit_rate = (finds / drops * 100) if drops > 0 else 0
-
- self.drops_label.setText(f"Drops: ~{drops}")
- self.finds_label.setText(f"Finds: {finds}")
- self.hit_rate_label.setText(f"Hit Rate: {hit_rate:.1f}%")
-
- def parse_chat_message(self, message):
- """Parse mining claims from chat."""
- # Look for claim patterns
- # Example: "You found a Tiny Lysterium claim"
- for resource in self.RESOURCES:
- if resource in message and "claim" in message.lower():
- # Extract size
- sizes = ["Tiny", "Small", "Medium", "Large", "Huge", "Massive"]
- size = "Unknown"
- for s in sizes:
- if s in message:
- size = s
- break
-
- claim = {
- 'resource': resource,
- 'size': size,
- 'tt_value': self._estimate_tt(size),
- 'time': datetime.now().isoformat(),
- 'location': None,
- }
-
- self.claims.append(claim)
- self._save_data()
- self._update_table()
- self._update_stats()
- break
diff --git a/plugins/mission_tracker/__init__.py b/plugins/mission_tracker/__init__.py
deleted file mode 100644
index de1d978..0000000
--- a/plugins/mission_tracker/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Mission Tracker Plugin
-"""
-
-from .plugin import MissionTrackerPlugin
-
-__all__ = ["MissionTrackerPlugin"]
diff --git a/plugins/mission_tracker/plugin.py b/plugins/mission_tracker/plugin.py
deleted file mode 100644
index bf2c878..0000000
--- a/plugins/mission_tracker/plugin.py
+++ /dev/null
@@ -1,315 +0,0 @@
-"""
-EU-Utility - Mission Tracker Plugin
-
-Track active missions and progress.
-"""
-
-import json
-from datetime import datetime
-from pathlib import Path
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QProgressBar, QFrame, QScrollArea
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-from core.icon_manager import get_icon_manager
-
-
-class MissionTrackerPlugin(BasePlugin):
- """Track active missions and daily challenges."""
-
- name = "Mission Tracker"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track missions, challenges, and objectives"
- hotkey = "ctrl+shift+m"
-
- def initialize(self):
- """Setup mission tracker."""
- self.data_file = Path("data/missions.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.missions = []
- self.daily_challenges = []
-
- self._load_data()
-
- def _load_data(self):
- """Load mission data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.missions = data.get('missions', [])
- self.daily_challenges = data.get('daily', [])
- except:
- pass
-
- def _save_data(self):
- """Save mission data."""
- with open(self.data_file, 'w') as f:
- json.dump({
- 'missions': self.missions,
- 'daily': self.daily_challenges
- }, f, indent=2)
-
- def get_ui(self):
- """Create mission tracker UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("๐ Mission Tracker")
- title.setStyleSheet("""
- color: white;
- font-size: 16px;
- font-weight: bold;
- """)
- layout.addWidget(title)
-
- # Active missions section
- active_label = QLabel("Active Missions")
- active_label.setStyleSheet("color: rgba(255,255,255,200); font-size: 12px;")
- layout.addWidget(active_label)
-
- # Mission cards
- self.missions_container = QWidget()
- self.missions_layout = QVBoxLayout(self.missions_container)
- self.missions_layout.setSpacing(10)
- self.missions_layout.setContentsMargins(0, 0, 0, 0)
-
- self._refresh_missions()
- layout.addWidget(self.missions_container)
-
- # Daily challenges
- daily_label = QLabel("Daily Challenges")
- daily_label.setStyleSheet("color: rgba(255,255,255,200); font-size: 12px; margin-top: 10px;")
- layout.addWidget(daily_label)
-
- self.daily_container = QWidget()
- self.daily_layout = QVBoxLayout(self.daily_container)
- self.daily_layout.setSpacing(8)
- self.daily_layout.setContentsMargins(0, 0, 0, 0)
-
- self._refresh_daily()
- layout.addWidget(self.daily_container)
-
- # Add mission button
- add_btn = QPushButton("+ Add Mission")
- add_btn.setStyleSheet("""
- QPushButton {
- background-color: rgba(255, 140, 66, 200);
- color: white;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: rgba(255, 160, 80, 230);
- }
- """)
- add_btn.clicked.connect(self._add_mission)
- layout.addWidget(add_btn)
-
- layout.addStretch()
- return widget
-
- def _create_mission_card(self, mission):
- """Create a mission card widget."""
- card = QFrame()
- card.setStyleSheet("""
- QFrame {
- background-color: rgba(30, 35, 45, 200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- """)
- layout = QVBoxLayout(card)
- layout.setContentsMargins(12, 10, 12, 10)
- layout.setSpacing(8)
-
- # Header
- header = QHBoxLayout()
-
- name = QLabel(mission.get('name', 'Unknown Mission'))
- name.setStyleSheet("color: #ff8c42; font-weight: bold; font-size: 12px;")
- header.addWidget(name)
-
- header.addStretch()
-
- # Complete button
- complete_btn = QPushButton("โ")
- complete_btn.setFixedSize(24, 24)
- complete_btn.setStyleSheet("""
- QPushButton {
- background-color: rgba(76, 175, 80, 150);
- color: white;
- border: none;
- border-radius: 3px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: rgba(76, 175, 80, 200);
- }
- """)
- complete_btn.clicked.connect(lambda: self._complete_mission(mission))
- header.addWidget(complete_btn)
-
- layout.addLayout(header)
-
- # Progress
- current = mission.get('current', 0)
- total = mission.get('total', 1)
- pct = min(100, int(current / total * 100))
-
- progress_layout = QHBoxLayout()
-
- progress = QProgressBar()
- progress.setValue(pct)
- progress.setTextVisible(False)
- progress.setFixedHeight(6)
- progress.setStyleSheet("""
- QProgressBar {
- background-color: rgba(60, 70, 90, 150);
- border: none;
- border-radius: 3px;
- }
- QProgressBar::chunk {
- background-color: #ff8c42;
- border-radius: 3px;
- }
- """)
- progress_layout.addWidget(progress, 1)
-
- progress_text = QLabel(f"{current}/{total}")
- progress_text.setStyleSheet("color: rgba(255,255,255,150); font-size: 10px;")
- progress_layout.addWidget(progress_text)
-
- layout.addLayout(progress_layout)
-
- return card
-
- def _create_challenge_card(self, challenge):
- """Create a daily challenge card."""
- card = QFrame()
- card.setStyleSheet("""
- QFrame {
- background-color: rgba(25, 30, 40, 180);
- border: 1px solid rgba(80, 90, 110, 60);
- border-radius: 4px;
- }
- """)
- layout = QHBoxLayout(card)
- layout.setContentsMargins(10, 8, 10, 8)
- layout.setSpacing(10)
-
- # Icon based on type
- icon_mgr = get_icon_manager()
- icon_label = QLabel()
- icon_pixmap = icon_mgr.get_pixmap('sword', size=16)
- icon_label.setPixmap(icon_pixmap)
- icon_label.setFixedSize(16, 16)
- layout.addWidget(icon_label)
-
- # Name
- name = QLabel(challenge.get('name', 'Challenge'))
- name.setStyleSheet("color: white; font-size: 11px;")
- layout.addWidget(name)
-
- layout.addStretch()
-
- # Progress
- current = challenge.get('current', 0)
- total = challenge.get('total', 1)
- pct = min(100, int(current / total * 100))
-
- progress = QProgressBar()
- progress.setValue(pct)
- progress.setTextVisible(False)
- progress.setFixedSize(60, 4)
- progress.setStyleSheet("""
- QProgressBar {
- background-color: rgba(60, 70, 90, 150);
- border: none;
- border-radius: 2px;
- }
- QProgressBar::chunk {
- background-color: #ffc107;
- border-radius: 2px;
- }
- """)
- layout.addWidget(progress)
-
- text = QLabel(f"{current}/{total}")
- text.setStyleSheet("color: rgba(255,255,255,120); font-size: 10px;")
- layout.addWidget(text)
-
- return card
-
- def _refresh_missions(self):
- """Refresh mission display."""
- # Clear existing
- while self.missions_layout.count():
- item = self.missions_layout.takeAt(0)
- if item.widget():
- item.widget().deleteLater()
-
- # Add missions
- for mission in self.missions:
- card = self._create_mission_card(mission)
- self.missions_layout.addWidget(card)
-
- def _refresh_daily(self):
- """Refresh daily challenges."""
- while self.daily_layout.count():
- item = self.daily_layout.takeAt(0)
- if item.widget():
- item.widget().deleteLater()
-
- for challenge in self.daily_challenges:
- card = self._create_challenge_card(challenge)
- self.daily_layout.addWidget(card)
-
- def _add_mission(self):
- """Add a new mission."""
- # Add sample mission
- self.missions.append({
- 'name': 'Oratan Payback Mission III',
- 'current': 0,
- 'total': 100,
- 'type': 'kill',
- 'target': 'Oratan Prospector Bandits',
- 'added': datetime.now().isoformat()
- })
- self._save_data()
- self._refresh_missions()
-
- def _complete_mission(self, mission):
- """Mark mission as complete."""
- if mission in self.missions:
- self.missions.remove(mission)
- self._save_data()
- self._refresh_missions()
-
- def parse_chat_message(self, message):
- """Parse mission progress from chat."""
- # Look for mission progress
- # Example: "Mission updated: 12/100 Oratan Prospector Bandits"
- for mission in self.missions:
- target = mission.get('target', '')
- if target and target in message:
- # Extract progress
- import re
- match = re.search(r'(\d+)/(\d+)', message)
- if match:
- mission['current'] = int(match.group(1))
- mission['total'] = int(match.group(2))
- self._save_data()
- self._refresh_missions()
diff --git a/plugins/nexus_search/__init__.py b/plugins/nexus_search/__init__.py
deleted file mode 100644
index fb6a458..0000000
--- a/plugins/nexus_search/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Nexus Search Plugin for EU-Utility
-"""
-
-from .plugin import NexusSearchPlugin
-
-__all__ = ["NexusSearchPlugin"]
diff --git a/plugins/nexus_search/plugin.py b/plugins/nexus_search/plugin.py
deleted file mode 100644
index 6dea9f3..0000000
--- a/plugins/nexus_search/plugin.py
+++ /dev/null
@@ -1,442 +0,0 @@
-"""
-EU-Utility - Nexus Search Plugin
-
-Built-in plugin for searching EntropiaNexus via API.
-Uses official Nexus API endpoints.
-"""
-
-import json
-import webbrowser
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout,
- QLineEdit, QPushButton, QLabel, QComboBox,
- QListWidget, QListWidgetItem, QTabWidget,
- QTableWidget, QTableWidgetItem, QHeaderView
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal
-
-from plugins.base_plugin import BasePlugin
-
-
-class NexusAPIClient:
- """Client for EntropiaNexus API."""
-
- BASE_URL = "https://www.entropianexus.com"
-
- @classmethod
- def fetch_exchange_items(cls, search_query=None, http_get_func=None):
- """Fetch exchange items from Nexus API."""
- try:
- url = f"{cls.BASE_URL}/api/market/exchange"
-
- if http_get_func:
- response = http_get_func(
- url,
- cache_ttl=60, # 1 minute cache for market data
- headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'}
- )
- data = response.get('json') if response else None
- else:
- # Fallback for standalone usage
- import urllib.request
- req = urllib.request.Request(
- url,
- headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'}
- )
- with urllib.request.urlopen(req, timeout=10) as resp:
- data = json.loads(resp.read().decode('utf-8'))
-
- # Filter by search query if provided
- if search_query and data:
- search_lower = search_query.lower()
- filtered = []
- for category in data:
- if 'items' in category:
- for item in category['items']:
- if search_lower in item.get('name', '').lower():
- filtered.append(item)
- return filtered
-
- return data
-
- except Exception as e:
- print(f"API Error: {e}")
- return None
-
- @classmethod
- def fetch_item_prices(cls, item_ids, http_get_func=None):
- """Fetch latest prices for items."""
- try:
- if not item_ids:
- return {}
-
- ids_str = ','.join(str(id) for id in item_ids[:100]) # Max 100
- url = f"{cls.BASE_URL}/api/market/prices/latest?items={ids_str}"
-
- if http_get_func:
- response = http_get_func(
- url,
- cache_ttl=60, # 1 minute cache
- headers={'Accept': 'application/json'}
- )
- return response.get('json') if response else {}
- else:
- # Fallback for standalone usage
- import urllib.request
- req = urllib.request.Request(
- url,
- headers={'Accept': 'application/json'}
- )
- with urllib.request.urlopen(req, timeout=10) as resp:
- return json.loads(resp.read().decode('utf-8'))
-
- except Exception as e:
- print(f"Price API Error: {e}")
- return {}
-
- @classmethod
- def search_users(cls, query, http_get_func=None):
- """Search for verified users."""
- try:
- params = {'q': query, 'limit': 10}
- query_string = '&'.join(f"{k}={v}" for k, v in params.items())
- url = f"{cls.BASE_URL}/api/users/search?{query_string}"
-
- if http_get_func:
- response = http_get_func(
- url,
- cache_ttl=300, # 5 minute cache for user search
- headers={'Accept': 'application/json'}
- )
- return response.get('json') if response else None
- else:
- # Fallback for standalone usage
- import urllib.request
- req = urllib.request.Request(
- url,
- headers={'Accept': 'application/json'}
- )
- with urllib.request.urlopen(req, timeout=10) as resp:
- return json.loads(resp.read().decode('utf-8'))
-
- except Exception as e:
- print(f"User Search Error: {e}")
- return None
-
-
-class NexusSearchThread(QThread):
- """Background thread for API searches."""
- results_ready = pyqtSignal(list, str) # results, search_type
- error_occurred = pyqtSignal(str)
-
- def __init__(self, query, search_type, http_get_func=None):
- super().__init__()
- self.query = query
- self.search_type = search_type
- self.http_get_func = http_get_func
-
- def run(self):
- """Perform API search."""
- try:
- results = []
-
- if self.search_type == "Items":
- # Search exchange items
- data = NexusAPIClient.fetch_exchange_items(self.query, http_get_func=self.http_get_func)
- if data:
- if isinstance(data, list) and len(data) > 0 and 'name' in data[0]:
- # Already filtered items
- results = data[:20] # Limit to 20
- else:
- # Full category structure
- for category in data:
- if 'items' in category:
- for item in category['items']:
- if self.query.lower() in item.get('name', '').lower():
- results.append(item)
- if len(results) >= 20:
- break
- if len(results) >= 20:
- break
-
- elif self.search_type == "Users":
- # Search users
- data = NexusAPIClient.search_users(self.query, http_get_func=self.http_get_func)
- if data:
- results = data[:10]
-
- self.results_ready.emit(results, self.search_type)
-
- except Exception as e:
- self.error_occurred.emit(str(e))
-
-
-class NexusSearchPlugin(BasePlugin):
- """Search EntropiaNexus via API."""
-
- name = "EntropiaNexus"
- version = "1.1.0"
- author = "ImpulsiveFPS"
- description = "Search items, users, and market data via Nexus API"
- hotkey = "ctrl+shift+n"
-
- def initialize(self):
- """Setup the plugin."""
- self.base_url = "https://www.entropianexus.com"
- self.search_thread = None
- self.current_results = []
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
-
- # Title
- title = QLabel("EntropiaNexus")
- title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
- layout.addWidget(title)
-
- # Search type
- type_layout = QHBoxLayout()
- type_layout.addWidget(QLabel("Search:"))
-
- self.search_type = QComboBox()
- self.search_type.addItems([
- "Items",
- "Users",
- ])
- self.search_type.setStyleSheet("""
- QComboBox {
- background-color: #444;
- color: white;
- padding: 5px;
- border-radius: 4px;
- min-width: 100px;
- }
- """)
- type_layout.addWidget(self.search_type)
-
- # Search input
- self.search_input = QLineEdit()
- self.search_input.setPlaceholderText("Enter search term...")
- self.search_input.setStyleSheet("""
- QLineEdit {
- background-color: #333;
- color: white;
- padding: 8px;
- border: 2px solid #555;
- border-radius: 4px;
- font-size: 13px;
- }
- QLineEdit:focus {
- border-color: #4a9eff;
- }
- """)
- self.search_input.returnPressed.connect(self._do_search)
- type_layout.addWidget(self.search_input, 1)
-
- # Search button
- search_btn = QPushButton("๐")
- search_btn.setFixedWidth(40)
- search_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a9eff;
- color: white;
- border: none;
- border-radius: 4px;
- font-size: 14px;
- }
- QPushButton:hover {
- background-color: #5aafff;
- }
- """)
- search_btn.clicked.connect(self._do_search)
- type_layout.addWidget(search_btn)
-
- layout.addLayout(type_layout)
-
- # Status
- self.status_label = QLabel("Ready")
- self.status_label.setStyleSheet("color: #666; font-size: 11px;")
- layout.addWidget(self.status_label)
-
- # Results table
- self.results_table = QTableWidget()
- self.results_table.setColumnCount(3)
- self.results_table.setHorizontalHeaderLabels(["Name", "Type", "Price"])
- self.results_table.horizontalHeader().setStretchLastSection(True)
- self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
- self.results_table.setStyleSheet("""
- QTableWidget {
- background-color: #2a2a2a;
- color: white;
- border: 1px solid #444;
- border-radius: 4px;
- gridline-color: #444;
- }
- QTableWidget::item {
- padding: 8px;
- border-bottom: 1px solid #333;
- }
- QTableWidget::item:selected {
- background-color: #4a9eff;
- }
- QHeaderView::section {
- background-color: #333;
- color: #aaa;
- padding: 8px;
- border: none;
- font-weight: bold;
- }
- """)
- self.results_table.cellClicked.connect(self._on_item_clicked)
- self.results_table.setMaximumHeight(300)
- layout.addWidget(self.results_table)
-
- # Action buttons
- btn_layout = QHBoxLayout()
-
- open_btn = QPushButton("Open on Nexus")
- open_btn.setStyleSheet("""
- QPushButton {
- background-color: #333;
- color: white;
- padding: 8px 16px;
- border: none;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #444;
- }
- """)
- open_btn.clicked.connect(self._open_selected)
- btn_layout.addWidget(open_btn)
-
- btn_layout.addStretch()
-
- # Quick links
- links_label = QLabel("Quick:")
- links_label.setStyleSheet("color: #666;")
- btn_layout.addWidget(links_label)
-
- for name, url in [
- ("Market", "/market/exchange"),
- ("Items", "/items"),
- ("Mobs", "/mobs"),
- ]:
- btn = QPushButton(name)
- btn.setStyleSheet("""
- QPushButton {
- background-color: transparent;
- color: #4a9eff;
- border: none;
- padding: 5px;
- }
- QPushButton:hover {
- color: #5aafff;
- text-decoration: underline;
- }
- """)
- btn.clicked.connect(lambda checked, u=self.base_url + url: webbrowser.open(u))
- btn_layout.addWidget(btn)
-
- layout.addLayout(btn_layout)
- layout.addStretch()
-
- return widget
-
- def _do_search(self):
- """Perform API search."""
- query = self.search_input.text().strip()
- if not query or len(query) < 2:
- self.status_label.setText("Enter at least 2 characters")
- return
-
- search_type = self.search_type.currentText()
-
- # Clear previous results
- self.results_table.setRowCount(0)
- self.current_results = []
- self.status_label.setText("Searching...")
-
- # Start search thread with http_get function
- self.search_thread = NexusSearchThread(query, search_type, http_get_func=self.http_get)
- self.search_thread.results_ready.connect(self._on_results)
- self.search_thread.error_occurred.connect(self._on_error)
- self.search_thread.start()
-
- def _on_results(self, results, search_type):
- """Handle search results."""
- self.current_results = results
-
- if not results:
- self.status_label.setText("No results found")
- return
-
- # Populate table
- self.results_table.setRowCount(len(results))
-
- for row, item in enumerate(results):
- if search_type == "Items":
- name = item.get('name', 'Unknown')
- item_type = item.get('type', 'Item')
-
- # Price info
- buy_price = item.get('buy', [])
- sell_price = item.get('sell', [])
-
- if buy_price:
- price_text = f"Buy: {buy_price[0].get('price', 'N/A')}"
- elif sell_price:
- price_text = f"Sell: {sell_price[0].get('price', 'N/A')}"
- else:
- price_text = "No orders"
-
- self.results_table.setItem(row, 0, QTableWidgetItem(name))
- self.results_table.setItem(row, 1, QTableWidgetItem(item_type))
- self.results_table.setItem(row, 2, QTableWidgetItem(price_text))
-
- elif search_type == "Users":
- name = item.get('name', 'Unknown')
- eu_name = item.get('euName', '')
-
- self.results_table.setItem(row, 0, QTableWidgetItem(name))
- self.results_table.setItem(row, 1, QTableWidgetItem("User"))
- self.results_table.setItem(row, 2, QTableWidgetItem(eu_name or ''))
-
- self.status_label.setText(f"Found {len(results)} results")
-
- def _on_error(self, error):
- """Handle search error."""
- self.status_label.setText(f"Error: {error}")
-
- def _on_item_clicked(self, row, column):
- """Handle item click."""
- if row < len(self.current_results):
- item = self.current_results[row]
- search_type = self.search_type.currentText()
-
- if search_type == "Items":
- item_id = item.get('id')
- if item_id:
- url = f"{self.base_url}/items/{item_id}"
- webbrowser.open(url)
-
- elif search_type == "Users":
- user_id = item.get('id')
- if user_id:
- url = f"{self.base_url}/users/{user_id}"
- webbrowser.open(url)
-
- def _open_selected(self):
- """Open selected item in browser."""
- selected = self.results_table.selectedItems()
- if selected:
- row = selected[0].row()
- self._on_item_clicked(row, 0)
-
- def on_hotkey(self):
- """Focus search when hotkey pressed."""
- if hasattr(self, 'search_input'):
- self.search_input.setFocus()
- self.search_input.selectAll()
diff --git a/plugins/price_alerts/__init__.py b/plugins/price_alerts/__init__.py
deleted file mode 100644
index e70c463..0000000
--- a/plugins/price_alerts/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-Price Alerts Plugin for EU-Utility
-
-Monitor Entropia Nexus prices and get alerts when items
-reach target prices.
-"""
-
-from .plugin import PriceAlertPlugin
-
-__all__ = ['PriceAlertPlugin']
diff --git a/plugins/price_alerts/plugin.py b/plugins/price_alerts/plugin.py
deleted file mode 100644
index d2d2648..0000000
--- a/plugins/price_alerts/plugin.py
+++ /dev/null
@@ -1,693 +0,0 @@
-"""
-EU-Utility - Price Alert Plugin
-
-Monitor Entropia Nexus API prices and get alerts when items
-reach target prices (good for buying low/selling high).
-"""
-
-import json
-from datetime import datetime, timedelta
-from typing import Dict, List, Optional, Any
-from dataclasses import dataclass, field, asdict
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
- QTableWidget, QTableWidgetItem, QHeaderView, QLineEdit,
- QDoubleSpinBox, QComboBox, QMessageBox, QGroupBox,
- QCheckBox, QSpinBox, QSplitter, QTextEdit
-)
-from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject
-from PyQt6.QtGui import QColor
-
-from plugins.base_plugin import BasePlugin
-from core.nexus_api import get_nexus_api, MarketData
-
-
-@dataclass
-class PriceAlert:
- """A price alert configuration."""
- id: str
- item_id: str
- item_name: str
- alert_type: str # 'below' or 'above'
- target_price: float
- current_price: Optional[float] = None
- last_checked: Optional[datetime] = None
- triggered: bool = False
- trigger_count: int = 0
- enabled: bool = True
- created_at: datetime = field(default_factory=datetime.now)
-
- def to_dict(self) -> Dict[str, Any]:
- """Convert to dictionary for serialization."""
- return {
- 'id': self.id,
- 'item_id': self.item_id,
- 'item_name': self.item_name,
- 'alert_type': self.alert_type,
- 'target_price': self.target_price,
- 'current_price': self.current_price,
- 'last_checked': self.last_checked.isoformat() if self.last_checked else None,
- 'triggered': self.triggered,
- 'trigger_count': self.trigger_count,
- 'enabled': self.enabled,
- 'created_at': self.created_at.isoformat()
- }
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'PriceAlert':
- """Create from dictionary."""
- alert = cls(
- id=data['id'],
- item_id=data['item_id'],
- item_name=data['item_name'],
- alert_type=data['alert_type'],
- target_price=data['target_price'],
- current_price=data.get('current_price'),
- triggered=data.get('triggered', False),
- trigger_count=data.get('trigger_count', 0),
- enabled=data.get('enabled', True)
- )
- if data.get('last_checked'):
- alert.last_checked = datetime.fromisoformat(data['last_checked'])
- if data.get('created_at'):
- alert.created_at = datetime.fromisoformat(data['created_at'])
- return alert
-
- def check_condition(self, current_price: float) -> bool:
- """Check if the alert condition is met."""
- self.current_price = current_price
- self.last_checked = datetime.now()
-
- if self.alert_type == 'below':
- return current_price <= self.target_price
- elif self.alert_type == 'above':
- return current_price >= self.target_price
- return False
-
-
-class PriceAlertSignals(QObject):
- """Signals for thread-safe UI updates."""
- alert_triggered = pyqtSignal(object) # PriceAlert
- prices_updated = pyqtSignal()
-
-
-class PriceAlertPlugin(BasePlugin):
- """
- Plugin for monitoring Entropia Nexus prices.
-
- Features:
- - Monitor any item's market price
- - Set alerts for price thresholds (buy low, sell high)
- - Auto-refresh at configurable intervals
- - Notification when alerts trigger
- - Price history tracking
- """
-
- name = "Price Alerts"
- version = "1.0.0"
- author = "EU-Utility"
- description = "Monitor Nexus prices and get alerts"
- icon = "๐"
- hotkey = "ctrl+shift+p"
-
- def __init__(self, overlay_window, config):
- super().__init__(overlay_window, config)
-
- # Alert storage
- self.alerts: Dict[str, PriceAlert] = {}
- self.price_history: Dict[str, List[Dict]] = {} # item_id -> price history
-
- # Services
- self.nexus = get_nexus_api()
- self.signals = PriceAlertSignals()
-
- # Settings
- self.check_interval = self.get_config('check_interval', 300) # 5 minutes
- self.notifications_enabled = self.get_config('notifications_enabled', True)
- self.sound_enabled = self.get_config('sound_enabled', True)
- self.history_days = self.get_config('history_days', 7)
-
- # UI references
- self._ui = None
- self.alerts_table = None
- self.search_results_table = None
- self.search_input = None
- self.status_label = None
-
- # Timer
- self._check_timer = None
-
- # Connect signals
- self.signals.alert_triggered.connect(self._on_alert_triggered)
- self.signals.prices_updated.connect(self._update_alerts_table)
-
- # Load saved alerts
- self._load_alerts()
-
- def initialize(self) -> None:
- """Initialize plugin."""
- self.log_info("Initializing Price Alerts")
-
- # Start price check timer
- self._check_timer = QTimer()
- self._check_timer.timeout.connect(self._check_all_prices)
- self._check_timer.start(self.check_interval * 1000)
-
- # Initial price check
- self._check_all_prices()
-
- self.log_info(f"Price Alerts initialized with {len(self.alerts)} alerts")
-
- def get_ui(self) -> QWidget:
- """Return the plugin's UI widget."""
- if self._ui is None:
- self._ui = self._create_ui()
- return self._ui
-
- def _create_ui(self) -> QWidget:
- """Create the plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(12)
- layout.setContentsMargins(16, 16, 16, 16)
-
- # Header
- header = QLabel("๐ Price Alerts")
- header.setStyleSheet("""
- font-size: 18px;
- font-weight: bold;
- color: white;
- padding-bottom: 8px;
- """)
- layout.addWidget(header)
-
- # Status
- self.status_label = QLabel(f"Active Alerts: {len(self.alerts)} | Next check: --")
- self.status_label.setStyleSheet("color: rgba(255, 255, 255, 150);")
- layout.addWidget(self.status_label)
-
- # Create tabs
- tabs = QSplitter(Qt.Orientation.Vertical)
-
- # === Alerts Tab ===
- alerts_widget = QWidget()
- alerts_layout = QVBoxLayout(alerts_widget)
- alerts_layout.setContentsMargins(0, 0, 0, 0)
-
- alerts_header = QLabel("Your Alerts")
- alerts_header.setStyleSheet("font-weight: bold; color: white;")
- alerts_layout.addWidget(alerts_header)
-
- self.alerts_table = QTableWidget()
- self.alerts_table.setColumnCount(6)
- self.alerts_table.setHorizontalHeaderLabels([
- "Item", "Condition", "Target", "Current", "Status", "Actions"
- ])
- self.alerts_table.horizontalHeader().setStretchLastSection(True)
- self.alerts_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
- self.alerts_table.setStyleSheet(self._table_style())
- alerts_layout.addWidget(self.alerts_table)
-
- # Alert buttons
- alerts_btn_layout = QHBoxLayout()
-
- refresh_btn = QPushButton("๐ Check Now")
- refresh_btn.setStyleSheet(self._button_style("#2196f3"))
- refresh_btn.clicked.connect(self._check_all_prices)
- alerts_btn_layout.addWidget(refresh_btn)
-
- clear_triggered_btn = QPushButton("๐งน Clear Triggered")
- clear_triggered_btn.setStyleSheet(self._button_style())
- clear_triggered_btn.clicked.connect(self._clear_triggered)
- alerts_btn_layout.addWidget(clear_triggered_btn)
-
- alerts_btn_layout.addStretch()
- alerts_layout.addLayout(alerts_btn_layout)
-
- tabs.addWidget(alerts_widget)
-
- # === Search Tab ===
- search_widget = QWidget()
- search_layout = QVBoxLayout(search_widget)
- search_layout.setContentsMargins(0, 0, 0, 0)
-
- search_header = QLabel("Search Items to Monitor")
- search_header.setStyleSheet("font-weight: bold; color: white;")
- search_layout.addWidget(search_header)
-
- # Search input
- search_input_layout = QHBoxLayout()
- self.search_input = QLineEdit()
- self.search_input.setPlaceholderText("Search for items...")
- self.search_input.setStyleSheet(self._input_style())
- self.search_input.returnPressed.connect(self._search_items)
- search_input_layout.addWidget(self.search_input)
-
- search_btn = QPushButton("๐ Search")
- search_btn.setStyleSheet(self._button_style("#4caf50"))
- search_btn.clicked.connect(self._search_items)
- search_input_layout.addWidget(search_btn)
-
- search_layout.addLayout(search_input_layout)
-
- # Search results
- self.search_results_table = QTableWidget()
- self.search_results_table.setColumnCount(4)
- self.search_results_table.setHorizontalHeaderLabels(["Name", "Type", "Current Markup", "Action"])
- self.search_results_table.horizontalHeader().setStretchLastSection(True)
- self.search_results_table.setStyleSheet(self._table_style())
- search_layout.addWidget(self.search_results_table)
-
- tabs.addWidget(search_widget)
-
- layout.addWidget(tabs)
-
- # Settings
- settings_group = QGroupBox("Settings")
- settings_layout = QVBoxLayout(settings_group)
-
- # Check interval
- interval_layout = QHBoxLayout()
- interval_label = QLabel("Check Interval:")
- interval_label.setStyleSheet("color: white;")
- interval_layout.addWidget(interval_label)
-
- self.interval_spin = QSpinBox()
- self.interval_spin.setRange(1, 60)
- self.interval_spin.setValue(self.check_interval // 60)
- self.interval_spin.setSuffix(" min")
- self.interval_spin.setStyleSheet(self._spinbox_style())
- interval_layout.addWidget(self.interval_spin)
-
- interval_layout.addStretch()
- settings_layout.addLayout(interval_layout)
-
- # Notification settings
- notif_layout = QHBoxLayout()
-
- self.notif_checkbox = QCheckBox("Show Notifications")
- self.notif_checkbox.setChecked(self.notifications_enabled)
- self.notif_checkbox.setStyleSheet("color: white;")
- notif_layout.addWidget(self.notif_checkbox)
-
- self.sound_checkbox = QCheckBox("Play Sound")
- self.sound_checkbox.setChecked(self.sound_enabled)
- self.sound_checkbox.setStyleSheet("color: white;")
- notif_layout.addWidget(self.sound_checkbox)
-
- notif_layout.addStretch()
- settings_layout.addLayout(notif_layout)
-
- layout.addWidget(settings_group)
-
- # Apply settings button
- apply_btn = QPushButton("๐พ Apply Settings")
- apply_btn.setStyleSheet(self._button_style("#4caf50"))
- apply_btn.clicked.connect(self._apply_settings)
- layout.addWidget(apply_btn)
-
- # Initial table population
- self._update_alerts_table()
-
- return widget
-
- def _table_style(self) -> str:
- """Generate table stylesheet."""
- return """
- QTableWidget {
- background-color: rgba(30, 35, 45, 150);
- border: 1px solid rgba(100, 150, 200, 50);
- border-radius: 8px;
- color: white;
- gridline-color: rgba(100, 150, 200, 30);
- }
- QHeaderView::section {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- padding: 8px;
- border: none;
- }
- QTableWidget::item {
- padding: 6px;
- }
- QTableWidget::item:selected {
- background-color: rgba(100, 150, 200, 100);
- }
- """
-
- def _button_style(self, color: str = "#607d8b") -> str:
- """Generate button stylesheet."""
- return f"""
- QPushButton {{
- background-color: {color};
- color: white;
- border: none;
- border-radius: 8px;
- padding: 8px 14px;
- font-weight: bold;
- }}
- QPushButton:hover {{
- background-color: {color}dd;
- }}
- QPushButton:pressed {{
- background-color: {color}aa;
- }}
- """
-
- def _input_style(self) -> str:
- """Generate input stylesheet."""
- return """
- QLineEdit {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- border: 1px solid rgba(100, 150, 200, 100);
- border-radius: 8px;
- padding: 8px 12px;
- }
- QLineEdit:focus {
- border: 1px solid rgba(100, 180, 255, 150);
- }
- """
-
- def _spinbox_style(self) -> str:
- """Generate spinbox stylesheet."""
- return """
- QSpinBox {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- border: 1px solid rgba(100, 150, 200, 100);
- border-radius: 4px;
- padding: 4px;
- }
- """
-
- def _search_items(self) -> None:
- """Search for items via Nexus API."""
- query = self.search_input.text().strip()
- if not query:
- return
-
- self.status_label.setText(f"Searching for '{query}'...")
-
- # Run search in background
- self.run_in_background(
- self.nexus.search_items,
- query,
- limit=20,
- priority='normal',
- on_complete=self._on_search_complete,
- on_error=self._on_search_error
- )
-
- def _on_search_complete(self, results: List[Any]) -> None:
- """Handle search completion."""
- self.search_results_table.setRowCount(len(results))
-
- for row, item in enumerate(results):
- # Name
- name_item = QTableWidgetItem(item.name)
- name_item.setForeground(QColor("white"))
- self.search_results_table.setItem(row, 0, name_item)
-
- # Type
- type_item = QTableWidgetItem(item.type)
- type_item.setForeground(QColor("#2196f3"))
- self.search_results_table.setItem(row, 1, type_item)
-
- # Current markup (will be fetched)
- markup_item = QTableWidgetItem("Click to check")
- markup_item.setForeground(QColor("rgba(255, 255, 255, 100)"))
- self.search_results_table.setItem(row, 2, markup_item)
-
- # Action button
- add_btn = QPushButton("+ Alert")
- add_btn.setStyleSheet(self._button_style("#4caf50"))
- add_btn.clicked.connect(lambda checked, i=item: self._show_add_alert_dialog(i))
- self.search_results_table.setCellWidget(row, 3, add_btn)
-
- self.status_label.setText(f"Found {len(results)} items")
-
- def _on_search_error(self, error: Exception) -> None:
- """Handle search error."""
- self.status_label.setText(f"Search failed: {str(error)}")
- self.notify_error("Search Failed", str(error))
-
- def _show_add_alert_dialog(self, item: Any) -> None:
- """Show dialog to add a new alert."""
- from PyQt6.QtWidgets import QDialog, QFormLayout, QDialogButtonBox
-
- dialog = QDialog(self._ui)
- dialog.setWindowTitle(f"Add Alert: {item.name}")
- dialog.setStyleSheet("""
- QDialog {
- background-color: #2a3040;
- }
- QLabel {
- color: white;
- }
- """)
-
- layout = QFormLayout(dialog)
-
- # Alert type
- type_combo = QComboBox()
- type_combo.addItems(["Price Below", "Price Above"])
- type_combo.setStyleSheet(self._input_style())
- layout.addRow("Alert When:", type_combo)
-
- # Target price
- price_spin = QDoubleSpinBox()
- price_spin.setRange(0.01, 1000000)
- price_spin.setDecimals(2)
- price_spin.setValue(100.0)
- price_spin.setSuffix(" %")
- price_spin.setStyleSheet(self._spinbox_style())
- layout.addRow("Target Price:", price_spin)
-
- # Buttons
- buttons = QDialogButtonBox(
- QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
- )
- buttons.accepted.connect(dialog.accept)
- buttons.rejected.connect(dialog.reject)
- layout.addRow(buttons)
-
- if dialog.exec() == QDialog.DialogCode.Accepted:
- alert_type = 'below' if type_combo.currentIndex() == 0 else 'above'
- self._add_alert(item.id, item.name, alert_type, price_spin.value())
-
- def _add_alert(self, item_id: str, item_name: str, alert_type: str, target_price: float) -> None:
- """Add a new price alert."""
- import uuid
-
- alert_id = str(uuid.uuid4())[:8]
- alert = PriceAlert(
- id=alert_id,
- item_id=item_id,
- item_name=item_name,
- alert_type=alert_type,
- target_price=target_price
- )
-
- self.alerts[alert_id] = alert
- self._save_alerts()
- self._update_alerts_table()
-
- # Check price immediately
- self._check_alert_price(alert)
-
- self.notify_success("Alert Added", f"Monitoring {item_name}")
- self.log_info(f"Added price alert for {item_name}: {alert_type} {target_price}%")
-
- def _remove_alert(self, alert_id: str) -> None:
- """Remove an alert."""
- if alert_id in self.alerts:
- del self.alerts[alert_id]
- self._save_alerts()
- self._update_alerts_table()
- self.log_info(f"Removed alert {alert_id}")
-
- def _check_all_prices(self) -> None:
- """Check prices for all enabled alerts."""
- if not self.alerts:
- return
-
- self.status_label.setText("Checking prices...")
-
- # Check each alert's price
- for alert in self.alerts.values():
- if alert.enabled:
- self._check_alert_price(alert)
-
- # Update UI
- self._update_alerts_table()
-
- # Schedule next check
- next_check = datetime.now() + timedelta(seconds=self.check_interval)
- self.status_label.setText(f"Active Alerts: {len(self.alerts)} | Next check: {next_check.strftime('%H:%M')}")
-
- def _check_alert_price(self, alert: PriceAlert) -> None:
- """Check price for a single alert."""
- try:
- market_data = self.nexus.get_market_data(alert.item_id)
- if market_data and market_data.current_markup is not None:
- current_price = market_data.current_markup
-
- # Update price history
- if alert.item_id not in self.price_history:
- self.price_history[alert.item_id] = []
-
- self.price_history[alert.item_id].append({
- 'timestamp': datetime.now().isoformat(),
- 'price': current_price
- })
-
- # Trim history
- cutoff = datetime.now() - timedelta(days=self.history_days)
- self.price_history[alert.item_id] = [
- h for h in self.price_history[alert.item_id]
- if datetime.fromisoformat(h['timestamp']) > cutoff
- ]
-
- # Check condition
- if alert.check_condition(current_price):
- if not alert.triggered:
- alert.triggered = True
- alert.trigger_count += 1
- self.signals.alert_triggered.emit(alert)
- else:
- alert.triggered = False
-
- alert.last_checked = datetime.now()
- except Exception as e:
- self.log_error(f"Error checking price for {alert.item_name}: {e}")
-
- def _on_alert_triggered(self, alert: PriceAlert) -> None:
- """Handle triggered alert."""
- condition = "dropped below" if alert.alert_type == 'below' else "rose above"
- message = f"{alert.item_name} {condition} {alert.target_price:.1f}% (current: {alert.current_price:.1f}%)"
-
- if self.notifications_enabled:
- self.notify("๐จ Price Alert", message, sound=self.sound_enabled)
-
- if self.sound_enabled:
- self.play_sound('alert')
-
- self.log_info(f"Price alert triggered: {message}")
- self._save_alerts()
-
- def _update_alerts_table(self) -> None:
- """Update the alerts table."""
- if not self.alerts_table:
- return
-
- enabled_alerts = [a for a in self.alerts.values() if a.enabled]
- self.alerts_table.setRowCount(len(enabled_alerts))
-
- for row, alert in enumerate(enabled_alerts):
- # Item name
- name_item = QTableWidgetItem(alert.item_name)
- name_item.setForeground(QColor("white"))
- self.alerts_table.setItem(row, 0, name_item)
-
- # Condition
- condition_text = f"Alert when {'below' if alert.alert_type == 'below' else 'above'}"
- condition_item = QTableWidgetItem(condition_text)
- condition_item.setForeground(QColor("#2196f3"))
- self.alerts_table.setItem(row, 1, condition_item)
-
- # Target price
- target_item = QTableWidgetItem(f"{alert.target_price:.1f}%")
- target_item.setForeground(QColor("#ffc107"))
- self.alerts_table.setItem(row, 2, target_item)
-
- # Current price
- current_text = f"{alert.current_price:.1f}%" if alert.current_price else "--"
- current_item = QTableWidgetItem(current_text)
- current_item.setForeground(QColor("#4caf50" if alert.current_price else "rgba(255,255,255,100)"))
- self.alerts_table.setItem(row, 3, current_item)
-
- # Status
- status_text = "๐จ TRIGGERED" if alert.triggered else ("โ
OK" if alert.last_checked else "โณ Pending")
- status_item = QTableWidgetItem(status_text)
- status_color = "#f44336" if alert.triggered else ("#4caf50" if alert.last_checked else "rgba(255,255,255,100)")
- status_item.setForeground(QColor(status_color))
- self.alerts_table.setItem(row, 4, status_item)
-
- # Actions
- actions_widget = QWidget()
- actions_layout = QHBoxLayout(actions_widget)
- actions_layout.setContentsMargins(4, 2, 4, 2)
-
- remove_btn = QPushButton("๐๏ธ")
- remove_btn.setFixedSize(28, 28)
- remove_btn.setStyleSheet("""
- QPushButton {
- background-color: #f44336;
- color: white;
- border: none;
- border-radius: 4px;
- }
- """)
- remove_btn.clicked.connect(lambda checked, aid=alert.id: self._remove_alert(aid))
- actions_layout.addWidget(remove_btn)
- actions_layout.addStretch()
-
- self.alerts_table.setCellWidget(row, 5, actions_widget)
-
- self.status_label.setText(f"Active Alerts: {len(self.alerts)}")
-
- def _clear_triggered(self) -> None:
- """Clear all triggered alerts."""
- for alert in self.alerts.values():
- alert.triggered = False
- self._save_alerts()
- self._update_alerts_table()
-
- def _apply_settings(self) -> None:
- """Apply and save settings."""
- self.check_interval = self.interval_spin.value() * 60
- self.notifications_enabled = self.notif_checkbox.isChecked()
- self.sound_enabled = self.sound_checkbox.isChecked()
-
- self.set_config('check_interval', self.check_interval)
- self.set_config('notifications_enabled', self.notifications_enabled)
- self.set_config('sound_enabled', self.sound_enabled)
-
- # Update timer
- if self._check_timer:
- self._check_timer.setInterval(self.check_interval * 1000)
-
- self.notify_success("Settings Saved", "Price alert settings updated")
-
- def _save_alerts(self) -> None:
- """Save alerts to storage."""
- alerts_data = {aid: alert.to_dict() for aid, alert in self.alerts.items()}
- self.save_data('alerts', alerts_data)
- self.save_data('price_history', self.price_history)
-
- def _load_alerts(self) -> None:
- """Load alerts from storage."""
- alerts_data = self.load_data('alerts', {})
- for aid, data in alerts_data.items():
- try:
- self.alerts[aid] = PriceAlert.from_dict(data)
- except Exception as e:
- self.log_error(f"Failed to load alert {aid}: {e}")
-
- self.price_history = self.load_data('price_history', {})
-
- def on_hotkey(self) -> None:
- """Handle hotkey press."""
- self._check_all_prices()
- self.notify("Price Check", f"Checked {len(self.alerts)} items")
-
- def shutdown(self) -> None:
- """Cleanup on shutdown."""
- self._save_alerts()
-
- if self._check_timer:
- self._check_timer.stop()
-
- super().shutdown()
diff --git a/plugins/profession_scanner/__init__.py b/plugins/profession_scanner/__init__.py
deleted file mode 100644
index 7248c5f..0000000
--- a/plugins/profession_scanner/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Profession Scanner Plugin
-"""
-
-from .plugin import ProfessionScannerPlugin
-
-__all__ = ["ProfessionScannerPlugin"]
diff --git a/plugins/profession_scanner/plugin.py b/plugins/profession_scanner/plugin.py
deleted file mode 100644
index c35ad5c..0000000
--- a/plugins/profession_scanner/plugin.py
+++ /dev/null
@@ -1,247 +0,0 @@
-"""
-EU-Utility - Profession Scanner Plugin
-
-Scan and track profession progress with OCR.
-"""
-
-import re
-import json
-from datetime import datetime
-from pathlib import Path
-from decimal import Decimal
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
- QFrame, QGroupBox, QComboBox
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal
-
-from plugins.base_plugin import BasePlugin
-
-
-class ProfessionOCRThread(QThread):
- """OCR scan for professions window."""
- scan_complete = pyqtSignal(dict)
- scan_error = pyqtSignal(str)
- progress_update = pyqtSignal(str)
-
- def run(self):
- """Perform OCR scan."""
- try:
- self.progress_update.emit("Capturing screen...")
-
- import pyautogui
- screenshot = pyautogui.screenshot()
-
- self.progress_update.emit("Running OCR...")
-
- # OCR
- try:
- import easyocr
- reader = easyocr.Reader(['en'], verbose=False)
- results = reader.readtext(screenshot)
- text = '\n'.join([r[1] for r in results])
- except:
- import pytesseract
- from PIL import Image
- text = pytesseract.image_to_string(screenshot)
-
- # Parse professions
- professions = self._parse_professions(text)
- self.scan_complete.emit(professions)
-
- except Exception as e:
- self.scan_error.emit(str(e))
-
- def _parse_professions(self, text):
- """Parse profession data from OCR text."""
- professions = {}
-
- # Pattern: ProfessionName Rank %Progress
- # Example: "Laser Pistoleer (Hit) Elite, 72 68.3%"
- lines = text.split('\n')
-
- for line in lines:
- # Match profession with rank and percentage
- match = re.search(r'(\w+(?:\s+\w+)*)\s+\(?(\w+)?\)?\s+(Elite|Champion|Astonishing|Remarkable|Outstanding|Marvelous|Prodigious|Amazing|Incredible|Awesome),?\s+(\d+)[,\s]+(\d+\.?\d*)%?', line)
- if match:
- prof_name = match.group(1).strip()
- spec = match.group(2) or ""
- rank_name = match.group(3)
- rank_num = match.group(4)
- progress = float(match.group(5))
-
- full_name = f"{prof_name} ({spec})" if spec else prof_name
-
- professions[full_name] = {
- 'rank_name': rank_name,
- 'rank_num': int(rank_num),
- 'progress': progress,
- 'scanned_at': datetime.now().isoformat()
- }
-
- return professions
-
-
-class ProfessionScannerPlugin(BasePlugin):
- """Scan and track profession progress."""
-
- name = "Profession Scanner"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Track profession ranks and progress"
- hotkey = "ctrl+shift+p"
-
- def initialize(self):
- """Setup profession scanner."""
- self.data_file = Path("data/professions.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.professions = {}
- self._load_data()
-
- def _load_data(self):
- """Load saved data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.professions = data.get('professions', {})
- except:
- pass
-
- def _save_data(self):
- """Save data."""
- with open(self.data_file, 'w') as f:
- json.dump({'professions': self.professions}, f, indent=2)
-
- def get_ui(self):
- """Create profession scanner UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Header
- header = QLabel("Profession Tracker")
- header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
- layout.addWidget(header)
-
- # Summary
- summary = QHBoxLayout()
- self.total_label = QLabel(f"Professions: {len(self.professions)}")
- self.total_label.setStyleSheet("color: #4ecdc4; font-weight: bold;")
- summary.addWidget(self.total_label)
-
- summary.addStretch()
- layout.addLayout(summary)
-
- # Scan button
- scan_btn = QPushButton("Scan Professions Window")
- scan_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- scan_btn.clicked.connect(self._scan_professions)
- layout.addWidget(scan_btn)
-
- # Progress
- self.progress_label = QLabel("Ready to scan")
- self.progress_label.setStyleSheet("color: rgba(255,255,255,150);")
- layout.addWidget(self.progress_label)
-
- # Professions table
- self.prof_table = QTableWidget()
- self.prof_table.setColumnCount(4)
- self.prof_table.setHorizontalHeaderLabels(["Profession", "Rank", "Level", "Progress"])
- self.prof_table.horizontalHeader().setStretchLastSection(True)
-
- # Style table
- self.prof_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 8px;
- font-weight: bold;
- }
- """)
-
- layout.addWidget(self.prof_table)
-
- # Refresh table
- self._refresh_table()
-
- return widget
-
- def _scan_professions(self):
- """Start OCR scan."""
- self.scanner = ProfessionOCRThread()
- self.scanner.scan_complete.connect(self._on_scan_complete)
- self.scanner.scan_error.connect(self._on_scan_error)
- self.scanner.progress_update.connect(self._on_progress)
- self.scanner.start()
-
- def _on_progress(self, message):
- """Update progress."""
- self.progress_label.setText(message)
-
- def _on_scan_complete(self, professions):
- """Handle scan completion."""
- self.professions.update(professions)
- self._save_data()
- self._refresh_table()
- self.progress_label.setText(f"Found {len(professions)} professions")
- self.total_label.setText(f"Professions: {len(self.professions)}")
-
- def _on_scan_error(self, error):
- """Handle error."""
- self.progress_label.setText(f"Error: {error}")
-
- def _refresh_table(self):
- """Refresh professions table."""
- self.prof_table.setRowCount(len(self.professions))
-
- for i, (name, data) in enumerate(sorted(self.professions.items())):
- self.prof_table.setItem(i, 0, QTableWidgetItem(name))
- self.prof_table.setItem(i, 1, QTableWidgetItem(data.get('rank_name', '-')))
- self.prof_table.setItem(i, 2, QTableWidgetItem(str(data.get('rank_num', 0))))
-
- # Progress with bar
- progress = data.get('progress', 0)
- progress_widget = QWidget()
- progress_layout = QHBoxLayout(progress_widget)
- progress_layout.setContentsMargins(5, 2, 5, 2)
-
- bar = QProgressBar()
- bar.setValue(int(progress))
- bar.setTextVisible(True)
- bar.setFormat(f"{progress:.1f}%")
- bar.setStyleSheet("""
- QProgressBar {
- background-color: rgba(60, 70, 90, 150);
- border: none;
- border-radius: 3px;
- text-align: center;
- color: white;
- }
- QProgressBar::chunk {
- background-color: #ff8c42;
- border-radius: 3px;
- }
- """)
- progress_layout.addWidget(bar)
-
- self.prof_table.setCellWidget(i, 3, progress_widget)
diff --git a/plugins/session_exporter/__init__.py b/plugins/session_exporter/__init__.py
deleted file mode 100644
index 68eccfd..0000000
--- a/plugins/session_exporter/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""
-Session Exporter Plugin for EU-Utility
-
-Export hunting/mining/crafting sessions to CSV/JSON formats.
-"""
-
-from .plugin import SessionExporterPlugin
-
-__all__ = ['SessionExporterPlugin']
diff --git a/plugins/session_exporter/plugin.py b/plugins/session_exporter/plugin.py
deleted file mode 100644
index f001c5f..0000000
--- a/plugins/session_exporter/plugin.py
+++ /dev/null
@@ -1,643 +0,0 @@
-"""
-EU-Utility - Session Exporter Plugin
-
-Export hunting/mining/crafting sessions to CSV/JSON formats.
-Tracks loot, skills gained, globals, and other session data.
-"""
-
-import json
-import csv
-from datetime import datetime
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-from dataclasses import dataclass, field, asdict
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
- QTableWidget, QTableWidgetItem, QHeaderView, QComboBox,
- QFileDialog, QMessageBox, QGroupBox, QSpinBox, QCheckBox,
- QTabWidget, QTextEdit, QSplitter
-)
-from PyQt6.QtCore import Qt, QTimer
-from PyQt6.QtGui import QColor
-
-from plugins.base_plugin import BasePlugin
-from core.event_bus import (
- LootEvent, SkillGainEvent, GlobalEvent, DamageEvent,
- EventCategory, get_event_bus
-)
-
-
-@dataclass
-class SessionEntry:
- """Single session entry representing an event."""
- timestamp: datetime
- event_type: str
- description: str
- value: float = 0.0
- details: Dict[str, Any] = field(default_factory=dict)
-
-
-@dataclass
-class SessionSummary:
- """Summary statistics for a session."""
- start_time: datetime
- end_time: Optional[datetime] = None
- total_loot_tt: float = 0.0
- total_loot_count: int = 0
- globals_count: int = 0
- hofs_count: int = 0
- skill_gains: int = 0
- total_damage_dealt: float = 0.0
- total_damage_taken: float = 0.0
- unique_items: List[str] = field(default_factory=list)
-
-
-class SessionExporterPlugin(BasePlugin):
- """
- Plugin for exporting hunting/mining sessions to various formats.
-
- Features:
- - Automatic session tracking from event bus
- - Export to CSV or JSON
- - Session statistics and summaries
- - Configurable auto-export
- """
-
- name = "Session Exporter"
- version = "1.0.0"
- author = "EU-Utility"
- description = "Export hunting/mining sessions to CSV/JSON"
- icon = "๐"
- hotkey = "ctrl+shift+e"
-
- def __init__(self, overlay_window, config):
- super().__init__(overlay_window, config)
-
- # Session state
- self.session_active = False
- self.session_start_time: Optional[datetime] = None
- self.session_entries: List[SessionEntry] = []
- self.session_summary = None
-
- # Event subscriptions
- self._subscriptions: List[str] = []
-
- # Auto-export settings
- self.auto_export_enabled = self.get_config('auto_export', False)
- self.auto_export_interval = self.get_config('auto_export_interval', 300) # 5 minutes
- self.auto_export_format = self.get_config('auto_export_format', 'json')
-
- # Export settings
- self.export_directory = self.get_config('export_directory', str(Path.home() / "Documents" / "EU-Sessions"))
- Path(self.export_directory).mkdir(parents=True, exist_ok=True)
-
- # UI references
- self._ui = None
- self.session_table = None
- self.status_label = None
- self.stats_label = None
-
- # Auto-export timer
- self._export_timer = None
-
- def initialize(self) -> None:
- """Initialize plugin and subscribe to events."""
- self.log_info("Initializing Session Exporter")
-
- # Subscribe to events
- self._subscriptions.append(
- self.subscribe_typed(LootEvent, self._on_loot)
- )
- self._subscriptions.append(
- self.subscribe_typed(SkillGainEvent, self._on_skill_gain)
- )
- self._subscriptions.append(
- self.subscribe_typed(GlobalEvent, self._on_global)
- )
- self._subscriptions.append(
- self.subscribe_typed(DamageEvent, self._on_damage)
- )
-
- # Start session automatically
- self.start_session()
-
- # Setup auto-export timer if enabled
- if self.auto_export_enabled:
- self._setup_auto_export()
-
- self.log_info("Session Exporter initialized")
-
- def get_ui(self) -> QWidget:
- """Return the plugin's UI widget."""
- if self._ui is None:
- self._ui = self._create_ui()
- return self._ui
-
- def _create_ui(self) -> QWidget:
- """Create the plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(12)
- layout.setContentsMargins(16, 16, 16, 16)
-
- # Header
- header = QLabel("๐ Session Exporter")
- header.setStyleSheet("""
- font-size: 18px;
- font-weight: bold;
- color: white;
- padding-bottom: 8px;
- """)
- layout.addWidget(header)
-
- # Status section
- status_group = QGroupBox("Session Status")
- status_layout = QVBoxLayout(status_group)
-
- self.status_label = QLabel("Session: Active")
- self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;")
- status_layout.addWidget(self.status_label)
-
- self.stats_label = QLabel(self._get_stats_text())
- self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 150);")
- status_layout.addWidget(self.stats_label)
-
- layout.addWidget(status_group)
-
- # Control buttons
- button_layout = QHBoxLayout()
-
- self.start_btn = QPushButton("โถ๏ธ Start New")
- self.start_btn.setStyleSheet(self._button_style())
- self.start_btn.clicked.connect(self.start_session)
- button_layout.addWidget(self.start_btn)
-
- self.stop_btn = QPushButton("โน๏ธ Stop")
- self.stop_btn.setStyleSheet(self._button_style())
- self.stop_btn.clicked.connect(self.stop_session)
- button_layout.addWidget(self.stop_btn)
-
- self.export_csv_btn = QPushButton("๐ Export CSV")
- self.export_csv_btn.setStyleSheet(self._button_style("#2196f3"))
- self.export_csv_btn.clicked.connect(lambda: self.export_session('csv'))
- button_layout.addWidget(self.export_csv_btn)
-
- self.export_json_btn = QPushButton("๐ Export JSON")
- self.export_json_btn.setStyleSheet(self._button_style("#2196f3"))
- self.export_json_btn.clicked.connect(lambda: self.export_session('json'))
- button_layout.addWidget(self.export_json_btn)
-
- layout.addLayout(button_layout)
-
- # Session entries table
- table_group = QGroupBox("Session Entries")
- table_layout = QVBoxLayout(table_group)
-
- self.session_table = QTableWidget()
- self.session_table.setColumnCount(4)
- self.session_table.setHorizontalHeaderLabels(["Time", "Type", "Description", "Value"])
- self.session_table.horizontalHeader().setStretchLastSection(True)
- self.session_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
- self.session_table.setStyleSheet("""
- QTableWidget {
- background-color: rgba(30, 35, 45, 150);
- border: 1px solid rgba(100, 150, 200, 50);
- border-radius: 8px;
- color: white;
- }
- QHeaderView::section {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- padding: 8px;
- border: none;
- }
- QTableWidget::item {
- padding: 6px;
- }
- """)
- table_layout.addWidget(self.session_table)
-
- layout.addWidget(table_group)
-
- # Settings
- settings_group = QGroupBox("Settings")
- settings_layout = QVBoxLayout(settings_group)
-
- # Auto-export
- auto_export_layout = QHBoxLayout()
- self.auto_export_checkbox = QCheckBox("Auto-export every")
- self.auto_export_checkbox.setChecked(self.auto_export_enabled)
- self.auto_export_checkbox.setStyleSheet("color: white;")
- auto_export_layout.addWidget(self.auto_export_checkbox)
-
- self.auto_export_spin = QSpinBox()
- self.auto_export_spin.setRange(1, 60)
- self.auto_export_spin.setValue(self.auto_export_interval // 60)
- self.auto_export_spin.setSuffix(" min")
- self.auto_export_spin.setStyleSheet("""
- QSpinBox {
- background-color: rgba(50, 60, 75, 200);
- color: white;
- border: 1px solid rgba(100, 150, 200, 100);
- border-radius: 4px;
- padding: 4px;
- }
- """)
- auto_export_layout.addWidget(self.auto_export_spin)
-
- auto_export_layout.addStretch()
- settings_layout.addLayout(auto_export_layout)
-
- # Export directory
- dir_layout = QHBoxLayout()
- dir_label = QLabel("Export Directory:")
- dir_label.setStyleSheet("color: rgba(255, 255, 255, 150);")
- dir_layout.addWidget(dir_label)
-
- self.dir_display = QLabel(self.export_directory)
- self.dir_display.setStyleSheet("color: white;")
- self.dir_display.setWordWrap(True)
- dir_layout.addWidget(self.dir_display, 1)
-
- change_dir_btn = QPushButton("๐ Change")
- change_dir_btn.setStyleSheet(self._button_style())
- change_dir_btn.clicked.connect(self._change_export_directory)
- dir_layout.addWidget(change_dir_btn)
-
- settings_layout.addLayout(dir_layout)
-
- layout.addWidget(settings_group)
-
- # Apply settings button
- apply_btn = QPushButton("๐พ Apply Settings")
- apply_btn.setStyleSheet(self._button_style("#4caf50"))
- apply_btn.clicked.connect(self._apply_settings)
- layout.addWidget(apply_btn)
-
- layout.addStretch()
-
- return widget
-
- def _button_style(self, color: str = "#607d8b") -> str:
- """Generate button stylesheet."""
- return f"""
- QPushButton {{
- background-color: {color};
- color: white;
- border: none;
- border-radius: 8px;
- padding: 10px 16px;
- font-weight: bold;
- }}
- QPushButton:hover {{
- background-color: {color}dd;
- }}
- QPushButton:pressed {{
- background-color: {color}aa;
- }}
- """
-
- def start_session(self) -> None:
- """Start a new tracking session."""
- self.session_active = True
- self.session_start_time = datetime.now()
- self.session_entries = []
- self.session_summary = SessionSummary(start_time=self.session_start_time)
-
- self.log_info(f"Session started at {self.session_start_time}")
-
- if self.status_label:
- self.status_label.setText(f"Session: Active (started {self.session_start_time.strftime('%H:%M:%S')})")
- self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;")
-
- self.notify("Session Started", "Tracking loot, skills, and globals")
-
- def stop_session(self) -> None:
- """Stop the current tracking session."""
- if not self.session_active:
- return
-
- self.session_active = False
- if self.session_summary:
- self.session_summary.end_time = datetime.now()
-
- self.log_info("Session stopped")
-
- if self.status_label:
- duration = ""
- if self.session_summary and self.session_summary.end_time:
- duration_secs = (self.session_summary.end_time - self.session_start_time).total_seconds()
- mins, secs = divmod(int(duration_secs), 60)
- hours, mins = divmod(mins, 60)
- duration = f"Duration: {hours:02d}:{mins:02d}:{secs:02d}"
-
- self.status_label.setText(f"Session: Stopped ({duration})")
- self.status_label.setStyleSheet("color: #ff9800; font-weight: bold;")
-
- self.notify("Session Stopped", f"Total entries: {len(self.session_entries)}")
-
- def _on_loot(self, event: LootEvent) -> None:
- """Handle loot events."""
- if not self.session_active:
- return
-
- item_names = ", ".join(event.get_item_names())
- entry = SessionEntry(
- timestamp=event.timestamp,
- event_type="Loot",
- description=f"From {event.mob_name}: {item_names}",
- value=event.total_tt_value,
- details={
- 'mob_name': event.mob_name,
- 'items': event.items,
- 'position': event.position
- }
- )
-
- self.session_entries.append(entry)
-
- if self.session_summary:
- self.session_summary.total_loot_tt += event.total_tt_value
- self.session_summary.total_loot_count += 1
- for item in event.get_item_names():
- if item not in self.session_summary.unique_items:
- self.session_summary.unique_items.append(item)
-
- self._update_ui()
-
- def _on_skill_gain(self, event: SkillGainEvent) -> None:
- """Handle skill gain events."""
- if not self.session_active:
- return
-
- entry = SessionEntry(
- timestamp=event.timestamp,
- event_type="Skill",
- description=f"{event.skill_name} +{event.gain_amount:.4f}",
- value=event.gain_amount,
- details={
- 'skill_name': event.skill_name,
- 'skill_value': event.skill_value,
- 'gain_amount': event.gain_amount
- }
- )
-
- self.session_entries.append(entry)
-
- if self.session_summary:
- self.session_summary.skill_gains += 1
-
- self._update_ui()
-
- def _on_global(self, event: GlobalEvent) -> None:
- """Handle global/HOF events."""
- if not self.session_active:
- return
-
- is_hof = event.achievement_type.lower() in ['hof', 'ath', 'discovery']
-
- entry = SessionEntry(
- timestamp=event.timestamp,
- event_type="HOF" if is_hof else "Global",
- description=f"{event.player_name}: {event.item_name or 'Value'} worth {event.value:.0f} PED",
- value=event.value,
- details={
- 'player_name': event.player_name,
- 'achievement_type': event.achievement_type,
- 'item_name': event.item_name
- }
- )
-
- self.session_entries.append(entry)
-
- if self.session_summary:
- if is_hof:
- self.session_summary.hofs_count += 1
- else:
- self.session_summary.globals_count += 1
-
- self._update_ui()
-
- def _on_damage(self, event: DamageEvent) -> None:
- """Handle damage events."""
- if not self.session_active:
- return
-
- if event.is_outgoing:
- if self.session_summary:
- self.session_summary.total_damage_dealt += event.damage_amount
- else:
- if self.session_summary:
- self.session_summary.total_damage_taken += event.damage_amount
-
- self._update_ui()
-
- def _update_ui(self) -> None:
- """Update the UI with current session data."""
- if not self._ui or not self.session_table:
- return
-
- # Update stats label
- if self.stats_label:
- self.stats_label.setText(self._get_stats_text())
-
- # Update table (show last 50 entries)
- recent_entries = self.session_entries[-50:]
- self.session_table.setRowCount(len(recent_entries))
-
- type_colors = {
- 'Loot': '#4caf50',
- 'Skill': '#2196f3',
- 'Global': '#ff9800',
- 'HOF': '#f44336'
- }
-
- for row, entry in enumerate(recent_entries):
- # Time
- time_item = QTableWidgetItem(entry.timestamp.strftime("%H:%M:%S"))
- time_item.setForeground(QColor("white"))
- self.session_table.setItem(row, 0, time_item)
-
- # Type
- type_item = QTableWidgetItem(entry.event_type)
- type_item.setForeground(QColor(type_colors.get(entry.event_type, 'white')))
- self.session_table.setItem(row, 1, type_item)
-
- # Description
- desc_item = QTableWidgetItem(entry.description)
- desc_item.setForeground(QColor("white"))
- self.session_table.setItem(row, 2, desc_item)
-
- # Value
- value_item = QTableWidgetItem(f"{entry.value:.2f}")
- value_item.setForeground(QColor("#ffc107"))
- self.session_table.setItem(row, 3, value_item)
-
- # Scroll to bottom
- self.session_table.scrollToBottom()
-
- def _get_stats_text(self) -> str:
- """Get formatted statistics text."""
- if not self.session_summary:
- return "No active session"
-
- lines = [
- f"Entries: {len(self.session_entries)}",
- f"Loot: {self.session_summary.total_loot_count} items, {self.session_summary.total_loot_tt:.2f} PED TT",
- f"Globals: {self.session_summary.globals_count} | HOFs: {self.session_summary.hofs_count}",
- f"Skills: {self.session_summary.skill_gains}",
- ]
-
- if self.session_summary.total_damage_dealt > 0:
- lines.append(f"Damage Dealt: {self.session_summary.total_damage_dealt:,.0f}")
-
- return " | ".join(lines)
-
- def export_session(self, format_type: str = 'json') -> Optional[Path]:
- """
- Export the current session to a file.
-
- Args:
- format_type: 'json' or 'csv'
-
- Returns:
- Path to exported file or None if failed
- """
- if not self.session_entries:
- self.notify_warning("Export Failed", "No session data to export")
- return None
-
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- filename = f"session_{timestamp}.{format_type}"
- filepath = Path(self.export_directory) / filename
-
- try:
- if format_type == 'json':
- self._export_json(filepath)
- elif format_type == 'csv':
- self._export_csv(filepath)
- else:
- raise ValueError(f"Unknown format: {format_type}")
-
- self.notify_success("Export Complete", f"Saved to {filename}")
- self.log_info(f"Session exported to {filepath}")
- return filepath
-
- except Exception as e:
- self.notify_error("Export Failed", str(e))
- self.log_error(f"Export failed: {e}")
- return None
-
- def _export_json(self, filepath: Path) -> None:
- """Export session to JSON format."""
- data = {
- 'export_info': {
- 'version': '1.0.0',
- 'exported_at': datetime.now().isoformat(),
- 'plugin': 'Session Exporter'
- },
- 'session': {
- 'start_time': self.session_start_time.isoformat() if self.session_start_time else None,
- 'end_time': self.session_summary.end_time.isoformat() if self.session_summary and self.session_summary.end_time else None,
- 'summary': asdict(self.session_summary) if self.session_summary else {},
- 'entries': [
- {
- 'timestamp': e.timestamp.isoformat(),
- 'event_type': e.event_type,
- 'description': e.description,
- 'value': e.value,
- 'details': e.details
- }
- for e in self.session_entries
- ]
- }
- }
-
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
-
- def _export_csv(self, filepath: Path) -> None:
- """Export session to CSV format."""
- with open(filepath, 'w', newline='', encoding='utf-8') as f:
- writer = csv.writer(f)
-
- # Write header
- writer.writerow(['timestamp', 'event_type', 'description', 'value', 'details'])
-
- # Write entries
- for entry in self.session_entries:
- writer.writerow([
- entry.timestamp.isoformat(),
- entry.event_type,
- entry.description,
- entry.value,
- json.dumps(entry.details)
- ])
-
- def _change_export_directory(self) -> None:
- """Change the export directory."""
- new_dir = QFileDialog.getExistingDirectory(
- self._ui,
- "Select Export Directory",
- self.export_directory
- )
-
- if new_dir:
- self.export_directory = new_dir
- if self.dir_display:
- self.dir_display.setText(new_dir)
-
- def _apply_settings(self) -> None:
- """Apply and save settings."""
- self.auto_export_enabled = self.auto_export_checkbox.isChecked()
- self.auto_export_interval = self.auto_export_spin.value() * 60
-
- self.set_config('auto_export', self.auto_export_enabled)
- self.set_config('auto_export_interval', self.auto_export_interval)
- self.set_config('export_directory', self.export_directory)
-
- # Update auto-export timer
- if self.auto_export_enabled:
- self._setup_auto_export()
- elif self._export_timer:
- self._export_timer.stop()
-
- self.notify_success("Settings Saved", "Session exporter settings updated")
-
- def _setup_auto_export(self) -> None:
- """Setup auto-export timer."""
- if self._export_timer:
- self._export_timer.stop()
-
- self._export_timer = QTimer()
- self._export_timer.timeout.connect(lambda: self.export_session(self.auto_export_format))
- self._export_timer.start(self.auto_export_interval * 1000)
-
- self.log_info(f"Auto-export enabled every {self.auto_export_interval // 60} minutes")
-
- def on_hotkey(self) -> None:
- """Handle hotkey press."""
- if self.session_active:
- self.stop_session()
- else:
- self.start_session()
-
- def shutdown(self) -> None:
- """Cleanup on shutdown."""
- # Auto-export on shutdown if enabled
- if self.auto_export_enabled and self.session_entries:
- self.export_session(self.auto_export_format)
-
- # Stop session
- if self.session_active:
- self.stop_session()
-
- # Unsubscribe from events
- for sub_id in self._subscriptions:
- self.unsubscribe_typed(sub_id)
-
- if self._export_timer:
- self._export_timer.stop()
-
- super().shutdown()
diff --git a/plugins/skill_scanner/__init__.py b/plugins/skill_scanner/__init__.py
deleted file mode 100644
index bf789dd..0000000
--- a/plugins/skill_scanner/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Skill Scanner Plugin for EU-Utility
-"""
-
-from .plugin import SkillScannerPlugin
-
-__all__ = ["SkillScannerPlugin"]
diff --git a/plugins/skill_scanner/plugin.py b/plugins/skill_scanner/plugin.py
deleted file mode 100644
index 0ba4b62..0000000
--- a/plugins/skill_scanner/plugin.py
+++ /dev/null
@@ -1,1240 +0,0 @@
-"""
-EU-Utility - Skill Scanner Plugin
-
-Uses core OCR and Log services via PluginAPI.
-"""
-
-import re
-import json
-from datetime import datetime
-from pathlib import Path
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
- QFrame, QGroupBox, QTextEdit, QSplitter, QComboBox
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QObject
-
-from plugins.base_plugin import BasePlugin
-
-
-def find_entropia_window():
- """
- Find the Entropia Universe game window.
- Returns (left, top, width, height) or None if not found.
- """
- try:
- import platform
-
- if platform.system() == 'Windows':
- import win32gui
- import win32process
- import psutil
-
- def callback(hwnd, windows):
- if not win32gui.IsWindowVisible(hwnd):
- return True
-
- # Get window title
- title = win32gui.GetWindowText(hwnd)
-
- # Check if it's an Entropia window (title contains "Entropia Universe")
- if 'Entropia Universe' in title:
- try:
- # Get process ID
- _, pid = win32process.GetWindowThreadProcessId(hwnd)
- process = psutil.Process(pid)
-
- # Verify process name contains "Entropia"
- if 'entropia' in process.name().lower():
- rect = win32gui.GetWindowRect(hwnd)
- left, top, right, bottom = rect
- windows.append((left, top, right - left, bottom - top, title))
- except:
- pass
-
- return True
-
- windows = []
- win32gui.EnumWindows(callback, windows)
-
- if windows:
- # Return the largest window (most likely the main game)
- windows.sort(key=lambda w: w[2] * w[3], reverse=True)
- left, top, width, height, title = windows[0]
- print(f"[SkillScanner] Found Entropia window: '{title}' at ({left}, {top}, {width}, {height})")
- return (left, top, width, height)
-
- elif platform.system() == 'Linux':
- # Try using xdotool or wmctrl
- try:
- import subprocess
- result = subprocess.run(
- ['xdotool', 'search', '--name', 'Entropia Universe'],
- capture_output=True, text=True
- )
- if result.returncode == 0 and result.stdout.strip():
- window_id = result.stdout.strip().split('\n')[0]
- # Get window geometry
- geo_result = subprocess.run(
- ['xdotool', 'getwindowgeometry', window_id],
- capture_output=True, text=True
- )
- if geo_result.returncode == 0:
- # Parse: "Position: 100,200 (screen: 0)" and "Geometry: 1920x1080"
- pos_match = re.search(r'Position: (\d+),(\d+)', geo_result.stdout)
- geo_match = re.search(r'Geometry: (\d+)x(\d+)', geo_result.stdout)
- if pos_match and geo_match:
- left = int(pos_match.group(1))
- top = int(pos_match.group(2))
- width = int(geo_match.group(1))
- height = int(geo_match.group(2))
- print(f"[SkillScanner] Found Entropia window at ({left}, {top}, {width}, {height})")
- return (left, top, width, height)
- except Exception as e:
- print(f"[SkillScanner] Linux window detection failed: {e}")
-
- except Exception as e:
- print(f"[SkillScanner] Window detection error: {e}")
-
- print("[SkillScanner] Could not find Entropia window - will use full screen")
- return None
-
-
-def capture_entropia_region(region=None):
- """
- Capture screen region of Entropia window.
- If region is None, tries to find the window automatically.
- Returns PIL Image or None.
- """
- try:
- from PIL import ImageGrab
-
- if region is None:
- region = find_entropia_window()
-
- if region:
- left, top, width, height = region
- # Add some padding to ensure we capture everything
- # Don't go below 0
- left = max(0, left)
- top = max(0, top)
- screenshot = ImageGrab.grab(bbox=(left, top, left + width, top + height))
- print(f"[SkillScanner] Captured Entropia window region: {width}x{height}")
- return screenshot
- else:
- # Fallback to full screen
- screenshot = ImageGrab.grab()
- print("[SkillScanner] Captured full screen (window not found)")
- return screenshot
-
- except Exception as e:
- print(f"[SkillScanner] Capture error: {e}")
- return None
-
-
-def is_valid_skill_text(text):
- """
- Filter out non-game text from OCR results.
- Returns True if text looks like it could be from the game.
- """
- # List of patterns that indicate NON-game text (UI, Discord, etc.)
- invalid_patterns = [
- # App/UI elements
- 'Discord', 'Presence', 'Event Bus', 'Example', 'Game Reader',
- 'Test', 'Page Scanner', 'HOTKEY MODE', 'Skill Tracker',
- 'Navigate', 'window', 'UI', 'Plugin', 'Settings',
- 'Click', 'Button', 'Menu', 'Panel', 'Tab', 'Loading...',
- 'Calculator', 'Nexus Search', 'Dashboard', 'Analytics',
- 'Multi-Page', 'Scanner', 'Auto-detect', 'F12',
- 'Cleared', 'Parsed:', '[SkillScanner]', 'INFO', 'DEBUG',
- 'Loading', 'Initializing', 'Connecting', 'Error:', 'Warning:',
- 'Entropia.exe', 'Client (64 bit)', 'Arkadia', 'Calypso',
- # Instructions from our own UI
- 'Position Skills', 'Position Skills window', 'Start Smart Scan',
- 'Scan Current Page', 'Save All', 'Clear Session',
- 'Select Area', 'Drag over', 'Navigate pages',
- # Column headers that might be picked up
- 'Skill', 'Skills', 'Rank', 'Points', 'Name',
- # Category names with extra text
- 'Combat Wounding', 'Combat Serendipity', 'Combat Reflexes',
- 'Scan Serendipity', 'Scan Wounding', 'Scan Reflexes',
- 'Position Wounding', 'Position Serendipity', 'Position Reflexes',
- 'Current Page', 'Smart Scan', 'All Scanned',
- ]
-
- # Check for invalid patterns
- text_upper = text.upper()
- for pattern in invalid_patterns:
- if pattern.upper() in text_upper:
- return False
-
- # Check for reasonable skill name length (not too long, not too short)
- words = text.split()
- if len(words) > 7: # Skills rarely have 7+ words (reduced from 10)
- return False
-
- # Check if text contains button/action words combined with skill-like text
- action_words = ['Click', 'Scan', 'Position', 'Select', 'Navigate', 'Start', 'Save', 'Clear']
- text_lower = text.lower()
- for word in action_words:
- if word.lower() in text_lower:
- # If it has action words, it's probably UI text
- return False
-
- return True
-
-
-class SkillOCRThread(QThread):
- """OCR scan using core service."""
- scan_complete = pyqtSignal(dict)
- scan_error = pyqtSignal(str)
- progress_update = pyqtSignal(str)
-
- def __init__(self, ocr_service, scan_area=None):
- super().__init__()
- self.ocr_service = ocr_service
- self.scan_area = scan_area # (x, y, width, height) or None
-
- def run(self):
- """Perform OCR using core service - only on selected area or Entropia window."""
- try:
- if self.scan_area:
- # Use user-selected area
- x, y, w, h = self.scan_area
- self.progress_update.emit(f"Capturing selected area ({w}x{h})...")
-
- from PIL import ImageGrab
- screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
- else:
- # Capture Entropia game window
- self.progress_update.emit("Finding Entropia window...")
- screenshot = capture_entropia_region()
-
- if screenshot is None:
- self.scan_error.emit("Could not capture screen")
- return
-
- self.progress_update.emit("Running OCR...")
-
- # Use core OCR service with the captured image
- result = self.ocr_service.recognize_image(screenshot)
-
- if 'error' in result and result['error']:
- self.scan_error.emit(result['error'])
- return
-
- self.progress_update.emit("Parsing skills...")
-
- # Parse skills from text
- raw_text = result.get('text', '')
- skills_data = self._parse_skills_filtered(raw_text)
-
- if not skills_data:
- self.progress_update.emit("No skills found. Make sure Skills window is visible.")
- else:
- self.progress_update.emit(f"Found {len(skills_data)} skills")
-
- self.scan_complete.emit(skills_data)
-
- except Exception as e:
- self.scan_error.emit(str(e))
-
- if not skills_data:
- self.progress_update.emit("No valid skills found. Make sure Skills window is open.")
- else:
- self.progress_update.emit(f"Found {len(skills_data)} skills")
-
- self.scan_complete.emit(skills_data)
-
- except Exception as e:
- self.scan_error.emit(str(e))
-
- def _parse_skills(self, text):
- """Parse skill data from OCR text with improved handling for 3-column layout."""
- skills = {}
-
- # Ranks in Entropia Universe (including multi-word ranks)
- # Single word ranks
- SINGLE_RANKS = [
- 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
- 'Skilled', 'Expert', 'Professional', 'Master',
- 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
- 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
- ]
- # Multi-word ranks (must be checked first - longer matches first)
- MULTI_RANKS = [
- 'Arch Master', 'Grand Master'
- ]
-
- # Combine: multi-word first (so they match before single word), then single
- ALL_RANKS = MULTI_RANKS + SINGLE_RANKS
- rank_pattern = '|'.join(ALL_RANKS)
-
- # Clean up the text - remove common headers and junk
- text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
- text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
-
- # Remove category names that appear as standalone words
- for category in ['Attributes', 'COMBAT', 'Combat', 'Design', 'Construction',
- 'Defense', 'General', 'Handgun', 'Heavy Melee Weapons',
- 'Heavy Weapons', 'Information', 'Inflict Melee Damage',
- 'Inflict Ranged Damage', 'Light Melee Weapons', 'Longblades',
- 'Medical', 'Mining', 'Science', 'Social', 'Beauty', 'Mindforce']:
- text = text.replace(category, ' ')
-
- # Remove extra whitespace
- text = ' '.join(text.split())
-
- # Find all skills in the text using finditer
- for match in re.finditer(
- rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)',
- text, re.IGNORECASE
- ):
- skill_name = match.group(1).strip()
- rank = match.group(2)
- points = int(match.group(3))
-
- # Clean up skill name - remove common words that might be prepended
- skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
- skill_name = skill_name.strip()
-
- # Validate skill name - filter out UI text
- if not is_valid_skill_text(skill_name):
- print(f"[SkillScanner] Filtered invalid skill name: '{skill_name}'")
- continue
-
- # Validate - points should be reasonable (not too small)
- if points > 0 and skill_name and len(skill_name) > 2:
- skills[skill_name] = {
- 'rank': rank,
- 'points': points,
- 'scanned_at': datetime.now().isoformat()
- }
- print(f"[SkillScanner] Parsed: {skill_name} = {rank} ({points})")
-
- # If no skills found with primary method, try alternative
- if not skills:
- skills = self._parse_skills_alternative(text, ALL_RANKS)
-
- return skills
-
- def _parse_skills_filtered(self, text):
- """
- Parse skills with filtering to remove non-game text.
- Only returns skills that pass validity checks.
- """
- # First, split text into lines and filter each line
- lines = text.split('\n')
- valid_lines = []
-
- for line in lines:
- line = line.strip()
- if not line:
- continue
-
- # Check if line contains a rank (required for skill lines)
- has_rank = any(rank in line for rank in [
- 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
- 'Skilled', 'Expert', 'Professional', 'Master',
- 'Arch Master', 'Grand Master', # Multi-word first
- 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
- 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
- ])
-
- if not has_rank:
- continue # Skip lines without ranks
-
- # Check for invalid patterns
- if not is_valid_skill_text(line):
- print(f"[SkillScanner] Filtered out: '{line[:50]}...'")
- continue
-
- valid_lines.append(line)
-
- # Join valid lines and parse
- filtered_text = '\n'.join(valid_lines)
-
- if not filtered_text.strip():
- print("[SkillScanner] No valid game text found after filtering")
- return {}
-
- print(f"[SkillScanner] Filtered {len(lines)} lines to {len(valid_lines)} valid lines")
-
- return self._parse_skills(filtered_text)
-
- def _parse_skills_alternative(self, text, ranks):
- """Alternative parser for when text is heavily merged."""
- skills = {}
-
- # Find all rank positions in the text
- for rank in ranks:
- # Look for pattern: [text] [Rank] [number]
- pattern = rf'([A-Z][a-z]{{2,}}(?:\s+[A-Z][a-z]{{2,}}){{0,3}})\s+{re.escape(rank)}\s+(\d{{1,6}})'
- matches = re.finditer(pattern, text, re.IGNORECASE)
-
- for match in matches:
- skill_name = match.group(1).strip()
- points = int(match.group(2))
-
- # Clean skill name
- skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
-
- if points > 0 and len(skill_name) > 2:
- skills[skill_name] = {
- 'rank': rank,
- 'points': points,
- 'scanned_at': datetime.now().isoformat()
- }
-
- return skills
-
-
-class SnippingWidget(QWidget):
- """Fullscreen overlay for snipping tool-style area selection."""
-
- area_selected = pyqtSignal(int, int, int, int) # x, y, width, height
- cancelled = pyqtSignal()
-
- def __init__(self):
- super().__init__()
- self.setWindowFlags(
- Qt.WindowType.FramelessWindowHint |
- Qt.WindowType.WindowStaysOnTopHint |
- Qt.WindowType.Tool
- )
- self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
-
- # Get screen geometry
- from PyQt6.QtWidgets import QApplication
- screen = QApplication.primaryScreen().geometry()
- self.setGeometry(screen)
-
- self.begin = None
- self.end = None
- self.drawing = False
-
- # Semi-transparent dark overlay
- self.overlay_color = Qt.GlobalColor.black
- self.overlay_opacity = 160 # 0-255
-
- def paintEvent(self, event):
- from PyQt6.QtGui import QPainter, QPen, QColor, QBrush
-
- painter = QPainter(self)
-
- # Draw semi-transparent overlay
- overlay = QColor(0, 0, 0, self.overlay_opacity)
- painter.fillRect(self.rect(), overlay)
-
- if self.begin and self.end:
- # Clear overlay in selected area (make it transparent)
- x = min(self.begin.x(), self.end.x())
- y = min(self.begin.y(), self.end.y())
- w = abs(self.end.x() - self.begin.x())
- h = abs(self.end.y() - self.begin.y())
-
- # Draw the clear rectangle
- painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
- painter.fillRect(x, y, w, h, Qt.GlobalColor.transparent)
- painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
-
- # Draw border around selection
- pen = QPen(Qt.GlobalColor.white, 2, Qt.PenStyle.SolidLine)
- painter.setPen(pen)
- painter.drawRect(x, y, w, h)
-
- # Draw dimensions text
- painter.setPen(Qt.GlobalColor.white)
- from PyQt6.QtGui import QFont
- font = QFont("Arial", 10)
- painter.setFont(font)
- painter.drawText(x, y - 5, f"{w} x {h}")
-
- def mousePressEvent(self, event):
- if event.button() == Qt.MouseButton.LeftButton:
- self.begin = event.pos()
- self.end = event.pos()
- self.drawing = True
- self.update()
-
- def mouseMoveEvent(self, event):
- if self.drawing:
- self.end = event.pos()
- self.update()
-
- def mouseReleaseEvent(self, event):
- if event.button() == Qt.MouseButton.LeftButton and self.drawing:
- self.drawing = False
- self.end = event.pos()
-
- # Calculate selection rectangle
- x = min(self.begin.x(), self.end.x())
- y = min(self.begin.y(), self.end.y())
- w = abs(self.end.x() - self.begin.x())
- h = abs(self.end.y() - self.begin.y())
-
- # Minimum size check
- if w > 50 and h > 50:
- self.area_selected.emit(x, y, w, h)
- else:
- self.cancelled.emit()
-
- self.close()
- elif event.button() == Qt.MouseButton.RightButton:
- # Right click to cancel
- self.cancelled.emit()
- self.close()
-
- def keyPressEvent(self, event):
- if event.key() == Qt.Key.Key_Escape:
- self.cancelled.emit()
- self.close()
-
-
-class SignalHelper(QObject):
- """Helper QObject to hold signals since BasePlugin doesn't inherit from QObject."""
- hotkey_triggered = pyqtSignal()
- update_status_signal = pyqtSignal(str, bool, bool) # message, success, error
- update_session_table_signal = pyqtSignal(object) # skills dict
- update_counters_signal = pyqtSignal()
- enable_scan_button_signal = pyqtSignal(bool)
-
-
-class SkillScannerPlugin(BasePlugin):
- """Scan skills using core OCR and track gains via core Log service."""
-
- name = "Skill Scanner"
- version = "2.1.0"
- author = "ImpulsiveFPS"
- description = "Uses core OCR and Log services"
- hotkey = "ctrl+shift+s"
-
- def initialize(self):
- """Setup skill scanner."""
- # Create signal helper (QObject) for thread-safe UI updates
- self._signals = SignalHelper()
-
- self.data_file = Path("data/skill_tracker.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- # Load saved data
- self.skills_data = {}
- self.skill_gains = []
- self._load_data()
-
- # Multi-page scanning state
- self.current_scan_session = {} # Skills collected in current multi-page scan
- self.pages_scanned = 0
-
- # Scan area selection (x, y, width, height) - None means auto-detect game window
- self.scan_area = None
-
- # Connect signals (using signal helper QObject)
- self._signals.hotkey_triggered.connect(self._scan_page_for_multi)
- self._signals.update_status_signal.connect(self._update_multi_page_status_slot)
- self._signals.update_session_table_signal.connect(self._update_session_table)
- self._signals.update_counters_signal.connect(self._update_counters_slot)
- # Note: enable_scan_button_signal connected in get_ui() after button created
-
- # Subscribe to skill gain events from core Log service
- try:
- from core.plugin_api import get_api
- api = get_api()
-
- # Check if log service available
- log_service = api.services.get('log')
- if log_service:
- print(f"[SkillScanner] Connected to core Log service")
-
- except Exception as e:
- print(f"[SkillScanner] Could not connect to Log service: {e}")
-
- def _load_data(self):
- """Load saved skill data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.skills_data = data.get('skills', {})
- self.skill_gains = data.get('gains', [])
- except:
- pass
-
- def _save_data(self):
- """Save skill data."""
- with open(self.data_file, 'w') as f:
- json.dump({
- 'skills': self.skills_data,
- 'gains': self.skill_gains
- }, f, indent=2)
-
- def get_ui(self):
- """Create skill scanner UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Header
- header = QLabel("Skill Tracker")
- header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
- layout.addWidget(header)
-
- # Info about core services
- info = self._get_service_status()
- info_label = QLabel(info)
- info_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
- layout.addWidget(info_label)
-
- # Splitter
- splitter = QSplitter(Qt.Orientation.Vertical)
-
- # Scan section
- scan_group = QGroupBox("OCR Scan (Core Service)")
- scan_layout = QVBoxLayout(scan_group)
-
- # Area selection row
- area_layout = QHBoxLayout()
-
- self.select_area_btn = QPushButton("๐ Select Area")
- self.select_area_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a9eff;
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- QPushButton:hover {
- background-color: #3a8eef;
- }
- """)
- self.select_area_btn.clicked.connect(self._start_area_selection)
- area_layout.addWidget(self.select_area_btn)
-
- self.area_label = QLabel("Area: Not selected (will scan full game window)")
- self.area_label.setStyleSheet("color: #888; font-size: 11px;")
- area_layout.addWidget(self.area_label)
- area_layout.addStretch()
-
- scan_layout.addLayout(area_layout)
-
- # Buttons row
- buttons_layout = QHBoxLayout()
-
- scan_btn = QPushButton("Scan Skills Window")
- scan_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- scan_btn.clicked.connect(self._scan_skills)
- buttons_layout.addWidget(scan_btn)
-
- reset_btn = QPushButton("Reset Data")
- reset_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff4757;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- reset_btn.clicked.connect(self._reset_data)
- buttons_layout.addWidget(reset_btn)
-
- scan_layout.addLayout(buttons_layout)
-
- self.scan_progress = QLabel("Ready to scan")
- self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);")
- scan_layout.addWidget(self.scan_progress)
-
- self.skills_table = QTableWidget()
- self.skills_table.setColumnCount(3)
- self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
- self.skills_table.horizontalHeader().setStretchLastSection(True)
- scan_layout.addWidget(self.skills_table)
-
- splitter.addWidget(scan_group)
-
- # Multi-Page Scanning section
- multi_page_group = QGroupBox("Multi-Page Scanner")
- multi_page_layout = QVBoxLayout(multi_page_group)
-
- # Mode selection
- mode_layout = QHBoxLayout()
- mode_layout.addWidget(QLabel("Mode:"))
-
- self.scan_mode_combo = QComboBox()
- self.scan_mode_combo.addItems(["Smart Auto + Hotkey Fallback", "Manual Hotkey Only", "Manual Click Only"])
- self.scan_mode_combo.currentIndexChanged.connect(self._on_scan_mode_changed)
- mode_layout.addWidget(self.scan_mode_combo)
- mode_layout.addStretch()
- multi_page_layout.addLayout(mode_layout)
-
- # Instructions
- self.instructions_label = QLabel(
- "๐ค SMART MODE:\n"
- "1. Click 'Select Area' above and drag over your Skills window\n"
- "2. Click 'Start Smart Scan'\n"
- "3. Navigate pages in EU - auto-detect will scan for you\n"
- "4. If auto fails, use hotkey F12 to scan manually\n"
- "5. Click 'Save All' when done"
- )
- self.instructions_label.setStyleSheet("color: #888; font-size: 11px;")
- multi_page_layout.addWidget(self.instructions_label)
-
- # Hotkey info
- self.hotkey_info = QLabel("Hotkey: F12 = Scan Current Page")
- self.hotkey_info.setStyleSheet("color: #4ecdc4; font-weight: bold;")
- multi_page_layout.addWidget(self.hotkey_info)
-
- # Status row
- status_layout = QHBoxLayout()
-
- self.multi_page_status = QLabel("โณ Ready to scan page 1")
- self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;")
- status_layout.addWidget(self.multi_page_status)
-
- self.pages_scanned_label = QLabel("Pages: 0")
- self.pages_scanned_label.setStyleSheet("color: #4ecdc4;")
- status_layout.addWidget(self.pages_scanned_label)
-
- self.total_skills_label = QLabel("Skills: 0")
- self.total_skills_label.setStyleSheet("color: #4ecdc4;")
- status_layout.addWidget(self.total_skills_label)
-
- status_layout.addStretch()
- multi_page_layout.addLayout(status_layout)
-
- # Buttons
- mp_buttons_layout = QHBoxLayout()
-
- self.scan_page_btn = QPushButton("โถ๏ธ Start Smart Scan")
- self.scan_page_btn.setStyleSheet("""
- QPushButton {
- background-color: #4ecdc4;
- color: #141f23;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- font-size: 13px;
- }
- QPushButton:hover {
- background-color: #3dbdb4;
- }
- """)
- self.scan_page_btn.clicked.connect(self._start_smart_scan)
- mp_buttons_layout.addWidget(self.scan_page_btn)
-
- # Connect signal helper for button enabling (now that button exists)
- self._signals.enable_scan_button_signal.connect(self.scan_page_btn.setEnabled)
-
- save_all_btn = QPushButton("๐พ Save All Scanned")
- save_all_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- font-size: 13px;
- }
- """)
- save_all_btn.clicked.connect(self._save_multi_page_scan)
- mp_buttons_layout.addWidget(save_all_btn)
-
- clear_session_btn = QPushButton("๐ Clear Session")
- clear_session_btn.setStyleSheet("""
- QPushButton {
- background-color: #666;
- color: white;
- padding: 12px;
- border: none;
- border-radius: 4px;
- }
- """)
- clear_session_btn.clicked.connect(self._clear_multi_page_session)
- mp_buttons_layout.addWidget(clear_session_btn)
-
- multi_page_layout.addLayout(mp_buttons_layout)
-
- # Current session table
- self.session_table = QTableWidget()
- self.session_table.setColumnCount(3)
- self.session_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
- self.session_table.horizontalHeader().setStretchLastSection(True)
- self.session_table.setMaximumHeight(200)
- self.session_table.setStyleSheet("""
- QTableWidget {
- background-color: #0d1117;
- border: 1px solid #333;
- }
- QTableWidget::item {
- padding: 4px;
- color: #c9d1d9;
- }
- """)
- multi_page_layout.addWidget(self.session_table)
-
- splitter.addWidget(multi_page_group)
-
- # Log section
- log_group = QGroupBox("Log Tracking (Core Service)")
- log_layout = QVBoxLayout(log_group)
-
- log_status = QLabel("Skill gains tracked from chat log")
- log_status.setStyleSheet("color: #4ecdc4;")
- log_layout.addWidget(log_status)
-
- self.gains_text = QTextEdit()
- self.gains_text.setReadOnly(True)
- self.gains_text.setMaximumHeight(150)
- self.gains_text.setPlaceholderText("Recent skill gains from core Log service...")
- log_layout.addWidget(self.gains_text)
-
- self.total_gains_label = QLabel(f"Total gains: {len(self.skill_gains)}")
- self.total_gains_label.setStyleSheet("color: rgba(255,255,255,150);")
- log_layout.addWidget(self.total_gains_label)
-
- splitter.addWidget(log_group)
-
- layout.addWidget(splitter)
-
- self._refresh_skills_table()
-
- return widget
-
- def _get_service_status(self) -> str:
- """Get status of core services."""
- try:
- from core.ocr_service import get_ocr_service
- from core.log_reader import get_log_reader
-
- ocr = get_ocr_service()
- log = get_log_reader()
-
- ocr_status = "โ" if ocr.is_available() else "โ"
- log_status = "โ" if log.is_available() else "โ"
-
- return f"Core Services - OCR: {ocr_status} Log: {log_status}"
- except:
- return "Core Services - status unknown"
-
- def _scan_skills(self):
- """Start OCR scan using core service."""
- try:
- from core.ocr_service import get_ocr_service
- ocr_service = get_ocr_service()
-
- if not ocr_service.is_available():
- self.scan_progress.setText("Error: OCR service not available")
- return
-
- # Pass scan_area if user has selected one
- self.scanner = SkillOCRThread(ocr_service, scan_area=self.scan_area)
- self.scanner.scan_complete.connect(self._on_scan_complete)
- self.scanner.scan_error.connect(self._on_scan_error)
- self.scanner.progress_update.connect(self._on_scan_progress)
- self.scanner.start()
-
- except Exception as e:
- self.scan_progress.setText(f"Error: {e}")
-
- def _on_scan_progress(self, message):
- self.scan_progress.setText(message)
-
- def _on_scan_complete(self, skills_data):
- self.skills_data.update(skills_data)
- self._save_data()
- self._refresh_skills_table()
- self.scan_progress.setText(f"Found {len(skills_data)} skills")
-
- def _reset_data(self):
- """Reset all skill data."""
- from PyQt6.QtWidgets import QMessageBox
-
- reply = QMessageBox.question(
- None,
- "Reset Skill Data",
- "Are you sure you want to clear all scanned skill data?\n\nThis cannot be undone.",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
- QMessageBox.StandardButton.No
- )
-
- if reply == QMessageBox.StandardButton.Yes:
- self.skills_data = {}
- self.skill_gains = []
- self._save_data()
- self._refresh_skills_table()
- self.gains_text.clear()
- self.total_gains_label.setText("Total gains: 0")
- self.scan_progress.setText("Data cleared")
-
- def _on_scan_error(self, error):
- self.scan_progress.setText(f"Error: {error}")
- self.scan_progress.setText(f"Error: {error}")
-
- def _refresh_skills_table(self):
- self.skills_table.setRowCount(len(self.skills_data))
- for i, (name, data) in enumerate(sorted(self.skills_data.items())):
- self.skills_table.setItem(i, 0, QTableWidgetItem(name))
- self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
- self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
-
- def _scan_page_for_multi(self):
- """Scan current page and add to multi-page session."""
- from PyQt6.QtCore import QTimer
-
- self.multi_page_status.setText("๐ท Scanning...")
- self.multi_page_status.setStyleSheet("color: #ffd93d;")
- self.scan_page_btn.setEnabled(False)
-
- # Run scan in thread
- from threading import Thread
-
- def do_scan():
- try:
- from core.ocr_service import get_ocr_service
- from PIL import ImageGrab
- import re
- from datetime import datetime
-
- ocr_service = get_ocr_service()
- if not ocr_service.is_available():
- self._signals.update_status_signal.emit("Error: OCR not available", False, True)
- return
-
- # Capture based on scan_area setting
- if self.scan_area:
- x, y, w, h = self.scan_area
- screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
- else:
- screenshot = capture_entropia_region()
-
- if screenshot is None:
- self._signals.update_status_signal.emit("Error: Could not capture screen", False, True)
- return
-
- # OCR
- result = ocr_service.recognize_image(screenshot)
- text = result.get('text', '')
-
- # Parse skills
- skills = self._parse_skills_from_text(text)
-
- # Add to session
- for skill_name, data in skills.items():
- self.current_scan_session[skill_name] = data
-
- self.pages_scanned += 1
-
- # Update UI via signals (thread-safe)
- self._signals.update_session_table_signal.emit(self.current_scan_session)
-
- # Show success with checkmark and beep
- self._signals.update_status_signal.emit(
- f"โ
Page {self.pages_scanned} scanned! {len(skills)} skills found. Click Next Page in game โ",
- True, False
- )
-
- # Play beep sound
- self._play_beep()
-
- except Exception as e:
- self._signals.update_status_signal.emit(f"Error: {str(e)}", False, True)
- finally:
- self._signals.enable_scan_button_signal.emit(True)
-
- thread = Thread(target=do_scan)
- thread.daemon = True
- thread.start()
-
- def _start_area_selection(self):
- """Open snipping tool for user to select scan area."""
- self.select_area_btn.setEnabled(False)
- self.select_area_btn.setText("๐ Selecting...")
-
- # Create and show snipping widget
- self.snipping_widget = SnippingWidget()
- self.snipping_widget.area_selected.connect(self._on_area_selected)
- self.snipping_widget.cancelled.connect(self._on_area_cancelled)
- self.snipping_widget.show()
-
- def _on_area_selected(self, x, y, w, h):
- """Handle area selection from snipping tool."""
- self.scan_area = (x, y, w, h)
- self.area_label.setText(f"Area: {w}x{h} at ({x}, {y})")
- self.area_label.setStyleSheet("color: #4ecdc4; font-size: 11px;")
- self.select_area_btn.setEnabled(True)
- self.select_area_btn.setText("๐ Select Area")
- self._signals.update_status_signal.emit(f"โ
Scan area selected: {w}x{h}", True, False)
-
- def _on_area_cancelled(self):
- """Handle cancelled area selection."""
- self.select_area_btn.setEnabled(True)
- self.select_area_btn.setText("๐ Select Area")
- self._signals.update_status_signal.emit("Area selection cancelled", False, False)
-
- def _parse_skills_from_text(self, text):
- """Parse skills from OCR text."""
- skills = {}
-
- # Ranks in Entropia Universe - multi-word first for proper matching
- SINGLE_RANKS = [
- 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
- 'Skilled', 'Expert', 'Professional', 'Master',
- 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
- 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
- ]
- MULTI_RANKS = ['Arch Master', 'Grand Master']
- ALL_RANKS = MULTI_RANKS + SINGLE_RANKS
- rank_pattern = '|'.join(ALL_RANKS)
-
- # Clean text
- text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
- text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
-
- # Remove category names
- for category in ['Attributes', 'COMBAT', 'Combat', 'Design', 'Construction',
- 'Defense', 'General', 'Handgun', 'Heavy Melee Weapons',
- 'Heavy Weapons', 'Information', 'Inflict Melee Damage',
- 'Inflict Ranged Damage', 'Light Melee Weapons', 'Longblades',
- 'Medical', 'Mining', 'Science', 'Social', 'Beauty', 'Mindforce']:
- text = text.replace(category, ' ')
-
- text = ' '.join(text.split())
-
- # Find all skills
- import re
- for match in re.finditer(
- rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)',
- text, re.IGNORECASE
- ):
- skill_name = match.group(1).strip()
- rank = match.group(2)
- points = int(match.group(3))
-
- skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
- skill_name = skill_name.strip()
-
- # Validate skill name - filter out UI text
- if not is_valid_skill_text(skill_name):
- print(f"[SkillScanner] Filtered invalid skill name: '{skill_name}'")
- continue
-
- if points > 0 and skill_name and len(skill_name) > 2:
- skills[skill_name] = {'rank': rank, 'points': points}
-
- return skills
-
- def _update_multi_page_status_slot(self, message, success=False, error=False):
- """Slot for updating multi-page status (called via signal)."""
- color = "#4ecdc4" if success else "#ff4757" if error else "#ff8c42"
-
- self.multi_page_status.setText(message)
- self.multi_page_status.setStyleSheet(f"color: {color}; font-size: 14px;")
- self._update_counters_slot()
-
- def _update_counters_slot(self):
- """Slot for updating counters (called via signal)."""
- self.pages_scanned_label.setText(f"Pages: {self.pages_scanned}")
- self.total_skills_label.setText(f"Skills: {len(self.current_scan_session)}")
-
- def _play_beep(self):
- """Play a beep sound to notify user."""
- try:
- import winsound
- winsound.MessageBeep(winsound.MB_OK)
- except:
- # Fallback - try to use system beep
- try:
- print('\a') # ASCII bell character
- except:
- pass
-
- def _update_session_table(self, skills):
- """Update the session table with current scan data."""
- self.session_table.setRowCount(len(skills))
- for i, (name, data) in enumerate(sorted(skills.items())):
- self.session_table.setItem(i, 0, QTableWidgetItem(name))
- self.session_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
- self.session_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
-
- def _save_multi_page_scan(self):
- """Save all scanned skills from multi-page session."""
- if not self.current_scan_session:
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.information(None, "No Data", "No skills scanned yet. Scan some pages first!")
- return
-
- # Merge with existing data
- self.skills_data.update(self.current_scan_session)
- self._save_data()
- self._refresh_skills_table()
-
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.information(
- None,
- "Scan Complete",
- f"Saved {len(self.current_scan_session)} skills from {self.pages_scanned} pages!"
- )
-
- # Clear session after saving
- self._clear_multi_page_session()
-
- def _clear_multi_page_session(self):
- """Clear the current multi-page scanning session."""
- self.current_scan_session = {}
- self.pages_scanned = 0
- self.auto_scan_active = False
- self.session_table.setRowCount(0)
- self.multi_page_status.setText("โณ Ready to scan page 1")
- self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;")
- self.pages_scanned_label.setText("Pages: 0")
- self.total_skills_label.setText("Skills: 0")
-
- # Unregister hotkey if active
- self._unregister_hotkey()
-
- def _on_scan_mode_changed(self, index):
- """Handle scan mode change."""
- modes = [
- "๐ค SMART MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Click 'Start Smart Scan'\n3. Navigate pages - auto-detect will scan\n4. If auto fails, press F12\n5. Click 'Save All' when done",
- "โจ๏ธ HOTKEY MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Navigate to page 1 in EU\n3. Press F12 to scan each page\n4. Click Next Page in EU\n5. Repeat F12 for each page",
- "๐ฑ๏ธ MANUAL MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat"
- ]
- self.instructions_label.setText(modes[index])
-
- def _start_smart_scan(self):
- """Start smart auto-scan with hotkey fallback."""
- mode = self.scan_mode_combo.currentIndex()
-
- if mode == 0: # Smart Auto + Hotkey
- self._start_auto_scan_with_hotkey()
- elif mode == 1: # Hotkey only
- self._register_hotkey()
- self._signals.update_status_signal.emit("Hotkey F12 ready! Navigate to first page and press F12", True, False)
- else: # Manual click
- self._scan_page_for_multi()
-
- def _start_auto_scan_with_hotkey(self):
- """Start auto-detection with fallback to hotkey."""
- self.auto_scan_active = True
- self.auto_scan_failures = 0
- self.last_page_number = None
-
- # Register F12 hotkey as fallback
- self._register_hotkey()
-
- # Start monitoring
- self._signals.update_status_signal.emit("๐ค Auto-detect started! Navigate to page 1...", True, False)
-
- # Start auto-detection timer
- self.auto_scan_timer = QTimer()
- self.auto_scan_timer.timeout.connect(self._check_for_page_change)
- self.auto_scan_timer.start(500) # Check every 500ms
-
- def _register_hotkey(self):
- """Register F12 hotkey for manual scan."""
- try:
- import keyboard
- keyboard.on_press_key('f12', lambda e: self._hotkey_scan())
- self.hotkey_registered = True
- except Exception as e:
- print(f"[SkillScanner] Could not register hotkey: {e}")
- self.hotkey_registered = False
-
- def _unregister_hotkey(self):
- """Unregister hotkey."""
- try:
- if hasattr(self, 'hotkey_registered') and self.hotkey_registered:
- import keyboard
- keyboard.unhook_all()
- self.hotkey_registered = False
- except:
- pass
-
- # Stop auto-scan timer
- if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer:
- self.auto_scan_timer.stop()
- self.auto_scan_active = False
-
- def _hotkey_scan(self):
- """Scan triggered by F12 hotkey - thread safe via signal."""
- # Emit signal to safely call from hotkey thread
- self._signals.hotkey_triggered.emit()
-
- def _check_for_page_change(self):
- """Auto-detect page changes by monitoring page number area."""
- if not self.auto_scan_active:
- return
-
- try:
- from PIL import ImageGrab
- import pytesseract
-
- # Capture page number area (bottom center of skills window)
- # This is approximate - may need adjustment
- screen = ImageGrab.grab()
- width, height = screen.size
-
- # Try to capture the page number area (bottom center, small region)
- # EU skills window shows page like "1/12" at bottom
- page_area = (width // 2 - 50, height - 100, width // 2 + 50, height - 50)
- page_img = ImageGrab.grab(bbox=page_area)
-
- # OCR just the page number
- page_text = pytesseract.image_to_string(page_img, config='--psm 7 -c tessedit_char_whitelist=0123456789/')
-
- # Extract current page number
- import re
- match = re.search(r'(\d+)/(\d+)', page_text)
- if match:
- current_page = int(match.group(1))
- total_pages = int(match.group(2))
-
- # If page changed, trigger scan
- if self.last_page_number is not None and current_page != self.last_page_number:
- self._signals.update_status_signal.emit(f"๐ Page change detected: {current_page}/{total_pages}", True, False)
- self._scan_page_for_multi()
-
- self.last_page_number = current_page
- else:
- # Failed to detect page number
- self.auto_scan_failures += 1
- if self.auto_scan_failures >= 10: # After 5 seconds of failures
- self._fallback_to_hotkey()
-
- except Exception as e:
- self.auto_scan_failures += 1
- if self.auto_scan_failures >= 10:
- self._fallback_to_hotkey()
-
- def _fallback_to_hotkey(self):
- """Fallback to hotkey mode when auto-detection fails."""
- if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer:
- self.auto_scan_timer.stop()
-
- self.auto_scan_active = False
-
- # Keep hotkey registered
- self._signals.update_status_signal.emit(
- "โ ๏ธ Auto-detect unreliable. Use F12 hotkey to scan each page manually!",
- False, True
- )
-
- # Play alert sound
- self._play_beep()
diff --git a/plugins/spotify_controller/__init__.py b/plugins/spotify_controller/__init__.py
deleted file mode 100644
index d283b86..0000000
--- a/plugins/spotify_controller/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Spotify Controller Plugin for EU-Utility
-"""
-
-from .plugin import SpotifyControllerPlugin
-
-__all__ = ["SpotifyControllerPlugin"]
diff --git a/plugins/spotify_controller/plugin.py b/plugins/spotify_controller/plugin.py
deleted file mode 100644
index d935400..0000000
--- a/plugins/spotify_controller/plugin.py
+++ /dev/null
@@ -1,473 +0,0 @@
-"""
-EU-Utility - Spotify Controller Plugin
-
-Control Spotify playback and display current track info.
-"""
-
-import subprocess
-import platform
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout,
- QLabel, QPushButton, QSlider, QProgressBar
-)
-from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
-
-from plugins.base_plugin import BasePlugin
-
-
-class SpotifyInfoThread(QThread):
- """Background thread to fetch Spotify info."""
- info_ready = pyqtSignal(dict)
- error = pyqtSignal(str)
-
- def __init__(self, system):
- super().__init__()
- self.system = system
-
- def run(self):
- """Fetch Spotify info."""
- try:
- if self.system == "Linux":
- result = subprocess.run(
- ['playerctl', '--player=spotify', 'metadata', '--format',
- '{{title}}|{{artist}}|{{album}}|{{position}}|{{mpris:length}}|{{status}}'],
- capture_output=True, text=True, timeout=5
- )
- if result.returncode == 0:
- parts = result.stdout.strip().split('|')
- if len(parts) >= 6:
- self.info_ready.emit({
- 'title': parts[0] or 'Unknown',
- 'artist': parts[1] or 'Unknown Artist',
- 'album': parts[2] or '',
- 'position': self._parse_time(parts[3]),
- 'duration': self._parse_time(parts[4]),
- 'is_playing': parts[5] == 'Playing'
- })
- return
-
- elif self.system == "Darwin":
- script = '''
- tell application "Spotify"
- if player state is playing then
- return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Playing"
- else
- return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Paused"
- end if
- end tell
- '''
- result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5)
- if result.returncode == 0:
- parts = result.stdout.strip().split('|')
- if len(parts) >= 6:
- self.info_ready.emit({
- 'title': parts[0] or 'Unknown',
- 'artist': parts[1] or 'Unknown Artist',
- 'album': parts[2] or '',
- 'position': float(parts[3]) if parts[3] else 0,
- 'duration': float(parts[4]) if parts[4] else 0,
- 'is_playing': parts[5] == 'Playing'
- })
- return
-
- # Default/empty response
- self.info_ready.emit({
- 'title': 'Not playing',
- 'artist': '',
- 'album': '',
- 'position': 0,
- 'duration': 0,
- 'is_playing': False
- })
-
- except Exception as e:
- self.error.emit(str(e))
- self.info_ready.emit({
- 'title': 'Not playing',
- 'artist': '',
- 'album': '',
- 'position': 0,
- 'duration': 0,
- 'is_playing': False
- })
-
- def _parse_time(self, time_str):
- """Parse time string to seconds."""
- try:
- return int(time_str) / 1000000
- except:
- return 0
-
-
-class SpotifyControllerPlugin(BasePlugin):
- """Control Spotify playback and display current track."""
-
- name = "Spotify"
- version = "1.1.0"
- author = "ImpulsiveFPS"
- description = "Control Spotify and view current track info"
- hotkey = "ctrl+shift+m"
-
- def initialize(self):
- """Setup Spotify controller."""
- self.system = platform.system()
- self.update_timer = None
- self.info_thread = None
- self.current_info = {
- 'title': 'Not playing',
- 'artist': '',
- 'album': '',
- 'position': 0,
- 'duration': 0,
- 'is_playing': False
- }
-
- def get_ui(self):
- """Create Spotify controller UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(12)
-
- # Title
- title = QLabel("Spotify")
- title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;")
- layout.addWidget(title)
-
- # Album Art Placeholder
- self.album_art = QLabel("๐ฟ")
- self.album_art.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.album_art.setStyleSheet("""
- QLabel {
- background-color: #282828;
- border-radius: 8px;
- font-size: 64px;
- padding: 20px;
- min-height: 120px;
- }
- """)
- layout.addWidget(self.album_art)
-
- # Track info container
- info_container = QWidget()
- info_container.setStyleSheet("""
- QWidget {
- background-color: #1a1a1a;
- border-radius: 8px;
- padding: 12px;
- }
- """)
- info_layout = QVBoxLayout(info_container)
- info_layout.setSpacing(4)
-
- # Track title
- self.track_label = QLabel("Not playing")
- self.track_label.setStyleSheet("""
- color: white;
- font-size: 16px;
- font-weight: bold;
- """)
- self.track_label.setWordWrap(True)
- info_layout.addWidget(self.track_label)
-
- # Artist
- self.artist_label = QLabel("")
- self.artist_label.setStyleSheet("""
- color: #b3b3b3;
- font-size: 13px;
- """)
- info_layout.addWidget(self.artist_label)
-
- # Album
- self.album_label = QLabel("")
- self.album_label.setStyleSheet("""
- color: #666;
- font-size: 11px;
- """)
- info_layout.addWidget(self.album_label)
-
- layout.addWidget(info_container)
-
- # Time info
- time_layout = QHBoxLayout()
- self.position_label = QLabel("0:00")
- self.position_label.setStyleSheet("color: #888; font-size: 11px;")
- time_layout.addWidget(self.position_label)
-
- time_layout.addStretch()
-
- self.duration_label = QLabel("0:00")
- self.duration_label.setStyleSheet("color: #888; font-size: 11px;")
- time_layout.addWidget(self.duration_label)
-
- layout.addLayout(time_layout)
-
- # Progress bar
- self.progress = QProgressBar()
- self.progress.setRange(0, 100)
- self.progress.setValue(0)
- self.progress.setTextVisible(False)
- self.progress.setStyleSheet("""
- QProgressBar {
- background-color: #404040;
- border: none;
- height: 4px;
- border-radius: 2px;
- }
- QProgressBar::chunk {
- background-color: #1DB954;
- border-radius: 2px;
- }
- """)
- layout.addWidget(self.progress)
-
- # Control buttons
- btn_layout = QHBoxLayout()
- btn_layout.setSpacing(15)
- btn_layout.addStretch()
-
- # Previous
- prev_btn = QPushButton("โฎ")
- prev_btn.setFixedSize(50, 50)
- prev_btn.setStyleSheet(self._get_button_style("#404040"))
- prev_btn.clicked.connect(self._previous_track)
- btn_layout.addWidget(prev_btn)
-
- # Play/Pause
- self.play_btn = QPushButton("โถ")
- self.play_btn.setFixedSize(60, 60)
- self.play_btn.setStyleSheet(self._get_play_button_style())
- self.play_btn.clicked.connect(self._toggle_playback)
- btn_layout.addWidget(self.play_btn)
-
- # Next
- next_btn = QPushButton("โญ")
- next_btn.setFixedSize(50, 50)
- next_btn.setStyleSheet(self._get_button_style("#404040"))
- next_btn.clicked.connect(self._next_track)
- btn_layout.addWidget(next_btn)
-
- btn_layout.addStretch()
- layout.addLayout(btn_layout)
-
- # Volume
- volume_layout = QHBoxLayout()
- volume_layout.addWidget(QLabel("๐"))
-
- self.volume_slider = QSlider(Qt.Orientation.Horizontal)
- self.volume_slider.setRange(0, 100)
- self.volume_slider.setValue(50)
- self.volume_slider.setStyleSheet("""
- QSlider::groove:horizontal {
- background: #404040;
- height: 4px;
- border-radius: 2px;
- }
- QSlider::handle:horizontal {
- background: #fff;
- width: 12px;
- margin: -4px 0;
- border-radius: 6px;
- }
- QSlider::sub-page:horizontal {
- background: #1DB954;
- border-radius: 2px;
- }
- """)
- self.volume_slider.valueChanged.connect(self._set_volume)
- volume_layout.addWidget(self.volume_slider)
-
- volume_layout.addWidget(QLabel("๐"))
- layout.addLayout(volume_layout)
-
- # Status
- self.status_label = QLabel("Click play to control Spotify")
- self.status_label.setStyleSheet("color: #666; font-size: 10px;")
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- layout.addWidget(self.status_label)
-
- layout.addStretch()
-
- # Start update timer
- self._start_timer()
-
- return widget
-
- def _get_button_style(self, color):
- """Get button stylesheet."""
- return f"""
- QPushButton {{
- background-color: {color};
- color: white;
- font-size: 18px;
- border: none;
- border-radius: 25px;
- }}
- QPushButton:hover {{
- background-color: #505050;
- }}
- QPushButton:pressed {{
- background-color: #303030;
- }}
- """
-
- def _get_play_button_style(self):
- """Get play button style (green)."""
- return """
- QPushButton {
- background-color: #1DB954;
- color: white;
- font-size: 22px;
- border: none;
- border-radius: 30px;
- }
- QPushButton:hover {
- background-color: #1ed760;
- }
- QPushButton:pressed {
- background-color: #1aa34a;
- }
- """
-
- def _start_timer(self):
- """Start status update timer."""
- self.update_timer = QTimer()
- self.update_timer.timeout.connect(self._fetch_spotify_info)
- self.update_timer.start(1000)
-
- def _fetch_spotify_info(self):
- """Fetch Spotify info in background."""
- if self.info_thread and self.info_thread.isRunning():
- return
-
- self.info_thread = SpotifyInfoThread(self.system)
- self.info_thread.info_ready.connect(self._update_ui)
- self.info_thread.start()
-
- def _update_ui(self, info):
- """Update UI with Spotify info."""
- self.current_info = info
-
- # Update track info
- self.track_label.setText(info.get('title', 'Unknown'))
- self.artist_label.setText(info.get('artist', ''))
- self.album_label.setText(info.get('album', ''))
-
- # Update play button
- is_playing = info.get('is_playing', False)
- self.play_btn.setText("โธ" if is_playing else "โถ")
-
- # Update time
- position = info.get('position', 0)
- duration = info.get('duration', 0)
-
- self.position_label.setText(self._format_time(position))
- self.duration_label.setText(self._format_time(duration))
-
- # Update progress bar
- if duration > 0:
- progress = int((position / duration) * 100)
- self.progress.setValue(progress)
- else:
- self.progress.setValue(0)
-
- def _format_time(self, seconds):
- """Format seconds to mm:ss."""
- try:
- minutes = int(seconds) // 60
- secs = int(seconds) % 60
- return f"{minutes}:{secs:02d}"
- except:
- return "0:00"
-
- def _send_media_key(self, key):
- """Send media key press to system."""
- try:
- if self.system == "Windows":
- import ctypes
- key_codes = {
- 'play': 0xB3,
- 'next': 0xB0,
- 'prev': 0xB1,
- }
- if key in key_codes:
- ctypes.windll.user32.keybd_event(key_codes[key], 0, 0, 0)
- ctypes.windll.user32.keybd_event(key_codes[key], 0, 2, 0)
- return True
-
- elif self.system == "Linux":
- cmd_map = {
- 'play': ['playerctl', '--player=spotify', 'play-pause'],
- 'next': ['playerctl', '--player=spotify', 'next'],
- 'prev': ['playerctl', '--player=spotify', 'previous'],
- }
- if key in cmd_map:
- subprocess.run(cmd_map[key], capture_output=True)
- return True
-
- elif self.system == "Darwin":
- cmd_map = {
- 'play': ['osascript', '-e', 'tell application "Spotify" to playpause'],
- 'next': ['osascript', '-e', 'tell application "Spotify" to next track'],
- 'prev': ['osascript', '-e', 'tell application "Spotify" to previous track'],
- }
- if key in cmd_map:
- subprocess.run(cmd_map[key], capture_output=True)
- return True
-
- except Exception as e:
- print(f"Error sending media key: {e}")
-
- return False
-
- def _toggle_playback(self):
- """Toggle play/pause."""
- if self._send_media_key('play'):
- self.current_info['is_playing'] = not self.current_info.get('is_playing', False)
- self.play_btn.setText("โธ" if self.current_info['is_playing'] else "โถ")
- self.status_label.setText("Command sent to Spotify")
- else:
- self.status_label.setText("โ Could not control Spotify")
-
- def _next_track(self):
- """Next track."""
- if self._send_media_key('next'):
- self.status_label.setText("โญ Next track")
- else:
- self.status_label.setText("โ Could not skip")
-
- def _previous_track(self):
- """Previous track."""
- if self._send_media_key('prev'):
- self.status_label.setText("โฎ Previous track")
- else:
- self.status_label.setText("โ Could not go back")
-
- def _set_volume(self, value):
- """Set volume (0-100)."""
- try:
- if self.system == "Linux":
- subprocess.run(['playerctl', '--player=spotify', 'volume', str(value / 100)], capture_output=True)
- except:
- pass
-
- def on_hotkey(self):
- """Toggle play/pause with hotkey."""
- self._toggle_playback()
-
- def on_hide(self):
- """Stop timer when overlay hidden."""
- if self.update_timer:
- self.update_timer.stop()
-
- def on_show(self):
- """Restart timer when overlay shown."""
- if self.update_timer:
- self.update_timer.start()
- self._fetch_spotify_info()
-
- def shutdown(self):
- """Cleanup."""
- if self.update_timer:
- self.update_timer.stop()
- if self.info_thread and self.info_thread.isRunning():
- self.info_thread.wait()
diff --git a/plugins/tp_runner/__init__.py b/plugins/tp_runner/__init__.py
deleted file mode 100644
index 25d81e4..0000000
--- a/plugins/tp_runner/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-TP Runner Plugin
-"""
-
-from .plugin import TPRunnerPlugin
-
-__all__ = ["TPRunnerPlugin"]
diff --git a/plugins/tp_runner/plugin.py b/plugins/tp_runner/plugin.py
deleted file mode 100644
index 1df3333..0000000
--- a/plugins/tp_runner/plugin.py
+++ /dev/null
@@ -1,219 +0,0 @@
-"""
-EU-Utility - TP Runner Plugin
-
-Track teleporter locations and plan routes.
-"""
-
-import json
-from pathlib import Path
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QComboBox, QTreeWidget, QTreeWidgetItem,
- QLineEdit, QFrame
-)
-from PyQt6.QtCore import Qt
-
-from plugins.base_plugin import BasePlugin
-
-
-class TPRunnerPlugin(BasePlugin):
- """Track TP locations and plan routes."""
-
- name = "TP Runner"
- version = "1.0.0"
- author = "ImpulsiveFPS"
- description = "Teleporter locations and route planner"
- hotkey = "ctrl+shift+p" # P for Port
-
- # Arkadia TPs
- ARKADIA_TPS = [
- "Arkadia City", "Arkadia City Outskirts", "8 Coins Creek",
- "Celeste Harbour", "Celeste Outpost North", "Celeste Outpost South",
- "Celeste Quarry", "Cycadia", "Dauntless Dock", "East Scythe",
- "Genesis", "Hadesheim", "Hadesheim Ashland", "Hadesheim Pass",
- "Hadesheim Valley", "Hellfire Hills", "Hero's Landing",
- "Horror-Filled Hallows", "Jagged Coast", "Jungle Camp",
- "Khorum Coast", "Khorum Highway", "Kronus", "Lava Camp",
- "Lighthouse", "Living Graveyard", "Mountaintop", "Neo-Shanghai",
- "North Scythe", "Nusul Fields", "Oily Business", "Perseus",
- "Pilgrim's Landing", "Poseidon West", "Red Sands",
- "Releks Hills", "Rest Stop", "Ripper Snapper", "Sacred Cove",
- "Sentinel Hill", "Shady Ridge", "Sisyphus", "South Scythe",
- "Spiral Mountain", "Stormbird Landing", "Sundari", "Tides",
- "Traveller's Landing", "Victorious", "Vikings", "West Scythe",
- "Wild Banks", "Wolf's Ridge",
- ]
-
- def initialize(self):
- """Setup TP runner."""
- self.data_file = Path("data/tp_runner.json")
- self.data_file.parent.mkdir(parents=True, exist_ok=True)
-
- self.unlocked_tps = set()
- self.favorite_routes = []
-
- self._load_data()
-
- def _load_data(self):
- """Load TP data."""
- if self.data_file.exists():
- try:
- with open(self.data_file, 'r') as f:
- data = json.load(f)
- self.unlocked_tps = set(data.get('unlocked', []))
- self.favorite_routes = data.get('routes', [])
- except:
- pass
-
- def _save_data(self):
- """Save TP data."""
- with open(self.data_file, 'w') as f:
- json.dump({
- 'unlocked': list(self.unlocked_tps),
- 'routes': self.favorite_routes
- }, f, indent=2)
-
- def get_ui(self):
- """Create TP runner UI."""
- widget = QWidget()
- widget.setStyleSheet("background: transparent;")
- layout = QVBoxLayout(widget)
- layout.setSpacing(15)
- layout.setContentsMargins(0, 0, 0, 0)
-
- # Title
- title = QLabel("๐ TP Runner")
- title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
- layout.addWidget(title)
-
- # Progress
- unlocked = len(self.unlocked_tps)
- total = len(self.ARKADIA_TPS)
- progress = QLabel(f"Unlocked: {unlocked}/{total} ({unlocked/total*100:.1f}%)")
- progress.setStyleSheet("color: #4caf50;")
- layout.addWidget(progress)
-
- # Route planner
- planner = QFrame()
- planner.setStyleSheet("""
- QFrame {
- background-color: rgba(30, 35, 45, 200);
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- """)
- planner_layout = QVBoxLayout(planner)
-
- # From/To
- fromto = QHBoxLayout()
-
- self.from_combo = QComboBox()
- self.from_combo.addItems(self.ARKADIA_TPS)
- self.from_combo.setStyleSheet("""
- QComboBox {
- background-color: rgba(20, 25, 35, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- padding: 5px;
- }
- """)
- fromto.addWidget(QLabel("From:"))
- fromto.addWidget(self.from_combo)
-
- self.to_combo = QComboBox()
- self.to_combo.addItems(self.ARKADIA_TPS)
- self.to_combo.setStyleSheet(self.from_combo.styleSheet())
- fromto.addWidget(QLabel("To:"))
- fromto.addWidget(self.to_combo)
-
- planner_layout.addLayout(fromto)
-
- # Route button
- route_btn = QPushButton("Find Route")
- route_btn.setStyleSheet("""
- QPushButton {
- background-color: #ff8c42;
- color: white;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- route_btn.clicked.connect(self._find_route)
- planner_layout.addWidget(route_btn)
-
- layout.addWidget(planner)
-
- # TP List
- self.tp_tree = QTreeWidget()
- self.tp_tree.setHeaderLabels(["Teleporter", "Status"])
- self.tp_tree.setStyleSheet("""
- QTreeWidget {
- background-color: rgba(30, 35, 45, 200);
- color: white;
- border: 1px solid rgba(100, 110, 130, 80);
- border-radius: 6px;
- }
- QHeaderView::section {
- background-color: rgba(35, 40, 55, 200);
- color: rgba(255,255,255,180);
- padding: 8px;
- font-weight: bold;
- font-size: 11px;
- }
- """)
-
- # Populate list
- for tp in sorted(self.ARKADIA_TPS):
- item = QTreeWidgetItem()
- item.setText(0, tp)
- if tp in self.unlocked_tps:
- item.setText(1, "Unlocked")
- item.setForeground(1, Qt.GlobalColor.green)
- else:
- item.setText(1, "Locked")
- item.setForeground(1, Qt.GlobalColor.gray)
- self.tp_tree.addTopLevelItem(item)
-
- layout.addWidget(self.tp_tree)
-
- # Mark as unlocked button
- unlock_btn = QPushButton("Mark Unlocked")
- unlock_btn.setStyleSheet("""
- QPushButton {
- background-color: #4caf50;
- color: white;
- padding: 10px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- }
- """)
- unlock_btn.clicked.connect(self._mark_unlocked)
- layout.addWidget(unlock_btn)
-
- layout.addStretch()
- return widget
-
- def _find_route(self):
- """Find route between TPs."""
- from_tp = self.from_combo.currentText()
- to_tp = self.to_combo.currentText()
-
- if from_tp == to_tp:
- return
-
- # Simple distance estimation (would use actual coordinates)
- # For now, just show direct route
-
- def _mark_unlocked(self):
- """Mark selected TP as unlocked."""
- item = self.tp_tree.currentItem()
- if item:
- tp_name = item.text(0)
- self.unlocked_tps.add(tp_name)
- item.setText(1, "Unlocked")
- item.setForeground(1, Qt.GlobalColor.green)
- self._save_data()
diff --git a/plugins/universal_search/__init__.py b/plugins/universal_search/__init__.py
deleted file mode 100644
index 44d5b53..0000000
--- a/plugins/universal_search/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Universal Search Plugin for EU-Utility
-"""
-
-from .plugin import UniversalSearchPlugin
-
-__all__ = ["UniversalSearchPlugin"]
diff --git a/plugins/universal_search/plugin.py b/plugins/universal_search/plugin.py
deleted file mode 100644
index 31f0f0c..0000000
--- a/plugins/universal_search/plugin.py
+++ /dev/null
@@ -1,600 +0,0 @@
-"""
-EU-Utility - Universal Search Plugin
-
-Search across all Entropia Nexus entities - items, mobs, locations, blueprints, skills, etc.
-"""
-
-import json
-import webbrowser
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout,
- QLineEdit, QPushButton, QLabel, QComboBox,
- QTableWidget, QTableWidgetItem, QHeaderView,
- QTabWidget, QStackedWidget, QFrame
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal
-
-from plugins.base_plugin import BasePlugin
-
-
-class NexusEntityAPI:
- """Client for Entropia Nexus Entity API."""
-
- BASE_URL = "https://api.entropianexus.com"
-
- # Entity type to API endpoint mapping
- ENDPOINTS = {
- "Items": "/items",
- "Weapons": "/weapons",
- "Armors": "/armors",
- "Blueprints": "/blueprints",
- "Mobs": "/mobs",
- "Locations": "/locations",
- "Skills": "/skills",
- "Materials": "/materials",
- "Enhancers": "/enhancers",
- "Medical Tools": "/medicaltools",
- "Finders": "/finders",
- "Excavators": "/excavators",
- "Refiners": "/refiners",
- "Vehicles": "/vehicles",
- "Pets": "/pets",
- "Decorations": "/decorations",
- "Furniture": "/furniture",
- "Storage": "/storagecontainers",
- "Strongboxes": "/strongboxes",
- "Teleporters": "/teleporters",
- "Shops": "/shops",
- "Vendors": "/vendors",
- "Planets": "/planets",
- "Areas": "/areas",
- }
-
- @classmethod
- def search_entities(cls, entity_type, query, limit=50, http_get_func=None):
- """Search for entities of a specific type."""
- try:
- endpoint = cls.ENDPOINTS.get(entity_type, "/items")
-
- # Build URL with query params
- params = {'q': query, 'limit': limit, 'fuzzy': 'true'}
- query_string = '&'.join(f"{k}={v}" for k, v in params.items())
- url = f"{cls.BASE_URL}{endpoint}?{query_string}"
-
- if http_get_func:
- response = http_get_func(
- url,
- cache_ttl=300, # 5 minute cache
- headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
- )
- else:
- # Fallback for standalone usage
- import urllib.request
- req = urllib.request.Request(
- url,
- headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
- )
- with urllib.request.urlopen(req, timeout=15) as resp:
- response = {'json': json.loads(resp.read().decode('utf-8'))}
-
- data = response.get('json') if response else None
- return data if isinstance(data, list) else []
-
- except Exception as e:
- print(f"API Error ({entity_type}): {e}")
- return []
-
- @classmethod
- def universal_search(cls, query, limit=30, http_get_func=None):
- """Universal search across all entity types."""
- try:
- params = {'query': query, 'limit': limit, 'fuzzy': 'true'}
- query_string = '&'.join(f"{k}={v}" for k, v in params.items())
- url = f"{cls.BASE_URL}/search?{query_string}"
-
- if http_get_func:
- response = http_get_func(
- url,
- cache_ttl=300, # 5 minute cache
- headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
- )
- else:
- # Fallback for standalone usage
- import urllib.request
- req = urllib.request.Request(
- url,
- headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'}
- )
- with urllib.request.urlopen(req, timeout=15) as resp:
- response = {'json': json.loads(resp.read().decode('utf-8'))}
-
- data = response.get('json') if response else None
- return data if isinstance(data, list) else []
-
- except Exception as e:
- print(f"Universal Search Error: {e}")
- return []
-
- @classmethod
- def get_entity_url(cls, entity_type, entity_id_or_name):
- """Get the web URL for an entity."""
- web_base = "https://www.entropianexus.com"
-
- # Map to web paths
- web_paths = {
- "Items": "items",
- "Weapons": "items",
- "Armors": "items",
- "Blueprints": "blueprints",
- "Mobs": "mobs",
- "Locations": "locations",
- "Skills": "skills",
- "Materials": "items",
- "Enhancers": "items",
- "Medical Tools": "items",
- "Finders": "items",
- "Excavators": "items",
- "Refiners": "items",
- "Vehicles": "items",
- "Pets": "items",
- "Decorations": "items",
- "Furniture": "items",
- "Storage": "items",
- "Strongboxes": "items",
- "Teleporters": "locations",
- "Shops": "locations",
- "Vendors": "locations",
- "Planets": "locations",
- "Areas": "locations",
- }
-
- path = web_paths.get(entity_type, "items")
- return f"{web_base}/{path}/{entity_id_or_name}"
-
-
-class UniversalSearchThread(QThread):
- """Background thread for API searches."""
- results_ready = pyqtSignal(list, str)
- error_occurred = pyqtSignal(str)
-
- def __init__(self, query, entity_type, universal=False, http_get_func=None):
- super().__init__()
- self.query = query
- self.entity_type = entity_type
- self.universal = universal
- self.http_get_func = http_get_func
-
- def run(self):
- """Perform API search."""
- try:
- if self.universal:
- results = NexusEntityAPI.universal_search(self.query, http_get_func=self.http_get_func)
- else:
- results = NexusEntityAPI.search_entities(self.entity_type, self.query, http_get_func=self.http_get_func)
-
- self.results_ready.emit(results, self.entity_type)
-
- except Exception as e:
- self.error_occurred.emit(str(e))
-
-
-class UniversalSearchPlugin(BasePlugin):
- """Universal search across all Nexus entities."""
-
- name = "Universal Search"
- version = "2.0.0"
- author = "ImpulsiveFPS"
- description = "Search items, mobs, locations, blueprints, skills, and more"
- hotkey = "ctrl+shift+f" # F for Find
-
- def initialize(self):
- """Setup the plugin."""
- self.search_thread = None
- self.current_results = []
- self.current_entity_type = "Universal"
-
- def get_ui(self):
- """Create plugin UI."""
- widget = QWidget()
- layout = QVBoxLayout(widget)
- layout.setSpacing(10)
-
- # Title - NO EMOJI
- title = QLabel("Universal Search")
- title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
- layout.addWidget(title)
-
- # Search mode selector
- mode_layout = QHBoxLayout()
- mode_layout.addWidget(QLabel("Mode:"))
-
- self.search_mode = QComboBox()
- self.search_mode.addItem("Universal (All Types)", "Universal")
- self.search_mode.addItem("โโโโโโโโโโโโโโโโโโ", "separator")
-
- # Add all entity types
- entity_types = [
- "Items",
- "Weapons",
- "Armors",
- "Blueprints",
- "Mobs",
- "Locations",
- "Skills",
- "Materials",
- "Enhancers",
- "Medical Tools",
- "Finders",
- "Excavators",
- "Refiners",
- "Vehicles",
- "Pets",
- "Decorations",
- "Furniture",
- "Storage",
- "Strongboxes",
- "Teleporters",
- "Shops",
- "Vendors",
- "Planets",
- "Areas",
- ]
-
- for etype in entity_types:
- self.search_mode.addItem(f" {etype}", etype)
-
- self.search_mode.setStyleSheet("""
- QComboBox {
- background-color: #444;
- color: white;
- padding: 8px;
- border-radius: 4px;
- min-width: 200px;
- }
- QComboBox::drop-down {
- border: none;
- }
- """)
- self.search_mode.currentIndexChanged.connect(self._on_mode_changed)
- mode_layout.addWidget(self.search_mode)
- mode_layout.addStretch()
-
- layout.addLayout(mode_layout)
-
- # Search bar
- search_layout = QHBoxLayout()
-
- self.search_input = QLineEdit()
- self.search_input.setPlaceholderText("Search for anything... (e.g., 'ArMatrix', 'Argonaut', 'Calypso')")
- self.search_input.setStyleSheet("""
- QLineEdit {
- background-color: #333;
- color: white;
- padding: 10px;
- border: 2px solid #555;
- border-radius: 4px;
- font-size: 14px;
- }
- QLineEdit:focus {
- border-color: #4a9eff;
- }
- """)
- self.search_input.returnPressed.connect(self._do_search)
- search_layout.addWidget(self.search_input, 1)
-
- search_btn = QPushButton("Search")
- search_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a9eff;
- color: white;
- padding: 10px 20px;
- border: none;
- border-radius: 4px;
- font-weight: bold;
- font-size: 13px;
- }
- QPushButton:hover {
- background-color: #5aafff;
- }
- """)
- search_btn.clicked.connect(self._do_search)
- search_layout.addWidget(search_btn)
-
- layout.addLayout(search_layout)
-
- # Status
- self.status_label = QLabel("Ready to search")
- self.status_label.setStyleSheet("color: #666; font-size: 11px;")
- layout.addWidget(self.status_label)
-
- # Results table
- self.results_table = QTableWidget()
- self.results_table.setColumnCount(4)
- self.results_table.setHorizontalHeaderLabels(["Name", "Type", "Details", "ID"])
- self.results_table.horizontalHeader().setStretchLastSection(False)
- self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
- self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
- self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
- self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
- self.results_table.setColumnWidth(1, 120)
- self.results_table.setColumnWidth(3, 60)
- self.results_table.verticalHeader().setVisible(False)
- self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
- self.results_table.setStyleSheet("""
- QTableWidget {
- background-color: #2a2a2a;
- color: white;
- border: 1px solid #444;
- border-radius: 4px;
- gridline-color: #333;
- }
- QTableWidget::item {
- padding: 10px;
- border-bottom: 1px solid #333;
- }
- QTableWidget::item:selected {
- background-color: #4a9eff;
- }
- QTableWidget::item:hover {
- background-color: #3a3a3a;
- }
- QHeaderView::section {
- background-color: #333;
- color: #aaa;
- padding: 10px;
- border: none;
- font-weight: bold;
- }
- """)
- self.results_table.cellDoubleClicked.connect(self._on_item_double_clicked)
- self.results_table.setMaximumHeight(350)
- self.results_table.setMinimumHeight(200)
- layout.addWidget(self.results_table)
-
- # Action buttons
- action_layout = QHBoxLayout()
-
- self.open_btn = QPushButton("Open Selected")
- self.open_btn.setEnabled(False)
- self.open_btn.setStyleSheet("""
- QPushButton {
- background-color: #4a9eff;
- color: white;
- padding: 8px 16px;
- border: none;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #5aafff;
- }
- QPushButton:disabled {
- background-color: #444;
- color: #666;
- }
- """)
- self.open_btn.clicked.connect(self._open_selected)
- action_layout.addWidget(self.open_btn)
-
- action_layout.addStretch()
-
- # Quick category buttons
- quick_label = QLabel("Quick:")
- quick_label.setStyleSheet("color: #666;")
- action_layout.addWidget(quick_label)
-
- for category in ["Items", "Mobs", "Blueprints", "Locations"]:
- btn = QPushButton(category)
- btn.setStyleSheet("""
- QPushButton {
- background-color: transparent;
- color: #4a9eff;
- border: 1px solid #4a9eff;
- padding: 5px 10px;
- border-radius: 3px;
- }
- QPushButton:hover {
- background-color: #4a9eff;
- color: white;
- }
- """)
- btn.clicked.connect(lambda checked, c=category: self._quick_search(c))
- action_layout.addWidget(btn)
-
- layout.addLayout(action_layout)
-
- # Tips
- tips = QLabel("Tip: Double-click result to open on Nexus website")
- tips.setStyleSheet("color: #555; font-size: 10px;")
- layout.addWidget(tips)
-
- layout.addStretch()
-
- return widget
-
- def _on_mode_changed(self):
- """Handle search mode change."""
- data = self.search_mode.currentData()
- if data == "separator":
- # Reset to previous valid selection
- self.search_mode.setCurrentIndex(0)
-
- def _do_search(self):
- """Perform search."""
- query = self.search_input.text().strip()
- if len(query) < 2:
- self.status_label.setText("Enter at least 2 characters")
- return
-
- entity_type = self.search_mode.currentData()
- if entity_type == "separator":
- entity_type = "Universal"
-
- self.current_entity_type = entity_type
- universal = (entity_type == "Universal")
-
- # Clear previous results
- self.results_table.setRowCount(0)
- self.current_results = []
- self.open_btn.setEnabled(False)
- self.status_label.setText(f"Searching for '{query}'...")
-
- # Start search thread with http_get function
- self.search_thread = UniversalSearchThread(
- query, entity_type, universal,
- http_get_func=self.http_get
- )
- self.search_thread.results_ready.connect(self._on_results)
- self.search_thread.error_occurred.connect(self._on_error)
- self.search_thread.start()
-
- def _quick_search(self, category):
- """Quick search for a specific category."""
- # Set the category
- index = self.search_mode.findData(category)
- if index >= 0:
- self.search_mode.setCurrentIndex(index)
-
- # If there's text in the search box, search immediately
- if self.search_input.text().strip():
- self._do_search()
- else:
- self.search_input.setFocus()
- self.status_label.setText(f"Selected: {category} - Enter search term")
-
- def _on_results(self, results, entity_type):
- """Handle search results."""
- self.current_results = results
-
- if not results:
- self.status_label.setText("No results found")
- return
-
- # Populate table
- self.results_table.setRowCount(len(results))
-
- for row, item in enumerate(results):
- # Extract data based on available fields
- name = item.get('name', item.get('Name', 'Unknown'))
- item_id = str(item.get('id', item.get('Id', '')))
-
- # Determine type
- if 'type' in item:
- item_type = item['type']
- elif entity_type != "Universal":
- item_type = entity_type
- else:
- # Try to infer from other fields
- item_type = self._infer_type(item)
-
- # Build details string
- details = self._build_details(item, item_type)
-
- # Set table items
- self.results_table.setItem(row, 0, QTableWidgetItem(name))
- self.results_table.setItem(row, 1, QTableWidgetItem(item_type))
- self.results_table.setItem(row, 2, QTableWidgetItem(details))
- self.results_table.setItem(row, 3, QTableWidgetItem(item_id))
-
- self.open_btn.setEnabled(True)
- self.status_label.setText(f"Found {len(results)} results")
-
- def _infer_type(self, item):
- """Infer entity type from item fields."""
- if 'damage' in item or 'range' in item:
- return "Weapon"
- elif 'protection' in item or 'durability' in item:
- return "Armor"
- elif 'hitpoints' in item:
- return "Mob"
- elif 'x' in item and 'y' in item:
- return "Location"
- elif 'qr' in item or 'click' in item:
- return "Blueprint"
- elif 'category' in item:
- return item['category']
- else:
- return "Item"
-
- def _build_details(self, item, item_type):
- """Build details string based on item type."""
- details = []
-
- if item_type in ["Weapon", "Weapons"]:
- if 'damage' in item:
- details.append(f"Dmg: {item['damage']}")
- if 'range' in item:
- details.append(f"Range: {item['range']}m")
- if 'attacks' in item:
- details.append(f"{item['attacks']} attacks")
-
- elif item_type in ["Armor", "Armors"]:
- if 'protection' in item:
- details.append(f"Prot: {item['protection']}")
- if 'durability' in item:
- details.append(f"Dur: {item['durability']}")
-
- elif item_type in ["Mob", "Mobs"]:
- if 'hitpoints' in item:
- details.append(f"HP: {item['hitpoints']}")
- if 'damage' in item:
- details.append(f"Dmg: {item['damage']}")
- if 'threat' in item:
- details.append(f"Threat: {item['threat']}")
-
- elif item_type in ["Blueprint", "Blueprints"]:
- if 'qr' in item:
- details.append(f"QR: {item['qr']}")
- if 'click' in item:
- details.append(f"Clicks: {item['click']}")
-
- elif item_type in ["Location", "Locations", "Teleporter", "Shop"]:
- if 'planet' in item:
- details.append(item['planet'])
- if 'x' in item and 'y' in item:
- details.append(f"[{item['x']}, {item['y']}]")
-
- elif item_type in ["Skill", "Skills"]:
- if 'category' in item:
- details.append(item['category'])
-
- # Add any other interesting fields
- if 'level' in item:
- details.append(f"Lvl: {item['level']}")
- if 'weight' in item:
- details.append(f"{item['weight']}kg")
-
- return " | ".join(details) if details else ""
-
- def _on_error(self, error):
- """Handle search error."""
- self.status_label.setText(f"Error: {error}")
-
- def _on_item_double_clicked(self, row, column):
- """Handle item double-click."""
- self._open_result(row)
-
- def _open_selected(self):
- """Open selected result."""
- selected = self.results_table.selectedItems()
- if selected:
- row = selected[0].row()
- self._open_result(row)
-
- def _open_result(self, row):
- """Open result in browser."""
- if row < len(self.current_results):
- item = self.current_results[row]
- entity_id = item.get('id', item.get('Id', ''))
- entity_name = item.get('name', item.get('Name', ''))
-
- # Use name for URL if available, otherwise ID
- url_param = entity_name if entity_name else str(entity_id)
- url = NexusEntityAPI.get_entity_url(self.current_entity_type, url_param)
-
- webbrowser.open(url)
-
- def on_hotkey(self):
- """Focus search when hotkey pressed."""
- if hasattr(self, 'search_input'):
- self.search_input.setFocus()
- self.search_input.selectAll()