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
- Introduction
- Getting Started
- Plugin Structure
- BasePlugin API
- Creating Your First Plugin
- UI Development
- Using Core Services
- Event System
- Background Tasks
- Nexus API Integration
- Plugin Examples
- Best Practices
- 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
- Restart EU-Utility
- Your plugin should appear in the plugin list
- Press
Ctrl + Shift + Tto 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,armorsmobs,petsblueprints,materialslocations,teleporters,shops,vendors,planets,areasskillsenhancers,medicaltools,finders,excavators,refinersvehicles,decorations,furniturestoragecontainers,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
- Zip your plugin folder
- Share on forums/Discord
- Include installation instructions
Plugin Store (Future)
EU-Utility may include a plugin store for easy installation.
Happy plugin development! 🚀