EU-Utility/core/base_plugin.py

894 lines
26 KiB
Python

"""
EU-Utility - Plugin Base Class
==============================
Defines the interface that all plugins must implement.
Includes PluginAPI integration for cross-plugin communication.
Quick Start:
------------
from core.base_plugin import BasePlugin
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
class MyPlugin(BasePlugin):
name = "My Plugin"
version = "1.0.0"
def initialize(self) -> None:
self.log_info("My Plugin initialized!")
def get_ui(self) -> QWidget:
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("Hello from My Plugin!"))
return widget
Hotkey Support:
---------------
Plugins can define hotkeys in two ways:
1. Legacy single hotkey (simple toggle):
hotkey = "ctrl+shift+n"
2. New multi-hotkey format (recommended):
hotkeys = [
{
'action': 'toggle',
'description': 'Toggle My Plugin',
'default': 'ctrl+shift+m',
'config_key': 'myplugin_toggle'
}
]
"""
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, TYPE_CHECKING, Callable, List, Type, Union
from datetime import datetime
if TYPE_CHECKING:
from PyQt6.QtWidgets import QWidget
from core.event_bus import BaseEvent
class BasePlugin(ABC):
"""Base class for all EU-Utility plugins.
To create a plugin, inherit from this class and implement
the required abstract methods. Override class attributes
to define plugin metadata.
Attributes:
name: Human-readable plugin name
version: Plugin version (semantic versioning recommended)
author: Plugin author name
description: Brief description of plugin functionality
icon: Optional path to plugin icon
hotkey: Legacy single hotkey (e.g., "ctrl+shift+n")
hotkeys: New multi-hotkey format (list of dicts)
enabled: Whether plugin starts enabled
dependencies: Dict of required dependencies
"""
# Plugin metadata - override in subclass
name: str = "Unnamed Plugin"
version: str = "1.0.0"
author: str = "Unknown"
description: str = "No description provided"
icon: Optional[str] = None
# Plugin settings
hotkey: Optional[str] = None
hotkeys: Optional[List[Dict[str, str]]] = None
enabled: bool = True
# Dependencies format:
# {
# 'pip': ['package1', 'package2>=1.0'],
# 'plugins': ['plugin_id1', 'plugin_id2'],
# 'optional': {'package3': 'description'}
# }
dependencies: Dict[str, Any] = {}
def __init__(self, overlay_window: Any, config: Dict[str, Any]) -> None:
"""Initialize the plugin.
Args:
overlay_window: The main overlay window instance
config: Plugin-specific configuration dictionary
"""
self.overlay = overlay_window
self.config = config
self._ui: Optional[Any] = None
self._api_registered: bool = False
self._plugin_id: str = f"{self.__class__.__module__}.{self.__class__.__name__}"
# Track event subscriptions for cleanup
self._event_subscriptions: List[str] = []
# Get API instance
try:
from core.plugin_api import get_api
self.api = get_api()
except ImportError:
self.api = None
@abstractmethod
def initialize(self) -> None:
"""Called when plugin is loaded. Setup API connections, etc.
This is where you should:
- Register API endpoints
- Subscribe to events
- Initialize resources
- Set up UI components
"""
pass
@abstractmethod
def get_ui(self) -> Any:
"""Return the plugin's UI widget (QWidget).
Returns:
QWidget instance for the plugin's interface
Example:
def get_ui(self) -> QWidget:
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("My Plugin"))
return widget
"""
return None
def on_show(self) -> None:
"""Called when overlay becomes visible.
Use this to refresh data or start animations when
the user opens the overlay.
"""
pass
def on_hide(self) -> None:
"""Called when overlay is hidden.
Use this to pause expensive operations when the
overlay is not visible.
"""
pass
def on_hotkey(self) -> None:
"""Called when plugin's hotkey is pressed.
Override this to handle hotkey actions.
Default behavior toggles the overlay.
"""
pass
def shutdown(self) -> None:
"""Called when app is closing. Cleanup resources.
This is called automatically when the application exits.
Override to perform custom cleanup.
"""
# Unregister APIs
if self.api and self._api_registered:
# Note: unregister_api method would need to be implemented
pass
# Unsubscribe from all typed events
self.unsubscribe_all_typed()
# ========== Config Methods ==========
def get_config(self, key: str, default: Any = None) -> Any:
"""Get a config value with default.
Args:
key: Configuration key
default: Default value if key not found
Returns:
Config value or default
"""
return self.config.get(key, default)
def set_config(self, key: str, value: Any) -> None:
"""Set a config value.
Args:
key: Configuration key
value: Value to store
"""
self.config[key] = value
# ========== API Methods ==========
def register_api(self, name: str, handler: Callable,
api_type: Optional[str] = None,
description: str = "") -> bool:
"""Register an API endpoint for other plugins to use.
Args:
name: API endpoint name
handler: Function to handle API calls
api_type: Optional API type categorization
description: Human-readable description
Returns:
True if registration succeeded
Example:
self.register_api(
"scan_window",
self.scan_window,
"ocr",
"Scan game window and return text"
)
"""
if not self.api:
print(f"[{self.name}] API not available")
return False
self._api_registered = True
return True
def call_api(self, plugin_id: str, api_name: str,
*args: Any, **kwargs: Any) -> Any:
"""Call another plugin's API.
Args:
plugin_id: ID of the plugin to call
api_name: Name of the API endpoint
*args: Positional arguments
**kwargs: Keyword arguments
Returns:
API call result
Raises:
RuntimeError: If API not available
"""
if not self.api:
raise RuntimeError("API not available")
# This would call through the API system
return None
# ========== Screenshot Service Methods ==========
def capture_screen(self, full_screen: bool = True) -> Optional[Any]:
"""Capture screenshot.
Args:
full_screen: If True, capture entire screen
Returns:
PIL Image object or None
"""
if not self.api:
return None
return self.api.capture_screen() if hasattr(self.api, 'capture_screen') else None
def capture_region(self, x: int, y: int, width: int, height: int) -> Optional[Any]:
"""Capture specific screen region.
Args:
x: Left coordinate
y: Top coordinate
width: Region width
height: Region height
Returns:
PIL Image object or None
"""
if not self.api:
return None
return self.api.capture_screen(region=(x, y, width, height))
# ========== OCR Service Methods ==========
def ocr_capture(self, region: Optional[tuple] = None) -> Dict[str, Any]:
"""Capture screen and perform OCR.
Args:
region: Optional (x, y, width, height) tuple
Returns:
Dict with 'text', 'confidence', 'error' keys
"""
if not self.api:
return {"text": "", "confidence": 0, "error": "API not available"}
try:
text = self.api.recognize_text(region=region)
return {"text": text, "confidence": 1.0, "error": None}
except Exception as e:
return {"text": "", "confidence": 0, "error": str(e)}
# ========== Log Reader Methods ==========
def read_log(self, lines: int = 50, filter_text: Optional[str] = None) -> List[str]:
"""Read recent game log lines.
Args:
lines: Number of lines to read
filter_text: Optional text to filter lines
Returns:
List of log line strings
"""
if not self.api:
return []
log_lines = self.api.read_log_lines(lines)
if filter_text:
log_lines = [line for line in log_lines if filter_text in line]
return log_lines
# ========== Data Store Methods ==========
def get_shared_data(self, key: str, default: Any = None) -> Any:
"""Get shared data from other plugins.
Args:
key: Data key
default: Default value if not found
Returns:
Stored data or default
"""
if not self.api:
return default
return self.api.get_data(key, default)
def set_shared_data(self, key: str, value: Any) -> None:
"""Set shared data for other plugins.
Args:
key: Data key
value: Value to store
"""
if self.api:
self.api.set_data(key, value)
# ========== Legacy Event System ==========
def publish_event(self, event_type: str, data: Dict[str, Any]) -> bool:
"""Publish an event for other plugins to consume (legacy).
Args:
event_type: Event type string
data: Event data dictionary
Returns:
True if published
"""
if self.api:
return self.api.publish(event_type, data)
return False
def subscribe(self, event_type: str, callback: Callable) -> str:
"""Subscribe to events from other plugins (legacy).
Args:
event_type: Event type string
callback: Function to call when event occurs
Returns:
Subscription ID
"""
if self.api:
return self.api.subscribe(event_type, callback)
return ""
# ========== Enhanced Typed Event System ==========
def publish_typed(self, event: 'BaseEvent') -> bool:
"""Publish a typed event to the Event Bus.
Args:
event: A typed event instance
Returns:
True if published
Example:
from core.event_bus import LootEvent
self.publish_typed(LootEvent(
mob_name="Daikiba",
items=[{"name": "Animal Oil", "value": 0.05}],
total_tt_value=0.05
))
"""
# This would integrate with the EventBus
return True
def subscribe_typed(
self,
event_class: Type['BaseEvent'],
callback: Callable,
**filter_kwargs: Any
) -> str:
"""Subscribe to a specific event type with optional filtering.
Args:
event_class: The event class to subscribe to
callback: Function to call when matching events occur
**filter_kwargs: Additional filter criteria
Returns:
Subscription ID
"""
if not self.api:
print(f"[{self.name}] API not available for event subscription")
return ""
sub_id = f"sub_{id(callback)}"
self._event_subscriptions.append(sub_id)
return sub_id
def unsubscribe_typed(self, subscription_id: str) -> bool:
"""Unsubscribe from a specific typed event subscription.
Args:
subscription_id: The subscription ID returned by subscribe_typed
Returns:
True if subscription was found and removed
"""
if subscription_id in self._event_subscriptions:
self._event_subscriptions.remove(subscription_id)
return True
return False
def unsubscribe_all_typed(self) -> None:
"""Unsubscribe from all typed event subscriptions."""
self._event_subscriptions.clear()
def get_recent_events(
self,
event_type: Optional[Type['BaseEvent']] = None,
count: int = 100,
category: Optional[str] = None
) -> List['BaseEvent']:
"""Get recent events from history.
Args:
event_type: Filter by event class
count: Maximum number of events to return
category: Filter by event category
Returns:
List of matching events
"""
return []
# ========== Utility Methods ==========
def format_ped(self, value: float) -> str:
"""Format PED value.
Args:
value: PED amount
Returns:
Formatted string (e.g., "12.34 PED")
"""
return f"{value:.2f} PED"
def format_pec(self, value: float) -> str:
"""Format PEC value.
Args:
value: PEC amount
Returns:
Formatted string (e.g., "123 PEC")
"""
return f"{value:.0f} PEC"
def calculate_dpp(self, damage: float, ammo: int, decay: float) -> float:
"""Calculate Damage Per PEC.
Args:
damage: Damage dealt
ammo: Ammo consumed
decay: Weapon decay in PED
Returns:
DPP value
"""
if damage <= 0:
return 0.0
ammo_cost = ammo * 0.01
total_cost = ammo_cost + decay
if total_cost <= 0:
return 0.0
return damage / (total_cost / 100)
def calculate_markup(self, price: float, tt: float) -> float:
"""Calculate markup percentage.
Args:
price: Market price
tt: TT value
Returns:
Markup percentage
"""
if tt <= 0:
return 0.0
return (price / tt) * 100
# ========== Audio Service Methods ==========
def play_sound(self, filename_or_key: str, blocking: bool = False) -> bool:
"""Play a sound by key or filename.
Args:
filename_or_key: Sound key or file path
blocking: If True, wait for sound to complete
Returns:
True if sound was queued/played
"""
if not self.api:
return False
return self.api.play_sound(filename_or_key)
def set_volume(self, volume: float) -> None:
"""Set global audio volume.
Args:
volume: Volume level from 0.0 to 1.0
"""
if self.api and hasattr(self.api, 'set_volume'):
self.api.set_volume(volume)
def get_volume(self) -> float:
"""Get current audio volume.
Returns:
Current volume level (0.0 to 1.0)
"""
if self.api and hasattr(self.api, 'get_volume'):
return self.api.get_volume()
return 0.0
# ========== Background Task Methods ==========
def run_in_background(self, func: Callable, *args: Any,
priority: str = 'normal',
on_complete: Optional[Callable] = None,
on_error: Optional[Callable] = None,
**kwargs: Any) -> str:
"""Run a function in a background thread.
Args:
func: Function to execute in background
*args: Positional arguments for the function
priority: 'high', 'normal', or 'low'
on_complete: Called with result when task completes
on_error: Called with exception when task fails
**kwargs: Keyword arguments for the function
Returns:
Task ID for tracking/cancellation
"""
if not self.api:
raise RuntimeError("API not available")
return self.api.run_task(func, *args, callback=on_complete,
error_handler=on_error)
# ========== Nexus API Methods ==========
def nexus_search(self, query: str, entity_type: str = "items",
limit: int = 20) -> List[Dict[str, Any]]:
"""Search for entities via Entropia Nexus API.
Args:
query: Search query string
entity_type: Type of entity (items, weapons, mobs, etc.)
limit: Maximum number of results
Returns:
List of search result dictionaries
"""
if not self.api:
return []
return self.api.search_items(query, limit)
def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]:
"""Get detailed information about a specific item.
Args:
item_id: The item's unique identifier
Returns:
Dictionary with item details, or None
"""
if not self.api:
return None
return self.api.get_item_details(item_id)
def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]:
"""Get market data for a specific item.
Args:
item_id: The item's unique identifier
Returns:
Dictionary with market data, or None
"""
if not self.api:
return None
# This would call the market data endpoint
return None
def nexus_is_available(self) -> bool:
"""Check if Nexus API is available.
Returns:
True if Nexus API service is ready
"""
if not self.api:
return False
return True
# ========== HTTP Client Methods ==========
def http_get(self, url: str, cache_ttl: int = 300,
headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Dict[str, Any]:
"""Make an HTTP GET request with caching.
Args:
url: The URL to fetch
cache_ttl: Cache TTL in seconds
headers: Additional headers
**kwargs: Additional arguments
Returns:
Response dictionary
"""
if not self.api:
raise RuntimeError("API not available")
return self.api.http_get(url, cache=True, cache_duration=cache_ttl)
# ========== DataStore Methods ==========
def save_data(self, key: str, data: Any) -> bool:
"""Save data to persistent storage.
Data is automatically scoped to this plugin and survives app restarts.
Args:
key: Key to store data under
data: Data to store (must be JSON serializable)
Returns:
True if saved successfully
"""
if not self.api:
return False
return self.api.set_data(key, data)
def load_data(self, key: str, default: Any = None) -> Any:
"""Load data from persistent storage.
Args:
key: Key to load data from
default: Default value if key doesn't exist
Returns:
Stored data or default value
"""
if not self.api:
return default
return self.api.get_data(key, default)
def delete_data(self, key: str) -> bool:
"""Delete data from persistent storage.
Args:
key: Key to delete
Returns:
True if deleted (or didn't exist)
"""
if not self.api:
return False
return self.api.delete_data(key)
def get_all_data_keys(self) -> List[str]:
"""Get all data keys stored by this plugin.
Returns:
List of key names
"""
if not self.api:
return []
# This would return keys from the data store
return []
# ========== Window Manager Methods ==========
def get_eu_window(self) -> Optional[Dict[str, Any]]:
"""Get information about the Entropia Universe game window.
Returns:
Dict with window info or None
"""
if not self.api:
return None
return self.api.get_eu_window()
def is_eu_focused(self) -> bool:
"""Check if Entropia Universe window is currently focused.
Returns:
True if EU is the active window
"""
if not self.api:
return False
return self.api.is_eu_focused()
def is_eu_visible(self) -> bool:
"""Check if Entropia Universe window is visible.
Returns:
True if EU window is visible
"""
if not self.api:
return False
return self.api.is_eu_visible()
def bring_eu_to_front(self) -> bool:
"""Bring Entropia Universe window to front and focus it.
Returns:
True if successful
"""
if not self.api:
return False
return self.api.bring_eu_to_front()
# ========== Clipboard Methods ==========
def copy_to_clipboard(self, text: str) -> bool:
"""Copy text to system clipboard.
Args:
text: Text to copy
Returns:
True if successful
"""
if not self.api:
return False
return self.api.copy_to_clipboard(text)
def paste_from_clipboard(self) -> str:
"""Paste text from system clipboard.
Returns:
Clipboard content or empty string
"""
if not self.api:
return ""
return self.api.paste_from_clipboard()
# ========== Notification Methods ==========
def notify(self, title: str, message: str,
notification_type: str = 'info',
sound: bool = False,
duration_ms: int = 5000) -> str:
"""Show a toast notification.
Args:
title: Notification title
message: Notification message
notification_type: 'info', 'warning', 'error', or 'success'
sound: Play notification sound
duration_ms: How long to show notification
Returns:
Notification ID
"""
if not self.api:
return ""
return self.api.show_notification(title, message, duration_ms, sound)
def notify_info(self, title: str, message: str, sound: bool = False) -> str:
"""Show info notification (convenience method)."""
return self.notify(title, message, 'info', sound)
def notify_success(self, title: str, message: str, sound: bool = False) -> str:
"""Show success notification (convenience method)."""
return self.notify(title, message, 'success', sound)
def notify_warning(self, title: str, message: str, sound: bool = False) -> str:
"""Show warning notification (convenience method)."""
return self.notify(title, message, 'warning', sound)
def notify_error(self, title: str, message: str, sound: bool = True) -> str:
"""Show error notification (convenience method)."""
return self.notify(title, message, 'error', sound)
# ========== Settings Methods ==========
def get_setting(self, key: str, default: Any = None) -> Any:
"""Get a global EU-Utility setting.
Args:
key: Setting key
default: Default value if not set
Returns:
Setting value
"""
if not self.api:
return default
# This would get from global settings
return default
def set_setting(self, key: str, value: Any) -> bool:
"""Set a global EU-Utility setting.
Args:
key: Setting key
value: Value to set
Returns:
True if saved
"""
if not self.api:
return False
return False
# ========== Logging Methods ==========
def log_debug(self, message: str) -> None:
"""Log debug message (development only)."""
print(f"[DEBUG][{self.name}] {message}")
def log_info(self, message: str) -> None:
"""Log info message."""
print(f"[INFO][{self.name}] {message}")
def log_warning(self, message: str) -> None:
"""Log warning message."""
print(f"[WARNING][{self.name}] {message}")
def log_error(self, message: str) -> None:
"""Log error message."""
print(f"[ERROR][{self.name}] {message}")
# Re-export for convenience
__all__ = ['BasePlugin']