1122 lines
26 KiB
Markdown
1122 lines
26 KiB
Markdown
# 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!** 🚀
|