cleanup: Remove all plugins moved to separate repository

All user-facing plugins have been moved to:
https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo

REMOVED FROM CORE (30+ plugins):
- analytics, auction_tracker, auto_screenshot, auto_updater
- calculator, chat_logger, codex_tracker, crafting_calc
- dashboard, discord_presence, dpp_calculator, enhancer_calc
- event_bus_example, game_reader, game_reader_test, global_tracker
- import_export, inventory_manager, log_parser_test, loot_tracker
- mining_helper, mission_tracker, nexus_search, price_alerts
- profession_scanner, session_exporter, skill_scanner
- spotify_controller, tp_runner, universal_search

ALSO REMOVED:
- plugins/base_plugin.py (was duplicate, should be in package root)
- plugins/__pycache__ (shouldn't be in git)

REMAINING IN CORE:
- plugins/__init__.py
- plugins/settings/ (essential for configuration)
- plugins/plugin_store_ui/ (essential for plugin installation)

EU-Utility is now a pure framework. Users install plugins via
Settings → Plugin Store or manually to the plugins/ folder.

This separation enables:
- Independent plugin development
- Modular installation (only what you need)
- Community contributions via plugin repo
- Cleaner core codebase focused on framework
This commit is contained in:
LemonNexus 2026-02-15 01:53:09 +00:00
parent 725590e247
commit 7d13dd1a29
61 changed files with 0 additions and 13656 deletions

View File

@ -1,3 +0,0 @@
from .plugin import AnalyticsPlugin
__all__ = ['AnalyticsPlugin']

View File

@ -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()
})

View File

@ -1,7 +0,0 @@
"""
Auction Tracker Plugin
"""
from .plugin import AuctionTrackerPlugin
__all__ = ["AuctionTrackerPlugin"]

View File

@ -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'])

View File

@ -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']

View File

@ -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()

View File

@ -1,3 +0,0 @@
from .plugin import AutoUpdaterPlugin
__all__ = ['AutoUpdaterPlugin']

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
"""
Calculator Plugin for EU-Utility
"""
from .plugin import CalculatorPlugin
__all__ = ["CalculatorPlugin"]

View File

@ -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'), ('', '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

View File

@ -1,7 +0,0 @@
"""
Chat Logger Plugin
"""
from .plugin import ChatLoggerPlugin
__all__ = ["ChatLoggerPlugin"]

View File

@ -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'''
<div style="margin: 2px 0;">
<span style="color: #666;">[{time_str}]</span>
<span style="color: {color}; font-weight: bold;">{author}:</span>
<span style="color: #ccc;">{text}</span>
</div>
''')
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)
]

View File

@ -1,7 +0,0 @@
"""
Codex Tracker Plugin
"""
from .plugin import CodexTrackerPlugin
__all__ = ["CodexTrackerPlugin"]

View File

@ -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

View File

@ -1,7 +0,0 @@
"""
Crafting Calculator Plugin
"""
from .plugin import CraftingCalculatorPlugin
__all__ = ["CraftingCalculatorPlugin"]

View File

@ -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}")

View File

@ -1,7 +0,0 @@
"""
Dashboard Plugin
"""
from .plugin import DashboardPlugin
__all__ = ["DashboardPlugin"]

View File

@ -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()

View File

@ -1,3 +0,0 @@
from .plugin import DiscordPresencePlugin
__all__ = ['DiscordPresencePlugin']

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
DPP Calculator Plugin
"""
from .plugin import DPPCalculatorPlugin
__all__ = ["DPPCalculatorPlugin"]

View File

@ -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')

View File

@ -1,7 +0,0 @@
"""
Enhancer Calculator Plugin
"""
from .plugin import EnhancerCalculatorPlugin
__all__ = ["EnhancerCalculatorPlugin"]

View File

@ -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}")

View File

@ -1,4 +0,0 @@
"""Event Bus Example Plugin."""
from .plugin import EventBusExamplePlugin
__all__ = ['EventBusExamplePlugin']

View File

@ -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"<h2>{self.name}</h2>")
layout.addWidget(title)
# Stats
stats_text = f"""
<b>Recorded Events:</b><br>
Big Hits (100 dmg): {len(self.big_hits)}<br>
Combat Skill Gains: {len(self.skill_gains)}<br>
Dragon Loot: {len(self.dragon_loot)}<br>
<br>
<b>Active Subscriptions:</b> {len(self._subscriptions)}
"""
stats_label = QLabel(stats_text)
stats_label.setWordWrap(True)
layout.addWidget(stats_label)
# Recent events
layout.addWidget(QLabel("<b>Recent Combat Events:</b>"))
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()

View File

@ -1,7 +0,0 @@
"""
Game Reader Plugin for EU-Utility
"""
from .plugin import GameReaderPlugin
__all__ = ["GameReaderPlugin"]

View File

@ -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()

View File

@ -1,2 +0,0 @@
"""Game Reader Test Plugin."""
from .plugin import GameReaderTestPlugin

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
"""
Global Tracker Plugin
"""
from .plugin import GlobalTrackerPlugin
__all__ = ["GlobalTrackerPlugin"]

View File

@ -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)

View File

@ -1,3 +0,0 @@
from .plugin import ImportExportPlugin
__all__ = ['ImportExportPlugin']

View File

@ -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)

View File

@ -1,7 +0,0 @@
"""
Inventory Manager Plugin
"""
from .plugin import InventoryManagerPlugin
__all__ = ["InventoryManagerPlugin"]

View File

@ -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()

View File

@ -1,2 +0,0 @@
"""Log Parser Test Plugin."""
from .plugin import LogParserTestPlugin

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
Loot Tracker Plugin
"""
from .plugin import LootTrackerPlugin
__all__ = ["LootTrackerPlugin"]

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
Mining Helper Plugin
"""
from .plugin import MiningHelperPlugin
__all__ = ["MiningHelperPlugin"]

View File

@ -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

View File

@ -1,7 +0,0 @@
"""
Mission Tracker Plugin
"""
from .plugin import MissionTrackerPlugin
__all__ = ["MissionTrackerPlugin"]

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
Nexus Search Plugin for EU-Utility
"""
from .plugin import NexusSearchPlugin
__all__ = ["NexusSearchPlugin"]

View File

@ -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()

View File

@ -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']

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
Profession Scanner Plugin
"""
from .plugin import ProfessionScannerPlugin
__all__ = ["ProfessionScannerPlugin"]

View File

@ -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)

View File

@ -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']

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
Skill Scanner Plugin for EU-Utility
"""
from .plugin import SkillScannerPlugin
__all__ = ["SkillScannerPlugin"]

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
"""
Spotify Controller Plugin for EU-Utility
"""
from .plugin import SpotifyControllerPlugin
__all__ = ["SpotifyControllerPlugin"]

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
TP Runner Plugin
"""
from .plugin import TPRunnerPlugin
__all__ = ["TPRunnerPlugin"]

View File

@ -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()

View File

@ -1,7 +0,0 @@
"""
Universal Search Plugin for EU-Utility
"""
from .plugin import UniversalSearchPlugin
__all__ = ["UniversalSearchPlugin"]

View File

@ -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()