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