1194 lines
38 KiB
Python
1194 lines
38 KiB
Python
"""
|
|
EU-Utility - Plugin Base Class
|
|
|
|
Defines the interface that all plugins must implement.
|
|
Includes PluginAPI integration for cross-plugin communication.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Optional, Dict, Any, TYPE_CHECKING, Callable, List, Type
|
|
|
|
if TYPE_CHECKING:
|
|
from core.overlay_window import OverlayWindow
|
|
from core.plugin_api import PluginAPI, APIEndpoint, APIType
|
|
from core.event_bus import BaseEvent, EventCategory
|
|
|
|
|
|
class BasePlugin(ABC):
|
|
"""Base class for all EU-Utility plugins.
|
|
|
|
To define hotkeys for your plugin, use either:
|
|
|
|
1. Legacy single hotkey (simple toggle):
|
|
hotkey = "ctrl+shift+n"
|
|
|
|
2. New multi-hotkey format (recommended):
|
|
hotkeys = [
|
|
{
|
|
'action': 'toggle', # Unique action identifier
|
|
'description': 'Toggle My Plugin', # Display name in settings
|
|
'default': 'ctrl+shift+m', # Default hotkey combination
|
|
'config_key': 'myplugin_toggle' # Settings key (optional)
|
|
},
|
|
{
|
|
'action': 'quick_action',
|
|
'description': 'Quick Scan',
|
|
'default': 'ctrl+shift+s',
|
|
}
|
|
]
|
|
"""
|
|
|
|
# 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 # Legacy single hotkey (e.g., "ctrl+shift+n")
|
|
hotkeys: Optional[List[Dict[str, str]]] = None # New multi-hotkey format
|
|
enabled: bool = True
|
|
|
|
# Dependencies - override in subclass
|
|
# Format: {
|
|
# 'pip': ['package1', 'package2>=1.0'],
|
|
# 'plugins': ['plugin_id1', 'plugin_id2'], # Other plugins this plugin requires
|
|
# 'optional': {'package3': 'description'}
|
|
# }
|
|
dependencies: Dict[str, Any] = {}
|
|
|
|
def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]):
|
|
self.overlay = overlay_window
|
|
self.config = config
|
|
self._ui = None
|
|
self._api_registered = False
|
|
self._plugin_id = 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."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_ui(self) -> Any:
|
|
"""Return the plugin's UI widget (QWidget)."""
|
|
return None
|
|
|
|
def on_show(self) -> None:
|
|
"""Called when overlay becomes visible."""
|
|
pass
|
|
|
|
def on_hide(self) -> None:
|
|
"""Called when overlay is hidden."""
|
|
pass
|
|
|
|
def on_hotkey(self) -> None:
|
|
"""Called when plugin's hotkey is pressed."""
|
|
pass
|
|
|
|
def shutdown(self) -> None:
|
|
"""Called when app is closing. Cleanup resources."""
|
|
# Unregister APIs
|
|
if self.api and self._api_registered:
|
|
self.api.unregister_api(self._plugin_id)
|
|
|
|
# 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."""
|
|
return self.config.get(key, default)
|
|
|
|
def set_config(self, key: str, value: Any) -> None:
|
|
"""Set a config value."""
|
|
self.config[key] = value
|
|
|
|
# ========== API Methods ==========
|
|
|
|
def register_api(self, name: str, handler: Callable, api_type: 'APIType' = None, description: str = "") -> bool:
|
|
"""Register an API endpoint for other plugins to use.
|
|
|
|
Example:
|
|
self.register_api(
|
|
"scan_window",
|
|
self.scan_window,
|
|
APIType.OCR,
|
|
"Scan game window and return text"
|
|
)
|
|
"""
|
|
if not self.api:
|
|
print(f"[{self.name}] API not available")
|
|
return False
|
|
|
|
try:
|
|
from core.plugin_api import APIEndpoint, APIType
|
|
|
|
if api_type is None:
|
|
api_type = APIType.UTILITY
|
|
|
|
endpoint = APIEndpoint(
|
|
name=name,
|
|
api_type=api_type,
|
|
description=description,
|
|
handler=handler,
|
|
plugin_id=self._plugin_id,
|
|
version=self.version
|
|
)
|
|
|
|
success = self.api.register_api(endpoint)
|
|
if success:
|
|
self._api_registered = True
|
|
return success
|
|
|
|
except Exception as e:
|
|
print(f"[{self.name}] Failed to register API: {e}")
|
|
return False
|
|
|
|
def call_api(self, plugin_id: str, api_name: str, *args, **kwargs) -> Any:
|
|
"""Call another plugin's API.
|
|
|
|
Example:
|
|
# Call Game Reader's OCR API
|
|
result = self.call_api("plugins.game_reader.plugin", "capture_screen")
|
|
"""
|
|
if not self.api:
|
|
raise RuntimeError("API not available")
|
|
|
|
return self.api.call_api(plugin_id, api_name, *args, **kwargs)
|
|
|
|
def find_apis(self, api_type: 'APIType' = None) -> list:
|
|
"""Find available APIs from other plugins."""
|
|
if not self.api:
|
|
return []
|
|
|
|
return self.api.find_apis(api_type)
|
|
|
|
# ========== Shared Services ==========
|
|
|
|
def ocr_capture(self, region: tuple = None) -> Dict[str, Any]:
|
|
"""Capture screen and perform OCR.
|
|
|
|
Returns:
|
|
{'text': str, 'confidence': float, 'raw_results': list}
|
|
"""
|
|
if not self.api:
|
|
return {"text": "", "confidence": 0, "error": "API not available"}
|
|
|
|
return self.api.ocr_capture(region)
|
|
|
|
# ========== Screenshot Service Methods ==========
|
|
|
|
def capture_screen(self, full_screen: bool = True):
|
|
"""Capture screenshot.
|
|
|
|
Args:
|
|
full_screen: If True, capture entire screen
|
|
|
|
Returns:
|
|
PIL Image object
|
|
|
|
Example:
|
|
# Capture full screen
|
|
screenshot = self.capture_screen()
|
|
|
|
# Capture specific region
|
|
region = self.capture_region(100, 100, 800, 600)
|
|
"""
|
|
if not self.api:
|
|
raise RuntimeError("API not available")
|
|
|
|
return self.api.capture_screen(full_screen)
|
|
|
|
def capture_region(self, x: int, y: int, width: int, height: int):
|
|
"""Capture specific screen region.
|
|
|
|
Args:
|
|
x: Left coordinate
|
|
y: Top coordinate
|
|
width: Region width
|
|
height: Region height
|
|
|
|
Returns:
|
|
PIL Image object
|
|
|
|
Example:
|
|
# Capture a 400x200 region starting at (100, 100)
|
|
image = self.capture_region(100, 100, 400, 200)
|
|
"""
|
|
if not self.api:
|
|
raise RuntimeError("API not available")
|
|
|
|
return self.api.capture_region(x, y, width, height)
|
|
|
|
def get_last_screenshot(self):
|
|
"""Get the most recent screenshot.
|
|
|
|
Returns:
|
|
PIL Image or None if no screenshots taken yet
|
|
"""
|
|
if not self.api:
|
|
return None
|
|
|
|
return self.api.get_last_screenshot()
|
|
|
|
def read_log(self, lines: int = 50, filter_text: str = None) -> list:
|
|
"""Read recent game log lines."""
|
|
if not self.api:
|
|
return []
|
|
|
|
return self.api.read_log(lines, filter_text)
|
|
|
|
def get_shared_data(self, key: str, default=None):
|
|
"""Get shared data from other plugins."""
|
|
if not self.api:
|
|
return default
|
|
|
|
return self.api.get_data(key, default)
|
|
|
|
def set_shared_data(self, key: str, value: Any):
|
|
"""Set shared data for other plugins."""
|
|
if self.api:
|
|
self.api.set_data(key, value)
|
|
|
|
# ========== Legacy Event System ==========
|
|
|
|
def publish_event(self, event_type: str, data: Dict[str, Any]):
|
|
"""Publish an event for other plugins to consume (legacy)."""
|
|
if self.api:
|
|
self.api.publish_event(event_type, data)
|
|
|
|
def subscribe(self, event_type: str, callback: Callable):
|
|
"""Subscribe to events from other plugins (legacy)."""
|
|
if self.api:
|
|
self.api.subscribe(event_type, callback)
|
|
|
|
# ========== Enhanced Typed Event System ==========
|
|
|
|
def publish_typed(self, event: 'BaseEvent') -> None:
|
|
"""
|
|
Publish a typed event to the Event Bus.
|
|
|
|
Args:
|
|
event: A typed event instance (SkillGainEvent, LootEvent, etc.)
|
|
|
|
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
|
|
))
|
|
"""
|
|
if self.api:
|
|
self.api.publish_typed(event)
|
|
|
|
def subscribe_typed(
|
|
self,
|
|
event_class: Type['BaseEvent'],
|
|
callback: Callable,
|
|
**filter_kwargs
|
|
) -> 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
|
|
- min_damage: Minimum damage threshold
|
|
- max_damage: Maximum damage threshold
|
|
- mob_types: List of mob names to filter
|
|
- skill_names: List of skill names to filter
|
|
- sources: List of event sources to filter
|
|
- replay_last: Number of recent events to replay
|
|
- predicate: Custom filter function
|
|
|
|
Returns:
|
|
Subscription ID (store this to unsubscribe later)
|
|
|
|
Example:
|
|
from core.event_bus import DamageEvent
|
|
|
|
# Subscribe to all damage events
|
|
self.sub_id = self.subscribe_typed(DamageEvent, self.on_damage)
|
|
|
|
# Subscribe to high damage events only
|
|
self.sub_id = self.subscribe_typed(
|
|
DamageEvent,
|
|
self.on_big_hit,
|
|
min_damage=100
|
|
)
|
|
|
|
# Subscribe with replay
|
|
self.sub_id = self.subscribe_typed(
|
|
SkillGainEvent,
|
|
self.on_skill_gain,
|
|
replay_last=10
|
|
)
|
|
"""
|
|
if not self.api:
|
|
print(f"[{self.name}] API not available for event subscription")
|
|
return ""
|
|
|
|
sub_id = self.api.subscribe_typed(event_class, callback, **filter_kwargs)
|
|
if sub_id:
|
|
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 not self.api:
|
|
return False
|
|
|
|
result = self.api.unsubscribe_typed(subscription_id)
|
|
if result and subscription_id in self._event_subscriptions:
|
|
self._event_subscriptions.remove(subscription_id)
|
|
return result
|
|
|
|
def unsubscribe_all_typed(self) -> None:
|
|
"""Unsubscribe from all typed event subscriptions."""
|
|
if not self.api:
|
|
return
|
|
|
|
for sub_id in self._event_subscriptions[:]: # Copy list to avoid modification during iteration
|
|
self.api.unsubscribe_typed(sub_id)
|
|
self._event_subscriptions.clear()
|
|
|
|
def get_recent_events(
|
|
self,
|
|
event_type: Type['BaseEvent'] = None,
|
|
count: int = 100,
|
|
category: 'EventCategory' = 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
|
|
|
|
Example:
|
|
from core.event_bus import LootEvent
|
|
|
|
# Get last 20 loot events
|
|
recent_loot = self.get_recent_events(LootEvent, 20)
|
|
"""
|
|
if not self.api:
|
|
return []
|
|
|
|
return self.api.get_recent_events(event_type, count, category)
|
|
|
|
def get_event_stats(self) -> Dict[str, Any]:
|
|
"""
|
|
Get Event Bus statistics.
|
|
|
|
Returns:
|
|
Dict with event bus statistics
|
|
"""
|
|
if not self.api:
|
|
return {}
|
|
|
|
return self.api.get_event_stats()
|
|
|
|
# ========== Utility Methods ==========
|
|
|
|
def format_ped(self, value: float) -> str:
|
|
"""Format PED value."""
|
|
if self.api:
|
|
return self.api.format_ped(value)
|
|
return f"{value:.2f} PED"
|
|
|
|
def format_pec(self, value: float) -> str:
|
|
"""Format PEC value."""
|
|
if self.api:
|
|
return self.api.format_pec(value)
|
|
return f"{value:.0f} PEC"
|
|
|
|
def calculate_dpp(self, damage: float, ammo: int, decay: float) -> float:
|
|
"""Calculate Damage Per PEC."""
|
|
if self.api:
|
|
return self.api.calculate_dpp(damage, ammo, decay)
|
|
|
|
# Fallback calculation
|
|
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."""
|
|
if self.api:
|
|
return self.api.calculate_markup(price, tt)
|
|
|
|
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 ('global', 'hof', 'skill_gain', 'alert', 'error')
|
|
or path to file
|
|
blocking: If True, wait for sound to complete (default: False)
|
|
|
|
Returns:
|
|
True if sound was queued/played, False on error or if muted
|
|
|
|
Examples:
|
|
# Play predefined sounds
|
|
self.play_sound('hof')
|
|
self.play_sound('skill_gain')
|
|
self.play_sound('alert')
|
|
|
|
# Play custom sound file
|
|
self.play_sound('/path/to/custom.wav')
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
return self.api.play_sound(filename_or_key, blocking)
|
|
|
|
def set_volume(self, volume: float) -> None:
|
|
"""Set global audio volume.
|
|
|
|
Args:
|
|
volume: Volume level from 0.0 (mute) to 1.0 (max)
|
|
"""
|
|
if self.api:
|
|
self.api.set_volume(volume)
|
|
|
|
def get_volume(self) -> float:
|
|
"""Get current audio volume.
|
|
|
|
Returns:
|
|
Current volume level (0.0 to 1.0)
|
|
"""
|
|
if not self.api:
|
|
return 0.0
|
|
|
|
return self.api.get_volume()
|
|
|
|
def mute(self) -> None:
|
|
"""Mute all audio."""
|
|
if self.api:
|
|
self.api.mute_audio()
|
|
|
|
def unmute(self) -> None:
|
|
"""Unmute audio."""
|
|
if self.api:
|
|
self.api.unmute_audio()
|
|
|
|
def toggle_mute(self) -> bool:
|
|
"""Toggle audio mute state.
|
|
|
|
Returns:
|
|
New muted state (True if now muted)
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
return self.api.toggle_mute_audio()
|
|
|
|
def is_muted(self) -> bool:
|
|
"""Check if audio is muted.
|
|
|
|
Returns:
|
|
True if audio is muted
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
return self.api.is_audio_muted()
|
|
|
|
def is_audio_available(self) -> bool:
|
|
"""Check if audio service is available.
|
|
|
|
Returns:
|
|
True if audio backend is initialized and working
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
return self.api.is_audio_available()
|
|
|
|
# ========== Background Task Methods ==========
|
|
|
|
def run_in_background(self, func: Callable, *args,
|
|
priority: str = 'normal',
|
|
on_complete: Callable = None,
|
|
on_error: Callable = None,
|
|
**kwargs) -> str:
|
|
"""Run a function in a background thread.
|
|
|
|
Use this instead of creating your own QThreads.
|
|
|
|
Args:
|
|
func: Function to execute in background
|
|
*args: Positional arguments for the function
|
|
priority: 'high', 'normal', or 'low' (default: 'normal')
|
|
on_complete: Called with result when task completes successfully
|
|
on_error: Called with exception when task fails
|
|
**kwargs: Keyword arguments for the function
|
|
|
|
Returns:
|
|
Task ID for tracking/cancellation
|
|
|
|
Example:
|
|
def heavy_calculation(data):
|
|
return process(data)
|
|
|
|
def on_done(result):
|
|
self.update_ui(result)
|
|
|
|
def on_fail(error):
|
|
self.show_error(str(error))
|
|
|
|
task_id = self.run_in_background(
|
|
heavy_calculation,
|
|
large_dataset,
|
|
priority='high',
|
|
on_complete=on_done,
|
|
on_error=on_fail
|
|
)
|
|
|
|
# Or with decorator style:
|
|
@self.run_in_background
|
|
def fetch_remote_data():
|
|
return requests.get(url).json()
|
|
"""
|
|
if not self.api:
|
|
raise RuntimeError("API not available")
|
|
|
|
return self.api.run_in_background(
|
|
func, *args,
|
|
priority=priority,
|
|
on_complete=on_complete,
|
|
on_error=on_error,
|
|
**kwargs
|
|
)
|
|
|
|
def schedule_task(self, delay_ms: int, func: Callable, *args,
|
|
priority: str = 'normal',
|
|
on_complete: Callable = None,
|
|
on_error: Callable = None,
|
|
periodic: bool = False,
|
|
interval_ms: int = None,
|
|
**kwargs) -> str:
|
|
"""Schedule a task for delayed or periodic execution.
|
|
|
|
Args:
|
|
delay_ms: Milliseconds to wait before first execution
|
|
func: Function to execute
|
|
*args: Positional arguments
|
|
priority: 'high', 'normal', or 'low'
|
|
on_complete: Called with result after each execution
|
|
on_error: Called with exception if execution fails
|
|
periodic: If True, repeat execution at interval_ms
|
|
interval_ms: Milliseconds between periodic executions
|
|
**kwargs: Keyword arguments
|
|
|
|
Returns:
|
|
Task ID for tracking/cancellation
|
|
|
|
Example:
|
|
# One-time delayed execution
|
|
task_id = self.schedule_task(
|
|
5000, # 5 seconds
|
|
lambda: print("Hello after delay!")
|
|
)
|
|
|
|
# Periodic data refresh (every 30 seconds)
|
|
self.schedule_task(
|
|
0, # Start immediately
|
|
self.refresh_data,
|
|
periodic=True,
|
|
interval_ms=30000,
|
|
on_complete=lambda data: self.update_display(data)
|
|
)
|
|
"""
|
|
if not self.api:
|
|
raise RuntimeError("API not available")
|
|
|
|
return self.api.schedule_task(
|
|
delay_ms, func, *args,
|
|
priority=priority,
|
|
on_complete=on_complete,
|
|
on_error=on_error,
|
|
periodic=periodic,
|
|
interval_ms=interval_ms,
|
|
**kwargs
|
|
)
|
|
|
|
def cancel_task(self, task_id: str) -> bool:
|
|
"""Cancel a pending or running task.
|
|
|
|
Args:
|
|
task_id: Task ID returned by run_in_background or schedule_task
|
|
|
|
Returns:
|
|
True if task was cancelled, False if not found or already done
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
return self.api.cancel_task(task_id)
|
|
|
|
def connect_task_signals(self,
|
|
on_completed: Callable = None,
|
|
on_failed: Callable = None,
|
|
on_started: Callable = None,
|
|
on_cancelled: Callable = None) -> bool:
|
|
"""Connect to task status signals for UI updates.
|
|
|
|
Connects Qt signals so UI updates from background threads are thread-safe.
|
|
|
|
Args:
|
|
on_completed: Called with (task_id, result) when tasks complete
|
|
on_failed: Called with (task_id, error_message) when tasks fail
|
|
on_started: Called with (task_id) when tasks start
|
|
on_cancelled: Called with (task_id) when tasks are cancelled
|
|
|
|
Returns:
|
|
True if signals were connected
|
|
|
|
Example:
|
|
class MyPlugin(BasePlugin):
|
|
def initialize(self):
|
|
# Connect task signals for UI updates
|
|
self.connect_task_signals(
|
|
on_completed=self._on_task_done,
|
|
on_failed=self._on_task_error
|
|
)
|
|
|
|
def _on_task_done(self, task_id, result):
|
|
self.status_label.setText(f"Task {task_id}: Done!")
|
|
|
|
def _on_task_error(self, task_id, error):
|
|
self.status_label.setText(f"Task {task_id} failed: {error}")
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
connected = False
|
|
|
|
if on_completed:
|
|
connected = self.api.connect_task_signal('completed', on_completed) or connected
|
|
if on_failed:
|
|
connected = self.api.connect_task_signal('failed', on_failed) or connected
|
|
if on_started:
|
|
connected = self.api.connect_task_signal('started', on_started) or connected
|
|
if on_cancelled:
|
|
connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected
|
|
|
|
return connected
|
|
|
|
# ========== 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 to search. Valid types:
|
|
- items, weapons, armors
|
|
- mobs, pets
|
|
- blueprints, materials
|
|
- locations, teleporters, shops, planets, areas
|
|
- skills
|
|
- enhancers, medicaltools, finders, excavators, refiners
|
|
- vehicles, decorations, furniture
|
|
- storagecontainers, strongboxes, vendors
|
|
limit: Maximum number of results (default: 20, max: 100)
|
|
|
|
Returns:
|
|
List of search result dictionaries
|
|
|
|
Example:
|
|
# Search for weapons
|
|
results = self.nexus_search("ArMatrix", entity_type="weapons")
|
|
|
|
# Search for mobs
|
|
mobs = self.nexus_search("Atrox", entity_type="mobs")
|
|
|
|
# Search for locations
|
|
locations = self.nexus_search("Fort", entity_type="locations")
|
|
|
|
# Process results
|
|
for item in results:
|
|
print(f"{item['name']} ({item['type']})")
|
|
"""
|
|
if not self.api:
|
|
return []
|
|
|
|
return self.api.nexus_search(query, entity_type, 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 (e.g., "armatrix_lp-35")
|
|
|
|
Returns:
|
|
Dictionary with item details, or None if not found
|
|
|
|
Example:
|
|
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', 'N/A')}")
|
|
print(f"Range: {details.get('range', 'N/A')}m")
|
|
"""
|
|
if not self.api:
|
|
return None
|
|
|
|
return self.api.nexus_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 found
|
|
|
|
Example:
|
|
market = self.nexus_get_market_data("armatrix_lp-35")
|
|
if market:
|
|
print(f"Current markup: {market['current_markup']:.1f}%")
|
|
print(f"7-day avg: {market['avg_markup_7d']:.1f}%")
|
|
print(f"24h Volume: {market['volume_24h']}")
|
|
|
|
# Check orders
|
|
for buy in market.get('buy_orders', [])[:5]:
|
|
print(f"Buy: {buy['price']} PED x {buy['quantity']}")
|
|
"""
|
|
if not self.api:
|
|
return None
|
|
|
|
return self.api.nexus_get_market_data(item_id)
|
|
|
|
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 self.api.nexus_is_available()
|
|
|
|
# ========== HTTP Client Methods ==========
|
|
|
|
def http_get(self, url: str, cache_ttl: int = 300, headers: Dict[str, str] = None, **kwargs) -> Dict[str, Any]:
|
|
"""Make an HTTP GET request with caching.
|
|
|
|
Args:
|
|
url: The URL to fetch
|
|
cache_ttl: Cache TTL in seconds (default: 300 = 5 minutes)
|
|
headers: Additional headers
|
|
**kwargs: Additional arguments
|
|
|
|
Returns:
|
|
Dict with 'status_code', 'headers', 'content', 'text', 'json', 'from_cache'
|
|
|
|
Example:
|
|
response = self.http_get(
|
|
"https://api.example.com/data",
|
|
cache_ttl=600,
|
|
headers={'Accept': 'application/json'}
|
|
)
|
|
if response['status_code'] == 200:
|
|
data = response['json']
|
|
"""
|
|
if not self.api:
|
|
raise RuntimeError("API not available")
|
|
|
|
# Get HTTP client from services
|
|
http_client = self.api.services.get('http')
|
|
if not http_client:
|
|
raise RuntimeError("HTTP client not available")
|
|
|
|
return http_client.get(url, cache_ttl=cache_ttl, headers=headers, **kwargs)
|
|
|
|
# ========== 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
|
|
|
|
Example:
|
|
# Save plugin settings
|
|
self.save_data("settings", {"theme": "dark", "volume": 0.8})
|
|
|
|
# Save user progress
|
|
self.save_data("total_loot", {"ped": 150.50, "items": 42})
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
data_store = self.api.services.get('data_store')
|
|
if not data_store:
|
|
print(f"[{self.name}] DataStore not available")
|
|
return False
|
|
|
|
return data_store.save(self._plugin_id, 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
|
|
|
|
Example:
|
|
# Load settings with defaults
|
|
settings = self.load_data("settings", {"theme": "light", "volume": 1.0})
|
|
|
|
# Load progress
|
|
progress = self.load_data("total_loot", {"ped": 0, "items": 0})
|
|
print(f"Total loot: {progress['ped']} PED")
|
|
"""
|
|
if not self.api:
|
|
return default
|
|
|
|
data_store = self.api.services.get('data_store')
|
|
if not data_store:
|
|
return default
|
|
|
|
return data_store.load(self._plugin_id, 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), False on error
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
data_store = self.api.services.get('data_store')
|
|
if not data_store:
|
|
return False
|
|
|
|
return data_store.delete(self._plugin_id, 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 []
|
|
|
|
data_store = self.api.services.get('data_store')
|
|
if not data_store:
|
|
return []
|
|
|
|
return data_store.get_all_keys(self._plugin_id)
|
|
|
|
# ========== 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 found:
|
|
- handle: Window handle (int)
|
|
- title: Window title (str)
|
|
- rect: (left, top, right, bottom) tuple
|
|
- width: Window width (int)
|
|
- height: Window height (int)
|
|
- visible: Whether window is visible (bool)
|
|
|
|
Example:
|
|
window = self.get_eu_window()
|
|
if window:
|
|
print(f"EU window: {window['width']}x{window['height']}")
|
|
print(f"Position: {window['rect']}")
|
|
"""
|
|
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
|
|
|
|
Example:
|
|
if self.is_eu_focused():
|
|
# Safe to capture screenshot
|
|
screenshot = self.capture_screen()
|
|
"""
|
|
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 (not minimized)
|
|
"""
|
|
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
|
|
|
|
Example:
|
|
# Copy coordinates
|
|
self.copy_to_clipboard("12345, 67890")
|
|
|
|
# Copy calculation result
|
|
result = self.calculate_dpp(50, 100, 2.5)
|
|
self.copy_to_clipboard(f"DPP: {result:.2f}")
|
|
"""
|
|
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
|
|
|
|
Example:
|
|
# Get pasted coordinates
|
|
coords = self.paste_from_clipboard()
|
|
if coords:
|
|
x, y = map(int, coords.split(","))
|
|
"""
|
|
if not self.api:
|
|
return ""
|
|
|
|
return self.api.paste_from_clipboard()
|
|
|
|
def get_clipboard_history(self, limit: int = 10) -> List[Dict[str, str]]:
|
|
"""Get recent clipboard history.
|
|
|
|
Args:
|
|
limit: Maximum number of entries to return
|
|
|
|
Returns:
|
|
List of clipboard entries with 'text', 'timestamp', 'source'
|
|
"""
|
|
if not self.api:
|
|
return []
|
|
|
|
return self.api.get_clipboard_history(limit)
|
|
|
|
# ========== 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 (default: 5000ms)
|
|
|
|
Returns:
|
|
Notification ID
|
|
|
|
Example:
|
|
# Info notification
|
|
self.notify("Session Started", "Tracking loot...")
|
|
|
|
# Success with sound
|
|
self.notify("Global!", "You received 150 PED", notification_type='success', sound=True)
|
|
|
|
# Warning
|
|
self.notify("Low Ammo", "Only 100 shots remaining", notification_type='warning')
|
|
|
|
# Error
|
|
self.notify("Connection Failed", "Check your internet", notification_type='error', sound=True)
|
|
"""
|
|
if not self.api:
|
|
return ""
|
|
|
|
return self.api.notify(title, message, notification_type, sound, duration_ms)
|
|
|
|
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)
|
|
|
|
def close_notification(self, notification_id: str) -> bool:
|
|
"""Close a specific notification.
|
|
|
|
Args:
|
|
notification_id: ID returned by notify()
|
|
|
|
Returns:
|
|
True if closed
|
|
"""
|
|
if not self.api:
|
|
return False
|
|
|
|
return self.api.close_notification(notification_id)
|
|
|
|
def close_all_notifications(self) -> None:
|
|
"""Close all visible notifications."""
|
|
if not self.api:
|
|
return
|
|
|
|
self.api.close_all_notifications()
|
|
|
|
# ========== Settings Methods ==========
|
|
|
|
def get_setting(self, key: str, default: Any = None) -> Any:
|
|
"""Get a global EU-Utility setting.
|
|
|
|
These are user preferences that apply across all plugins.
|
|
|
|
Args:
|
|
key: Setting key
|
|
default: Default value if not set
|
|
|
|
Returns:
|
|
Setting value
|
|
|
|
Available settings:
|
|
- theme: 'dark', 'light', or 'auto'
|
|
- overlay_opacity: float 0.0-1.0
|
|
- icon_size: 'small', 'medium', 'large'
|
|
- minimize_to_tray: bool
|
|
- show_tooltips: bool
|
|
- global_hotkeys: Dict of hotkey mappings
|
|
"""
|
|
if not self.api:
|
|
return default
|
|
|
|
settings = self.api.services.get('settings')
|
|
if not settings:
|
|
return default
|
|
|
|
return settings.get(key, 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
|
|
|
|
settings = self.api.services.get('settings')
|
|
if not settings:
|
|
return False
|
|
|
|
return settings.set(key, value)
|
|
|
|
# ========== 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}")
|