620 lines
14 KiB
Markdown
620 lines
14 KiB
Markdown
# EU-Utility Plugin Development Guide
|
|
|
|
Welcome to plugin development for EU-Utility! This guide covers everything you need to create professional plugins.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Quick Start](#quick-start)
|
|
2. [Plugin Structure](#plugin-structure)
|
|
3. [BasePlugin API Reference](#baseplugin-api-reference)
|
|
4. [Core Services](#core-services)
|
|
5. [Event System](#event-system)
|
|
6. [UI Development](#ui-development)
|
|
7. [Best Practices](#best-practices)
|
|
8. [Examples](#examples)
|
|
9. [Publishing](#publishing)
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### Creating Your First Plugin
|
|
|
|
1. **Create plugin directory:**
|
|
```bash
|
|
mkdir plugins/my_plugin
|
|
touch plugins/my_plugin/__init__.py
|
|
touch plugins/my_plugin/plugin.py
|
|
```
|
|
|
|
2. **Basic plugin structure:**
|
|
```python
|
|
# plugins/my_plugin/plugin.py
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
|
from plugins.base_plugin import BasePlugin
|
|
|
|
class MyPlugin(BasePlugin):
|
|
"""My first EU-Utility plugin."""
|
|
|
|
name = "My Plugin"
|
|
version = "1.0.0"
|
|
author = "Your Name"
|
|
description = "Description of what my plugin does"
|
|
icon = "target" # Icon name from assets/icons/
|
|
hotkey = "ctrl+shift+m" # Optional global hotkey
|
|
|
|
def initialize(self):
|
|
"""Called when plugin is loaded."""
|
|
self.log_info("My Plugin initialized!")
|
|
|
|
# Load saved data
|
|
self.settings = self.load_data("settings", {"enabled": True})
|
|
|
|
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."""
|
|
self.notify_info("Hotkey pressed!", "My Plugin is working!")
|
|
|
|
def shutdown(self):
|
|
"""Called when app closes."""
|
|
self.save_data("settings", self.settings)
|
|
```
|
|
|
|
3. **That's it!** Your plugin will auto-load on next start.
|
|
|
|
---
|
|
|
|
## Plugin Structure
|
|
|
|
```
|
|
plugins/my_plugin/
|
|
├── __init__.py # Makes it a Python package
|
|
├── plugin.py # Main plugin code (required)
|
|
├── ui/ # UI components (optional)
|
|
│ ├── __init__.py
|
|
│ └── widgets.py
|
|
├── assets/ # Plugin-specific assets (optional)
|
|
│ └── icon.svg
|
|
└── README.md # Plugin documentation (optional)
|
|
```
|
|
|
|
---
|
|
|
|
## BasePlugin API Reference
|
|
|
|
### Lifecycle Methods
|
|
|
|
| Method | When Called | Purpose |
|
|
|--------|-------------|---------|
|
|
| `initialize()` | Plugin loaded | Setup, load data, connect to APIs |
|
|
| `get_ui()` | UI requested | Return QWidget for display |
|
|
| `on_show()` | Overlay shown | Resume updates |
|
|
| `on_hide()` | Overlay hidden | Pause updates |
|
|
| `on_hotkey()` | Hotkey pressed | Handle global shortcut |
|
|
| `shutdown()` | App closing | Cleanup, save data |
|
|
|
|
### Data Persistence
|
|
|
|
```python
|
|
# Save data (automatically scoped to your plugin)
|
|
self.save_data("my_key", {"value": 42})
|
|
|
|
# Load data with default
|
|
config = self.load_data("my_key", {"value": 0})
|
|
|
|
# Delete data
|
|
self.delete_data("my_key")
|
|
|
|
# Get all keys
|
|
keys = self.get_all_data_keys()
|
|
```
|
|
|
|
### Notifications
|
|
|
|
```python
|
|
# Show toast notifications
|
|
self.notify("Title", "Message")
|
|
self.notify_info("Info", "Something happened")
|
|
self.notify_success("Success!", "Operation completed", sound=True)
|
|
self.notify_warning("Warning", "Low ammo", sound=False)
|
|
self.notify_error("Error!", "Connection failed", sound=True)
|
|
```
|
|
|
|
### Screen Capture & OCR
|
|
|
|
```python
|
|
# Capture full screen
|
|
screenshot = self.capture_screen()
|
|
|
|
# Capture region
|
|
region = self.capture_region(100, 100, 400, 300)
|
|
|
|
# OCR capture
|
|
result = self.ocr_capture(region=(100, 100, 400, 300))
|
|
text = result['text']
|
|
confidence = result['confidence']
|
|
```
|
|
|
|
### Log Reading
|
|
|
|
```python
|
|
# Read recent log lines
|
|
lines = self.read_log(lines=100)
|
|
|
|
# Filtered reading
|
|
damage_lines = self.read_log(lines=50, filter_text="damage")
|
|
```
|
|
|
|
### Nexus API
|
|
|
|
```python
|
|
# Search items
|
|
items = self.nexus_search("ArMatrix", entity_type="weapons", limit=10)
|
|
|
|
# Get item details
|
|
details = self.nexus_get_item_details("armatrix_lp-35")
|
|
|
|
# Get market data
|
|
market = self.nexus_get_market_data("armatrix_lp-35")
|
|
```
|
|
|
|
### HTTP Requests
|
|
|
|
```python
|
|
# GET with caching
|
|
response = self.http_get("https://api.example.com/data", cache_ttl=300)
|
|
if response['status_code'] == 200:
|
|
data = response['json']
|
|
```
|
|
|
|
### Background Tasks
|
|
|
|
```python
|
|
# Run in background
|
|
def heavy_calculation(data):
|
|
return process(data)
|
|
|
|
task_id = self.run_in_background(
|
|
heavy_calculation,
|
|
large_dataset,
|
|
priority='high',
|
|
on_complete=lambda result: self.update_ui(result),
|
|
on_error=lambda error: self.show_error(error)
|
|
)
|
|
|
|
# Scheduled tasks
|
|
self.schedule_task(
|
|
delay_ms=5000, # Start in 5 seconds
|
|
func=self.refresh_data,
|
|
periodic=True,
|
|
interval_ms=30000 # Repeat every 30 seconds
|
|
)
|
|
|
|
# Cancel task
|
|
self.cancel_task(task_id)
|
|
```
|
|
|
|
### Audio
|
|
|
|
```python
|
|
# Play sounds
|
|
self.play_sound('global') # Predefined sounds
|
|
self.play_sound('hof')
|
|
self.play_sound('skill_gain')
|
|
self.play_sound('/path/to/custom.wav')
|
|
|
|
# Volume control
|
|
self.set_volume(0.8)
|
|
volume = self.get_volume()
|
|
self.mute()
|
|
self.unmute()
|
|
```
|
|
|
|
### Clipboard
|
|
|
|
```python
|
|
# Copy/paste
|
|
self.copy_to_clipboard("Hello World")
|
|
text = self.paste_from_clipboard()
|
|
|
|
# History
|
|
history = self.get_clipboard_history(limit=10)
|
|
```
|
|
|
|
### Window Management
|
|
|
|
```python
|
|
# EU window info
|
|
window = self.get_eu_window()
|
|
if window:
|
|
print(f"EU is at {window['rect']}")
|
|
|
|
# Check focus
|
|
if self.is_eu_focused():
|
|
# Safe to interact
|
|
pass
|
|
|
|
# Bring to front
|
|
self.bring_eu_to_front()
|
|
```
|
|
|
|
### Settings
|
|
|
|
```python
|
|
# Get global settings
|
|
theme = self.get_setting("theme", "dark")
|
|
opacity = self.get_setting("overlay_opacity", 0.9)
|
|
|
|
# Set global settings
|
|
self.set_setting("theme", "light")
|
|
```
|
|
|
|
### Logging
|
|
|
|
```python
|
|
self.log_debug("Debug info")
|
|
self.log_info("Something happened")
|
|
self.log_warning("Warning!")
|
|
self.log_error("Error occurred")
|
|
```
|
|
|
|
---
|
|
|
|
## Core Services
|
|
|
|
### Available Services
|
|
|
|
All services are accessed via `self.api` or convenience methods:
|
|
|
|
| Service | Access | Purpose |
|
|
|---------|--------|---------|
|
|
| OCR | `self.ocr_capture()` | Screen text recognition |
|
|
| Log Reader | `self.read_log()` | Read chat.log |
|
|
| Screenshot | `self.capture_screen()` | Screen capture |
|
|
| DataStore | `self.save_data()` | Persistent storage |
|
|
| Notifications | `self.notify()` | Toast notifications |
|
|
| Window Manager | `self.get_eu_window()` | EU window control |
|
|
| Audio | `self.play_sound()` | Sound playback |
|
|
| Clipboard | `self.copy_to_clipboard()` | Clipboard access |
|
|
| Task Manager | `self.run_in_background()` | Background tasks |
|
|
| Nexus API | `self.nexus_search()` | Entropia data |
|
|
| HTTP Client | `self.http_get()` | Web requests |
|
|
| Event Bus | `self.publish_typed()` | Event system |
|
|
|
|
---
|
|
|
|
## Event System
|
|
|
|
### Publishing Events
|
|
|
|
```python
|
|
from core.event_bus import LootEvent, SkillGainEvent
|
|
|
|
# Publish loot event
|
|
self.publish_typed(LootEvent(
|
|
mob_name="Argonaut",
|
|
items=[{"name": "Animal Oil", "value": 0.05}],
|
|
total_tt_value=0.05
|
|
))
|
|
|
|
# Publish skill gain
|
|
self.publish_typed(SkillGainEvent(
|
|
skill_name="Rifle",
|
|
gain_amount=5.2,
|
|
new_total=1500.0
|
|
))
|
|
```
|
|
|
|
### Subscribing to Events
|
|
|
|
```python
|
|
from core.event_bus import LootEvent, DamageEvent
|
|
|
|
# Subscribe to all loot events
|
|
self.subscribe_typed(LootEvent, self.on_loot)
|
|
|
|
# Subscribe with filters
|
|
self.subscribe_typed(
|
|
DamageEvent,
|
|
self.on_big_hit,
|
|
min_damage=100 # Only events with damage > 100
|
|
)
|
|
|
|
# Unsubscribe
|
|
self.unsubscribe_typed(subscription_id)
|
|
```
|
|
|
|
### Event Types
|
|
|
|
- `SkillGainEvent` - Skill improvements
|
|
- `LootEvent` - Loot received
|
|
- `DamageEvent` - Combat damage
|
|
- `GlobalEvent` - Globals/HOFs
|
|
- `ChatEvent` - Chat messages
|
|
- `EconomyEvent` - Economic events
|
|
- `SystemEvent` - System events
|
|
|
|
---
|
|
|
|
## UI Development
|
|
|
|
### EU Styling
|
|
|
|
Use the EU style constants:
|
|
|
|
```python
|
|
from core.eu_styles import EU_COLORS, EU_STYLES
|
|
|
|
# Colors
|
|
bg_color = EU_COLORS['bg_dark'] # #14131f
|
|
accent = EU_COLORS['accent_orange'] # #ff8c42
|
|
text = EU_COLORS['text_primary'] # #ffffff
|
|
|
|
# Apply to widget
|
|
widget.setStyleSheet(f"""
|
|
QWidget {{
|
|
background-color: {EU_COLORS['bg_dark']};
|
|
color: {EU_COLORS['text_primary']};
|
|
border: 1px solid {EU_COLORS['border_medium']};
|
|
}}
|
|
""")
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
1. **Use layouts** - Never hardcode positions
|
|
2. **Thread safety** - Use `run_in_background()` for heavy work
|
|
3. **Responsive** - Handle window resizing
|
|
4. **Consistent** - Follow EU aesthetic
|
|
5. **Accessible** - Add tooltips, use clear labels
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### Do's ✓
|
|
|
|
- Use `save_data()`/`load_data()` for persistence
|
|
- Handle errors gracefully
|
|
- Use background tasks for heavy work
|
|
- Clean up in `shutdown()`
|
|
- Add docstrings to your methods
|
|
- Test on both Windows and Linux
|
|
|
|
### Don'ts ✗
|
|
|
|
- Don't block the main thread
|
|
- Don't use bare `except:` clauses
|
|
- Don't hardcode paths
|
|
- Don't ignore errors
|
|
- Don't forget to unsubscribe from events
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### Example 1: Simple Loot Tracker
|
|
|
|
```python
|
|
from plugins.base_plugin import BasePlugin
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
|
from core.event_bus import LootEvent
|
|
|
|
class SimpleLootTracker(BasePlugin):
|
|
name = "Simple Loot Tracker"
|
|
version = "1.0.0"
|
|
author = "Your Name"
|
|
description = "Track total loot value"
|
|
|
|
def initialize(self):
|
|
self.total_loot = self.load_data("total", 0.0)
|
|
self.subscribe_typed(LootEvent, self.on_loot)
|
|
|
|
def get_ui(self):
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
self.label = QLabel(f"Total Loot: {self.total_loot:.2f} PED")
|
|
layout.addWidget(self.label)
|
|
return widget
|
|
|
|
def on_loot(self, event):
|
|
self.total_loot += event.total_tt_value
|
|
self.save_data("total", self.total_loot)
|
|
self.label.setText(f"Total Loot: {self.total_loot:.2f} PED")
|
|
|
|
def shutdown(self):
|
|
self.save_data("total", self.total_loot)
|
|
```
|
|
|
|
### Example 2: Calculator with OCR
|
|
|
|
```python
|
|
from plugins.base_plugin import BasePlugin
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel
|
|
|
|
class OcrCalculator(BasePlugin):
|
|
name = "OCR Calculator"
|
|
version = "1.0.0"
|
|
|
|
def get_ui(self):
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
self.result_label = QLabel("Click Scan to read numbers")
|
|
layout.addWidget(self.result_label)
|
|
|
|
scan_btn = QPushButton("Scan Screen")
|
|
scan_btn.clicked.connect(self.scan_numbers)
|
|
layout.addWidget(scan_btn)
|
|
|
|
return widget
|
|
|
|
def scan_numbers(self):
|
|
result = self.ocr_capture()
|
|
text = result['text']
|
|
|
|
# Extract numbers and sum them
|
|
import re
|
|
numbers = [float(n) for n in re.findall(r'\d+\.?\d*', text)]
|
|
total = sum(numbers)
|
|
|
|
self.result_label.setText(f"Found: {numbers}\nTotal: {total}")
|
|
```
|
|
|
|
### Example 3: HTTP API Integration
|
|
|
|
```python
|
|
from plugins.base_plugin import BasePlugin
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextEdit
|
|
|
|
class PriceChecker(BasePlugin):
|
|
name = "Price Checker"
|
|
version = "1.0.0"
|
|
|
|
def get_ui(self):
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
self.text = QTextEdit()
|
|
self.text.setReadOnly(True)
|
|
layout.addWidget(self.text)
|
|
|
|
check_btn = QPushButton("Check Prices")
|
|
check_btn.clicked.connect(self.check_prices)
|
|
layout.addWidget(check_btn)
|
|
|
|
return widget
|
|
|
|
def check_prices(self):
|
|
# Fetch data from external API
|
|
response = self.http_get(
|
|
"https://api.example.com/prices",
|
|
cache_ttl=600 # Cache for 10 minutes
|
|
)
|
|
|
|
if response['status_code'] == 200:
|
|
prices = response['json']
|
|
self.text.setText(f"Current prices: {prices}")
|
|
else:
|
|
self.notify_error("Error", "Failed to fetch prices")
|
|
```
|
|
|
|
---
|
|
|
|
## Publishing
|
|
|
|
### Preparing for Release
|
|
|
|
1. **Test thoroughly**
|
|
- Test on Windows
|
|
- Test all features
|
|
- Check error handling
|
|
|
|
2. **Add documentation**
|
|
- README.md with description
|
|
- Screenshot of UI
|
|
- List of features
|
|
|
|
3. **Version properly**
|
|
- Use semantic versioning (1.0.0)
|
|
- Update version when making changes
|
|
|
|
4. **Submit to marketplace**
|
|
- Package your plugin
|
|
- Submit via Settings → Plugin Store
|
|
|
|
### Plugin Template
|
|
|
|
Use this template to start:
|
|
|
|
```python
|
|
"""
|
|
[Plugin Name] - [Brief description]
|
|
|
|
Author: [Your Name]
|
|
Version: 1.0.0
|
|
License: MIT
|
|
"""
|
|
|
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
|
from plugins.base_plugin import BasePlugin
|
|
|
|
|
|
class [PluginName]Plugin(BasePlugin):
|
|
"""
|
|
[Detailed description of your plugin]
|
|
|
|
Features:
|
|
- Feature 1
|
|
- Feature 2
|
|
- Feature 3
|
|
"""
|
|
|
|
# Metadata
|
|
name = "[Plugin Name]"
|
|
version = "1.0.0"
|
|
author = "[Your Name]"
|
|
description = "[Brief description]"
|
|
icon = "target" # Choose from assets/icons/
|
|
hotkey = None # Set to "ctrl+shift+x" for hotkey support
|
|
|
|
def initialize(self):
|
|
"""Initialize plugin. Called when plugin is loaded."""
|
|
# Load saved data
|
|
self.config = self.load_data("config", {
|
|
"enabled": True,
|
|
"setting1": "default"
|
|
})
|
|
|
|
# Subscribe to events if needed
|
|
# from core.event_bus import SomeEvent
|
|
# self.subscribe_typed(SomeEvent, self.on_event)
|
|
|
|
self.log_info(f"{self.name} initialized")
|
|
|
|
def get_ui(self):
|
|
"""Return the plugin's UI widget."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Add your UI components here
|
|
label = QLabel(f"Welcome to {self.name}!")
|
|
layout.addWidget(label)
|
|
|
|
return widget
|
|
|
|
def on_hotkey(self):
|
|
"""Handle global hotkey press."""
|
|
self.notify_info("Hotkey", f"{self.name} hotkey pressed!")
|
|
|
|
def shutdown(self):
|
|
"""Cleanup when app closes."""
|
|
# Save data
|
|
self.save_data("config", self.config)
|
|
|
|
# Unsubscribe from events
|
|
# self.unsubscribe_all_typed()
|
|
|
|
self.log_info(f"{self.name} shutdown")
|
|
```
|
|
|
|
---
|
|
|
|
## Need Help?
|
|
|
|
- Check existing plugins in `plugins/` directory
|
|
- Read the full API documentation
|
|
- Join the community Discord
|
|
- Open an issue on GitHub
|
|
|
|
Happy plugin development! 🚀
|