# EU-Utility Plugin Development Guide > Complete guide for creating custom plugins for EU-Utility > > **Version:** 2.0 > **Last Updated:** 2025-02-14 --- ## Table of Contents 1. [Introduction](#introduction) 2. [Getting Started](#getting-started) 3. [Plugin Structure](#plugin-structure) 4. [BasePlugin API](#baseplugin-api) 5. [Creating Your First Plugin](#creating-your-first-plugin) 6. [UI Development](#ui-development) 7. [Using Core Services](#using-core-services) 8. [Event System](#event-system) 9. [Background Tasks](#background-tasks) 10. [Nexus API Integration](#nexus-api-integration) 11. [Plugin Examples](#plugin-examples) 12. [Best Practices](#best-practices) 13. [Publishing Plugins](#publishing-plugins) --- ## Introduction EU-Utility uses a **modular plugin architecture** that allows anyone to extend its functionality. Plugins are Python classes that inherit from `BasePlugin` and implement specific methods. ### What You Can Build - **Calculators** - DPP, crafting, markup calculators - **Trackers** - Loot, skills, missions, globals - **Integrations** - Discord, spreadsheets, external APIs - **Tools** - OCR scanners, data exporters, analyzers - **Widgets** - Dashboard widgets, overlay elements ### Prerequisites - Python 3.11+ - PyQt6 knowledge (for UI) - Basic understanding of EU game mechanics --- ## Getting Started ### Plugin Location Plugins are stored in the `plugins/` directory: ``` plugins/ ├── my_plugin/ # Your plugin folder │ ├── __init__.py # Makes it a Python package │ └── plugin.py # Main plugin code └── base_plugin.py # Base class (don't modify) ``` ### Minimal Plugin Template ```python # plugins/my_plugin/plugin.py from plugins.base_plugin import BasePlugin from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel class MyPlugin(BasePlugin): """My first EU-Utility plugin.""" # Required metadata name = "My Plugin" version = "1.0.0" author = "Your Name" description = "What my plugin does" # Optional settings hotkey = "ctrl+shift+y" # Global hotkey enabled = True def initialize(self): """Called when plugin is loaded.""" print(f"[{self.name}] Initialized!") def get_ui(self): """Return the plugin's UI widget.""" widget = QWidget() layout = QVBoxLayout(widget) label = QLabel(f"Hello from {self.name}!") layout.addWidget(label) return widget def on_hotkey(self): """Called when hotkey is pressed.""" print(f"[{self.name}] Hotkey pressed!") def shutdown(self): """Called when app is closing.""" print(f"[{self.name}] Shutting down...") ``` ### Plugin Registration Create `__init__.py`: ```python # plugins/my_plugin/__init__.py from .plugin import MyPlugin __all__ = ['MyPlugin'] ``` --- ## Plugin Structure ### Required Attributes | Attribute | Type | Description | |-----------|------|-------------| | `name` | str | Display name | | `version` | str | Version string | | `author` | str | Your name/handle | | `description` | str | Short description | ### Optional Attributes | Attribute | Type | Default | Description | |-----------|------|---------|-------------| | `hotkey` | str | None | Global hotkey | | `enabled` | bool | True | Start enabled | | `icon` | str | None | Icon path/emoji | ### Required Methods | Method | Purpose | |--------|---------| | `initialize()` | Setup plugin | | `get_ui()` | Return QWidget | ### Optional Methods | Method | Purpose | |--------|---------| | `on_show()` | Overlay became visible | | `on_hide()` | Overlay hidden | | `on_hotkey()` | Hotkey pressed | | `shutdown()` | Cleanup on exit | --- ## BasePlugin API ### Configuration Methods ```python # Get config value with default value = self.get_config('key', default_value) # Set config value self.set_config('key', value) ``` ### OCR Service ```python # Capture screen and OCR result = self.ocr_capture() # Returns: {'text': str, 'confidence': float, 'raw_results': list} # Capture specific region result = self.ocr_capture(region=(x, y, width, height)) ``` ### Screenshot Service ```python # Capture full screen screenshot = self.capture_screen(full_screen=True) # Capture region region = self.capture_region(x, y, width, height) # Get last screenshot last = self.get_last_screenshot() ``` ### Log Service ```python # Read recent log lines lines = self.read_log(lines=50) # Read with filter lines = self.read_log(lines=100, filter_text="loot") ``` ### Shared Data ```python # Set data for other plugins self.set_shared_data('my_key', data) # Get data from other plugins data = self.get_shared_data('other_key', default=None) ``` ### Utility Methods ```python # Format currency ped_text = self.format_ped(123.45) # "123.45 PED" pec_text = self.format_pec(50) # "50 PEC" # Calculate DPP dpp = self.calculate_dpp(damage=45.0, ammo=100, decay=2.5) # Calculate markup markup = self.calculate_markup(price=150.0, tt=100.0) # 150.0% ``` ### Audio Service ```python # Play sounds self.play_sound('hof') # Predefined sounds self.play_sound('skill_gain') self.play_sound('alert') self.play_sound('/path/to/custom.wav') # Volume control self.set_volume(0.8) # 0.0 to 1.0 volume = self.get_volume() self.mute() self.unmute() ``` --- ## Creating Your First Plugin ### Step-by-Step Tutorial Let's create a simple **TT Value Calculator** plugin. #### Step 1: Create Plugin Directory ```bash mkdir plugins/tt_calculator touch plugins/tt_calculator/__init__.py touch plugins/tt_calculator/plugin.py ``` #### Step 2: Create `__init__.py` ```python from .plugin import TTValuePlugin __all__ = ['TTValuePlugin'] ``` #### Step 3: Create `plugin.py` ```python """ EU-Utility - TT Value Calculator Plugin Calculate total TT value from a list of items. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit ) from plugins.base_plugin import BasePlugin class TTValuePlugin(BasePlugin): """Calculate total TT value of items.""" name = "TT Calculator" version = "1.0.0" author = "Your Name" description = "Calculate total TT value from item list" hotkey = "ctrl+shift+t" def initialize(self): """Setup the plugin.""" self.items = [] def get_ui(self): """Create the plugin UI.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setSpacing(15) # Title title = QLabel("TT Value Calculator") title.setStyleSheet(""" color: #4a9eff; font-size: 18px; font-weight: bold; """) layout.addWidget(title) # Instructions info = QLabel("Enter items (format: Name - TT Value):") info.setStyleSheet("color: rgba(255,255,255,150);") layout.addWidget(info) # Input area self.input_text = QTextEdit() self.input_text.setPlaceholderText( "Example:\n" "Animal Oil - 0.05\n" "Lysterium Stone - 0.01\n" "Weapon - 45.50" ) self.input_text.setStyleSheet(""" QTextEdit { background-color: #2a2a2a; color: white; border: 1px solid #444; border-radius: 4px; padding: 8px; } """) layout.addWidget(self.input_text) # Calculate button calc_btn = QPushButton("Calculate Total") calc_btn.setStyleSheet(""" QPushButton { background-color: #4caf50; color: white; padding: 12px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #5cbf60; } """) calc_btn.clicked.connect(self._calculate) layout.addWidget(calc_btn) # Result self.result_label = QLabel("Total: 0.00 PED") self.result_label.setStyleSheet(""" color: #ffc107; font-size: 20px; font-weight: bold; """) layout.addWidget(self.result_label) layout.addStretch() return widget def _calculate(self): """Parse input and calculate total.""" text = self.input_text.toPlainText() lines = text.strip().split('\n') total = 0.0 item_count = 0 for line in lines: line = line.strip() if not line: continue # Try to extract number from line parts = line.split('-') if len(parts) >= 2: try: value = float(parts[-1].strip().replace('PED', '').replace('PEC', '')) total += value item_count += 1 except ValueError: pass self.result_label.setText( f"Items: {item_count} | Total: {total:.2f} PED" ) def on_hotkey(self): """Focus input when hotkey pressed.""" self.input_text.setFocus() ``` #### Step 4: Test Your Plugin 1. Restart EU-Utility 2. Your plugin should appear in the plugin list 3. Press `Ctrl + Shift + T` to open it --- ## UI Development ### Styling Guidelines EU-Utility uses a dark theme. Follow these conventions: ```python # Background colors BG_DARK = "#1a1a1a" # Main background BG_PANEL = "#2a2a2a" # Panel/card background BG_INPUT = "#333333" # Input fields # Accent colors ACCENT_BLUE = "#4a9eff" # Primary accent ACCENT_GREEN = "#4caf50" # Success ACCENT_ORANGE = "#ff8c42" # Warning/Highlight ACCENT_RED = "#f44336" # Error # Text colors TEXT_WHITE = "#ffffff" TEXT_MUTED = "rgba(255,255,255,150)" ``` ### Common Widget Patterns #### Card/Panel ```python card = QFrame() card.setStyleSheet(""" QFrame { background-color: #2a2a2a; border: 1px solid #444; border-radius: 8px; } """) ``` #### Button ```python btn = QPushButton("Click Me") btn.setStyleSheet(""" QPushButton { background-color: #4a9eff; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #5aafff; } QPushButton:pressed { background-color: #3a8eef; } """) ``` #### Input Field ```python input_field = QLineEdit() input_field.setStyleSheet(""" QLineEdit { background-color: #333; color: white; padding: 8px; border: 2px solid #555; border-radius: 4px; } QLineEdit:focus { border-color: #4a9eff; } """) ``` #### Progress Bar ```python progress = QProgressBar() progress.setStyleSheet(""" QProgressBar { background-color: #333; border: none; border-radius: 4px; height: 8px; } QProgressBar::chunk { background-color: #4a9eff; border-radius: 4px; } """) ``` ### Layout Best Practices ```python # Use consistent spacing layout.setSpacing(10) layout.setContentsMargins(15, 15, 15, 15) # Add stretch at the end layout.addStretch() # Use QGroupBox for sections group = QGroupBox("Section Title") group.setStyleSheet(""" QGroupBox { color: rgba(255,255,255,200); border: 1px solid #444; border-radius: 6px; margin-top: 10px; font-weight: bold; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } """) ``` --- ## Using Core Services ### HTTP Client Make API requests with caching: ```python def fetch_data(self): # Use the built-in http_get method response = self.http_get( "https://api.example.com/data", cache_ttl=300, # Cache for 5 minutes headers={'Accept': 'application/json'} ) if response: return response.get('json') return None ``` ### Window Manager (Windows) ```python # Check if EU window exists if self.api.services.get('window'): window_manager = self.api.services['window'] # Find EU window eu_window = window_manager.find_eu_window() if eu_window: print(f"EU window: {eu_window.width}x{eu_window.height}") # Check if focused if window_manager.is_eu_focused(): print("EU is active!") ``` ### Notifications ```python # Show notification self.api.show_notification( title="Skill Gain!", message="Rifle increased to 25.5", duration_ms=3000 ) ``` ### Clipboard ```python # Copy to clipboard self.api.copy_to_clipboard("Text to copy") # Paste from clipboard text = self.api.paste_from_clipboard() ``` --- ## Event System ### Publishing Events ```python from core.event_bus import SkillGainEvent, LootEvent # Publish a skill gain self.publish_typed(SkillGainEvent( skill_name="Rifle", skill_value=25.5, gain_amount=0.01 )) # Publish loot event self.publish_typed(LootEvent( mob_name="Daikiba", items=[{"name": "Animal Oil", "value": 0.05}], total_tt_value=0.05 )) ``` ### Subscribing to Events ```python from core.event_bus import SkillGainEvent, LootEvent, DamageEvent class MyPlugin(BasePlugin): def initialize(self): # Subscribe to skill gains self.sub_id = self.subscribe_typed( SkillGainEvent, self.on_skill_gain, replay_last=10 # Get last 10 events ) def on_skill_gain(self, event: SkillGainEvent): print(f"Skill gained: {event.skill_name} = {event.skill_value}") def shutdown(self): # Unsubscribe self.unsubscribe_typed(self.sub_id) ``` ### Event Filtering ```python # Subscribe to high damage only self.subscribe_typed( DamageEvent, self.on_big_hit, min_damage=100 ) # Subscribe to specific mob loot self.subscribe_typed( LootEvent, self.on_dragon_loot, mob_types=["Dragon", "Drake"] ) # Subscribe to specific skills self.subscribe_typed( SkillGainEvent, self.on_combat_skill, skill_names=["Rifle", "Pistol", "Melee"] ) ``` ### Available Event Types | Event | Attributes | |-------|------------| | `SkillGainEvent` | skill_name, skill_value, gain_amount | | `LootEvent` | mob_name, items, total_tt_value, position | | `DamageEvent` | damage_amount, damage_type, is_critical, target_name | | `GlobalEvent` | player_name, achievement_type, value, item_name | | `ChatEvent` | channel, sender, message | | `EconomyEvent` | transaction_type, amount, currency, description | | `SystemEvent` | message, severity | --- ## Background Tasks ### Running Tasks in Background ```python def heavy_calculation(data): # This runs in a background thread result = process_large_dataset(data) return result # Run in background task_id = self.run_in_background( heavy_calculation, large_dataset, priority='high', # 'high', 'normal', 'low' on_complete=self.on_done, # Called with result on_error=self.on_error # Called with exception ) ``` ### Scheduling Tasks ```python # One-time delayed task task_id = self.schedule_task( delay_ms=5000, # 5 seconds func=lambda: print("Delayed!"), on_complete=lambda _: print("Done") ) # Periodic task task_id = self.schedule_task( delay_ms=0, # Start immediately func=self.refresh_data, periodic=True, interval_ms=30000, # Every 30 seconds on_complete=lambda data: self.update_ui(data) ) # Cancel task self.cancel_task(task_id) ``` ### Thread-Safe UI Updates ```python def initialize(self): # Connect signals once self.connect_task_signals( on_completed=self._on_task_done, on_failed=self._on_task_error ) def _on_task_done(self, task_id, result): # This runs in main thread - safe to update UI! self.status_label.setText(f"Complete: {result}") def _on_task_error(self, task_id, error): self.status_label.setText(f"Error: {error}") ``` --- ## Nexus API Integration ### Searching ```python # Search for items results = self.nexus_search("ArMatrix", entity_type="items") # Search for mobs mobs = self.nexus_search("Atrox", entity_type="mobs") # Search for locations locations = self.nexus_search("Fort", entity_type="locations") ``` ### Getting Item Details ```python details = self.nexus_get_item_details("armatrix_lp-35") if details: print(f"Name: {details['name']}") print(f"TT Value: {details['tt_value']} PED") print(f"Damage: {details.get('damage')}") ``` ### Getting Market Data ```python market = self.nexus_get_market_data("armatrix_lp-35") if market: print(f"Current markup: {market['current_markup']:.1f}%") print(f"24h Volume: {market['volume_24h']}") # Access order book for buy in market.get('buy_orders', [])[:5]: print(f"Buy: {buy['price']} PED x {buy['quantity']}") ``` ### Entity Types Valid entity types for `nexus_search()`: - `items`, `weapons`, `armors` - `mobs`, `pets` - `blueprints`, `materials` - `locations`, `teleporters`, `shops`, `vendors`, `planets`, `areas` - `skills` - `enhancers`, `medicaltools`, `finders`, `excavators`, `refiners` - `vehicles`, `decorations`, `furniture` - `storagecontainers`, `strongboxes` --- ## Plugin Examples ### Example 1: Simple Counter Plugin ```python from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton from plugins.base_plugin import BasePlugin class CounterPlugin(BasePlugin): """A simple click counter.""" name = "Click Counter" version = "1.0.0" author = "Tutorial" description = "Counts button clicks" hotkey = "ctrl+shift+k" def initialize(self): self.count = self.get_config('count', 0) def get_ui(self): widget = QWidget() layout = QVBoxLayout(widget) self.label = QLabel(f"Count: {self.count}") self.label.setStyleSheet("font-size: 24px; color: #4a9eff;") layout.addWidget(self.label) btn = QPushButton("Click Me!") btn.clicked.connect(self._increment) layout.addWidget(btn) return widget def _increment(self): self.count += 1 self.set_config('count', self.count) self.label.setText(f"Count: {self.count}") ``` ### Example 2: Price Monitor Plugin ```python from PyQt6.QtWidgets import * from plugins.base_plugin import BasePlugin from core.event_bus import LootEvent class PriceMonitorPlugin(BasePlugin): """Monitor item prices from loot.""" name = "Price Monitor" version = "1.0.0" author = "Tutorial" description = "Track item prices from loot" def initialize(self): self.prices = {} # Subscribe to loot events self.sub_id = self.subscribe_typed( LootEvent, self.on_loot, replay_last=50 ) def get_ui(self): widget = QWidget() layout = QVBoxLayout(widget) title = QLabel("Price Monitor") title.setStyleSheet("font-size: 18px; color: #4a9eff;") layout.addWidget(title) self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels(["Item", "TT", "Count"]) layout.addWidget(self.table) self._update_table() return widget def on_loot(self, event): for item in event.items: name = item['name'] value = item['value'] if name not in self.prices: self.prices[name] = {'tt': value, 'count': 0} self.prices[name]['count'] += 1 self._update_table() def _update_table(self): self.table.setRowCount(len(self.prices)) for i, (name, data) in enumerate(sorted(self.prices.items())): self.table.setItem(i, 0, QTableWidgetItem(name)) self.table.setItem(i, 1, QTableWidgetItem(f"{data['tt']:.2f}")) self.table.setItem(i, 2, QTableWidgetItem(str(data['count']))) def shutdown(self): self.unsubscribe_typed(self.sub_id) ``` ### Example 3: Weapon Comparator ```python from decimal import Decimal from PyQt6.QtWidgets import * from plugins.base_plugin import BasePlugin class WeaponComparatorPlugin(BasePlugin): """Compare weapons from Nexus.""" name = "Weapon Comparator" version = "1.0.0" author = "Tutorial" description = "Compare weapon stats" def initialize(self): self.weapons = [] def get_ui(self): widget = QWidget() layout = QVBoxLayout(widget) # Search search_layout = QHBoxLayout() self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search weapons...") search_layout.addWidget(self.search_input) search_btn = QPushButton("Search") search_btn.clicked.connect(self._search) search_layout.addWidget(search_btn) layout.addLayout(search_layout) # Results self.results = QTableWidget() self.results.setColumnCount(5) self.results.setHorizontalHeaderLabels( ["Name", "Damage", "Range", "DPP", "Add"] ) layout.addWidget(self.results) # Comparison self.compare_table = QTableWidget() self.compare_table.setColumnCount(4) self.compare_table.setHorizontalHeaderLabels( ["Name", "Damage", "DPP", "Remove"] ) layout.addWidget(QLabel("Comparison:")) layout.addWidget(self.compare_table) return widget def _search(self): query = self.search_input.text() results = self.nexus_search(query, entity_type="weapons") self.results.setRowCount(len(results)) for i, weapon in enumerate(results): self.results.setItem(i, 0, QTableWidgetItem(weapon['name'])) # Get details for full stats details = self.nexus_get_item_details(weapon['id']) if details: dmg = details.get('damage', 0) range_val = details.get('range', 0) dpp = self._calculate_dpp(details) self.results.setItem(i, 1, QTableWidgetItem(str(dmg))) self.results.setItem(i, 2, QTableWidgetItem(f"{range_val}m")) self.results.setItem(i, 3, QTableWidgetItem(f"{dpp:.2f}")) add_btn = QPushButton("Add") add_btn.clicked.connect(lambda _, d=details: self._add_to_compare(d)) self.results.setCellWidget(i, 4, add_btn) def _calculate_dpp(self, details): damage = Decimal(str(details.get('damage', 0))) decay = Decimal(str(details.get('decay', 0))) ammo = Decimal(str(details.get('ammo_consumption', 0))) ammo_cost = ammo * Decimal('0.01') total_cost = ammo_cost + decay if total_cost > 0: return damage / (total_cost / Decimal('100')) return Decimal('0') def _add_to_compare(self, details): self.weapons.append(details) self._update_comparison() def _update_comparison(self): self.compare_table.setRowCount(len(self.weapons)) for i, w in enumerate(self.weapons): self.compare_table.setItem(i, 0, QTableWidgetItem(w['name'])) self.compare_table.setItem(i, 1, QTableWidgetItem(str(w.get('damage', 0)))) self.compare_table.setItem(i, 2, QTableWidgetItem(f"{self._calculate_dpp(w):.2f}")) ``` --- ## Best Practices ### Do's ✅ **Use background tasks** for heavy operations ✅ **Unsubscribe from events** in `shutdown()` ✅ **Save data** to disk for persistence ✅ **Handle errors gracefully** ✅ **Use the API** instead of reinventing ✅ **Follow styling conventions** ✅ **Add docstrings** to your plugin ### Don'ts ❌ **Block the main thread** with long operations ❌ **Forget to cleanup** resources ❌ **Hardcode paths** - use relative paths ❌ **Ignore errors** - users need feedback ❌ **Access game files directly** - use services ### Code Style ```python """ EU-Utility - Plugin Name Short description of what this plugin does. Features: - Feature 1 - Feature 2 """ from typing import Optional, Dict, Any from PyQt6.QtWidgets import * from plugins.base_plugin import BasePlugin class MyPlugin(BasePlugin): """ Detailed plugin description. Attributes: name: Display name version: Semantic version author: Creator name description: Short description """ name = "My Plugin" version = "1.0.0" author = "Your Name" description = "What it does" def initialize(self) -> None: """Initialize plugin state.""" self.data = self.get_config('data', []) def get_ui(self) -> QWidget: """Create and return plugin UI.""" # Implementation pass def shutdown(self) -> None: """Cleanup resources.""" self.set_config('data', self.data) super().shutdown() ``` --- ## Publishing Plugins ### Plugin Package Structure ``` my_plugin/ ├── __init__.py ├── plugin.py ├── README.md ├── requirements.txt # Optional: extra dependencies └── assets/ # Optional: icons, sounds └── icon.png ``` ### README Template ```markdown # My Plugin Description of your plugin. ## Installation 1. Copy `my_plugin` folder to `plugins/` 2. Restart EU-Utility ## Usage Press `Ctrl+Shift+Y` to open. ## Features - Feature 1 - Feature 2 ## Changelog ### v1.0.0 - Initial release ``` ### Sharing 1. **Zip your plugin folder** 2. **Share on forums/Discord** 3. **Include installation instructions** ### Plugin Store (Future) EU-Utility may include a plugin store for easy installation. --- **Happy plugin development!** 🚀