EU-Utility/docs/PLUGIN_DEVELOPMENT.md

26 KiB

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
  2. Getting Started
  3. Plugin Structure
  4. BasePlugin API
  5. Creating Your First Plugin
  6. UI Development
  7. Using Core Services
  8. Event System
  9. Background Tasks
  10. Nexus API Integration
  11. Plugin Examples
  12. Best Practices
  13. 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

# 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:

# 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

# Get config value with default
value = self.get_config('key', default_value)

# Set config value
self.set_config('key', value)

OCR Service

# 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

# 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

# Read recent log lines
lines = self.read_log(lines=50)

# Read with filter
lines = self.read_log(lines=100, filter_text="loot")

Shared Data

# 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

# 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

# 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

mkdir plugins/tt_calculator
touch plugins/tt_calculator/__init__.py
touch plugins/tt_calculator/plugin.py

Step 2: Create __init__.py

from .plugin import TTValuePlugin

__all__ = ['TTValuePlugin']

Step 3: Create plugin.py

"""
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:

# 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

card = QFrame()
card.setStyleSheet("""
    QFrame {
        background-color: #2a2a2a;
        border: 1px solid #444;
        border-radius: 8px;
    }
""")

Button

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

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

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

# 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:

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)

# 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

# Show notification
self.api.show_notification(
    title="Skill Gain!",
    message="Rifle increased to 25.5",
    duration_ms=3000
)

Clipboard

# Copy to clipboard
self.api.copy_to_clipboard("Text to copy")

# Paste from clipboard
text = self.api.paste_from_clipboard()

Event System

Publishing Events

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

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

# 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

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

# 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

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

# 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

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

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

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

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

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

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

# 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! 🚀