660 lines
20 KiB
Python
660 lines
20 KiB
Python
"""
|
|
EU-Utility Premium - Event Bus
|
|
===============================
|
|
|
|
High-performance async event system for inter-plugin communication.
|
|
|
|
Features:
|
|
- Async/await support
|
|
- Event prioritization
|
|
- Event filtering and routing
|
|
- Buffered events for high-throughput scenarios
|
|
- Type-safe event definitions
|
|
|
|
Example:
|
|
from premium.core.event_bus import EventBus, Event, EventPriority
|
|
|
|
bus = EventBus()
|
|
|
|
# Subscribe to events
|
|
@bus.on('game.loot')
|
|
async def handle_loot(event: Event):
|
|
print(f"Got loot: {event.data}")
|
|
|
|
# Emit events
|
|
bus.emit('game.loot', {'item': 'Angel Scales', 'value': 150.50})
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import inspect
|
|
import logging
|
|
import time
|
|
import traceback
|
|
import weakref
|
|
from abc import ABC, abstractmethod
|
|
from collections import defaultdict, deque
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from queue import Queue
|
|
from threading import Lock
|
|
from typing import (
|
|
Any, Callable, Coroutine, Dict, List, Optional, Set, Type, TypeVar,
|
|
Union, Generic, Protocol, runtime_checkable
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# EVENT PRIORITY
|
|
# =============================================================================
|
|
|
|
class EventPriority(Enum):
|
|
"""Priority levels for event handlers.
|
|
|
|
Higher priority handlers are called first.
|
|
"""
|
|
CRITICAL = 100 # System-critical events (error handling, shutdown)
|
|
HIGH = 75 # Important game events (loot, globals)
|
|
NORMAL = 50 # Standard events
|
|
LOW = 25 # Background tasks
|
|
BACKGROUND = 10 # Analytics, logging
|
|
|
|
|
|
# =============================================================================
|
|
# EVENT CLASS
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class Event:
|
|
"""Represents an event in the system.
|
|
|
|
Attributes:
|
|
type: Event type/category
|
|
data: Event payload data
|
|
source: Source of the event (plugin_id or system)
|
|
timestamp: When the event was created
|
|
priority: Event priority
|
|
id: Unique event identifier
|
|
"""
|
|
type: str
|
|
data: Dict[str, Any] = field(default_factory=dict)
|
|
source: str = "system"
|
|
timestamp: float = field(default_factory=time.time)
|
|
priority: EventPriority = EventPriority.NORMAL
|
|
id: str = field(default_factory=lambda: f"evt_{int(time.time()*1000)}_{id(Event) % 10000}")
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get a value from event data."""
|
|
return self.data.get(key, default)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert event to dictionary."""
|
|
return {
|
|
'id': self.id,
|
|
'type': self.type,
|
|
'data': self.data,
|
|
'source': self.source,
|
|
'timestamp': self.timestamp,
|
|
'priority': self.priority.name,
|
|
}
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
event_type: str,
|
|
data: Dict[str, Any],
|
|
source: str = "system",
|
|
priority: EventPriority = EventPriority.NORMAL
|
|
) -> Event:
|
|
"""Create a new event."""
|
|
return cls(
|
|
type=event_type,
|
|
data=data,
|
|
source=source,
|
|
priority=priority
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# EVENT FILTER
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class EventFilter:
|
|
"""Filter for event subscription.
|
|
|
|
Allows subscribers to receive only events matching specific criteria.
|
|
|
|
Example:
|
|
filter = EventFilter(
|
|
event_types=['game.loot', 'game.skill'],
|
|
sources=['game_client'],
|
|
min_priority=EventPriority.HIGH
|
|
)
|
|
"""
|
|
event_types: Optional[Set[str]] = None
|
|
sources: Optional[Set[str]] = None
|
|
min_priority: Optional[EventPriority] = None
|
|
data_filter: Optional[Callable[[Dict[str, Any]], bool]] = None
|
|
|
|
def matches(self, event: Event) -> bool:
|
|
"""Check if an event matches this filter."""
|
|
if self.event_types and event.type not in self.event_types:
|
|
return False
|
|
|
|
if self.sources and event.source not in self.sources:
|
|
return False
|
|
|
|
if self.min_priority and event.priority.value < self.min_priority.value:
|
|
return False
|
|
|
|
if self.data_filter and not self.data_filter(event.data):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
# =============================================================================
|
|
# SUBSCRIPTION
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class Subscription:
|
|
"""Represents an event subscription."""
|
|
id: str
|
|
event_types: Set[str]
|
|
callback: Callable[[Event], Any]
|
|
filter: Optional[EventFilter]
|
|
priority: EventPriority
|
|
once: bool
|
|
|
|
def __hash__(self) -> int:
|
|
"""Make subscription hashable (use id only)."""
|
|
return hash(self.id)
|
|
|
|
def __eq__(self, other) -> bool:
|
|
"""Compare subscriptions by id."""
|
|
if not isinstance(other, Subscription):
|
|
return False
|
|
return self.id == other.id
|
|
|
|
def matches(self, event: Event) -> bool:
|
|
"""Check if this subscription matches an event."""
|
|
if self.event_types and event.type not in self.event_types:
|
|
return False
|
|
|
|
if self.filter and not self.filter.matches(event):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
# =============================================================================
|
|
# EVENT BUS
|
|
# =============================================================================
|
|
|
|
HandlerType = Callable[[Event], Any]
|
|
|
|
|
|
class EventBus:
|
|
"""High-performance async event bus.
|
|
|
|
Features:
|
|
- Async and sync handler support
|
|
- Priority-based execution order
|
|
- Event filtering
|
|
- Buffered event queues
|
|
- Wildcard subscriptions
|
|
|
|
Example:
|
|
bus = EventBus()
|
|
|
|
# Sync handler
|
|
@bus.on('game.loot')
|
|
def handle_loot(event):
|
|
print(f"Loot: {event.data}")
|
|
|
|
# Async handler
|
|
@bus.on('game.global', priority=EventPriority.HIGH)
|
|
async def handle_global(event):
|
|
await notify_discord(event.data)
|
|
|
|
# Emit event
|
|
bus.emit('game.loot', {'item': 'Uber Item', 'value': 1000})
|
|
"""
|
|
|
|
def __init__(self, max_queue_size: int = 10000):
|
|
"""Initialize event bus.
|
|
|
|
Args:
|
|
max_queue_size: Maximum events in queue before dropping
|
|
"""
|
|
self._subscribers: Dict[str, Set[Subscription]] = defaultdict(set)
|
|
self._wildcard_subscribers: Set[Subscription] = set()
|
|
self._queue: deque = deque(maxlen=max_queue_size)
|
|
self._logger = logging.getLogger("EventBus")
|
|
self._sub_counter = 0
|
|
self._running = False
|
|
self._lock = asyncio.Lock()
|
|
self._event_count = 0
|
|
self._dropped_count = 0
|
|
|
|
# Thread-safe queue for cross-thread event emission
|
|
self._thread_queue: Queue = Queue()
|
|
self._thread_lock = Lock()
|
|
self._main_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
|
|
# ========== Subscription Methods ==========
|
|
|
|
def on(
|
|
self,
|
|
event_type: Union[str, List[str]],
|
|
priority: EventPriority = EventPriority.NORMAL,
|
|
filter: Optional[EventFilter] = None,
|
|
once: bool = False
|
|
) -> Callable[[HandlerType], HandlerType]:
|
|
"""Decorator to subscribe to events.
|
|
|
|
Args:
|
|
event_type: Event type(s) to subscribe to
|
|
priority: Handler priority
|
|
filter: Optional event filter
|
|
once: Remove after first matching event
|
|
|
|
Example:
|
|
@bus.on('game.loot')
|
|
def handle_loot(event):
|
|
print(event.data)
|
|
"""
|
|
def decorator(handler: HandlerType) -> HandlerType:
|
|
self.subscribe(event_type, handler, priority, filter, once)
|
|
return handler
|
|
return decorator
|
|
|
|
def subscribe(
|
|
self,
|
|
event_type: Union[str, List[str]],
|
|
handler: HandlerType,
|
|
priority: EventPriority = EventPriority.NORMAL,
|
|
filter: Optional[EventFilter] = None,
|
|
once: bool = False
|
|
) -> str:
|
|
"""Subscribe to events.
|
|
|
|
Args:
|
|
event_type: Event type(s) to subscribe to, or '*' for all
|
|
handler: Callback function
|
|
priority: Handler priority
|
|
filter: Optional event filter
|
|
once: Remove after first matching event
|
|
|
|
Returns:
|
|
Subscription ID
|
|
"""
|
|
self._sub_counter += 1
|
|
sub_id = f"sub_{self._sub_counter}"
|
|
|
|
# Handle wildcard subscription
|
|
if event_type == '*':
|
|
sub = Subscription(
|
|
id=sub_id,
|
|
event_types=set(),
|
|
callback=handler,
|
|
filter=filter,
|
|
priority=priority,
|
|
once=once
|
|
)
|
|
self._wildcard_subscribers.add(sub)
|
|
return sub_id
|
|
|
|
# Handle single or multiple event types
|
|
if isinstance(event_type, str):
|
|
event_types = {event_type}
|
|
else:
|
|
event_types = set(event_type)
|
|
|
|
sub = Subscription(
|
|
id=sub_id,
|
|
event_types=event_types,
|
|
callback=handler,
|
|
filter=filter,
|
|
priority=priority,
|
|
once=once
|
|
)
|
|
|
|
for et in event_types:
|
|
self._subscribers[et].add(sub)
|
|
|
|
return sub_id
|
|
|
|
def start(self) -> None:
|
|
"""Start the event bus and capture the main event loop.
|
|
|
|
This should be called from the main thread's async context.
|
|
"""
|
|
try:
|
|
self._main_loop = asyncio.get_running_loop()
|
|
self._running = True
|
|
self._logger.info("Event bus started")
|
|
except RuntimeError:
|
|
self._logger.warning("Event bus started without event loop")
|
|
|
|
def stop(self) -> None:
|
|
"""Stop the event bus."""
|
|
self._running = False
|
|
self._main_loop = None
|
|
self._logger.info("Event bus stopped")
|
|
|
|
def unsubscribe(self, subscription_id: str) -> bool:
|
|
"""Unsubscribe from events.
|
|
|
|
Args:
|
|
subscription_id: ID returned by subscribe()
|
|
|
|
Returns:
|
|
True if subscription was found and removed
|
|
"""
|
|
# Check wildcard subscribers
|
|
for sub in self._wildcard_subscribers:
|
|
if sub.id == subscription_id:
|
|
self._wildcard_subscribers.remove(sub)
|
|
return True
|
|
|
|
# Check typed subscribers
|
|
for event_type, subs in self._subscribers.items():
|
|
for sub in list(subs):
|
|
if sub.id == subscription_id:
|
|
subs.remove(sub)
|
|
return True
|
|
|
|
return False
|
|
|
|
def once(
|
|
self,
|
|
event_type: str,
|
|
handler: HandlerType,
|
|
priority: EventPriority = EventPriority.NORMAL,
|
|
timeout: Optional[float] = None
|
|
) -> asyncio.Future:
|
|
"""Subscribe to a single event.
|
|
|
|
Args:
|
|
event_type: Event type to wait for
|
|
handler: Optional handler callback
|
|
priority: Handler priority
|
|
timeout: Optional timeout in seconds
|
|
|
|
Returns:
|
|
Future that resolves with the event
|
|
"""
|
|
future = asyncio.Future()
|
|
|
|
def wrapper(event: Event):
|
|
if handler:
|
|
handler(event)
|
|
if not future.done():
|
|
future.set_result(event)
|
|
|
|
self.subscribe(event_type, wrapper, priority, once=True)
|
|
|
|
if timeout:
|
|
async def timeout_handler():
|
|
await asyncio.sleep(timeout)
|
|
if not future.done():
|
|
future.set_exception(TimeoutError(f"Event {event_type} timed out"))
|
|
|
|
asyncio.create_task(timeout_handler())
|
|
|
|
return future
|
|
|
|
# ========== Event Emission ==========
|
|
|
|
def emit(
|
|
self,
|
|
event_type: str,
|
|
data: Dict[str, Any],
|
|
source: str = "system",
|
|
priority: EventPriority = EventPriority.NORMAL
|
|
) -> Event:
|
|
"""Emit an event.
|
|
|
|
Args:
|
|
event_type: Type of event
|
|
data: Event data
|
|
source: Event source
|
|
priority: Event priority
|
|
|
|
Returns:
|
|
The created event
|
|
"""
|
|
event = Event.create(
|
|
event_type=event_type,
|
|
data=data,
|
|
source=source,
|
|
priority=priority
|
|
)
|
|
|
|
self._queue.append(event)
|
|
self._event_count += 1
|
|
|
|
# Try to process in async context
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
# We're in an async context - safe to create task
|
|
asyncio.create_task(self._process_event(event))
|
|
except RuntimeError:
|
|
# No event loop in this thread - queue for main thread
|
|
with self._thread_lock:
|
|
self._thread_queue.put(event)
|
|
# Try to notify main loop if available
|
|
if self._main_loop and self._main_loop.is_running():
|
|
try:
|
|
self._main_loop.call_soon_threadsafe(
|
|
lambda: asyncio.create_task(self._process_thread_queue())
|
|
)
|
|
except:
|
|
pass
|
|
|
|
return event
|
|
|
|
async def _process_thread_queue(self) -> None:
|
|
"""Process events queued from other threads."""
|
|
while not self._thread_queue.empty():
|
|
try:
|
|
with self._thread_lock:
|
|
if not self._thread_queue.empty():
|
|
event = self._thread_queue.get_nowait()
|
|
await self._process_event(event)
|
|
except:
|
|
break
|
|
|
|
async def emit_async(
|
|
self,
|
|
event_type: str,
|
|
data: Dict[str, Any],
|
|
source: str = "system",
|
|
priority: EventPriority = EventPriority.NORMAL
|
|
) -> Event:
|
|
"""Emit an event asynchronously."""
|
|
event = Event.create(
|
|
event_type=event_type,
|
|
data=data,
|
|
source=source,
|
|
priority=priority
|
|
)
|
|
|
|
await self._process_event(event)
|
|
return event
|
|
|
|
async def _process_event(self, event: Event) -> None:
|
|
"""Process a single event."""
|
|
# Collect all matching handlers
|
|
handlers: List[tuple] = []
|
|
|
|
# Get handlers for specific event type
|
|
for sub in self._subscribers.get(event.type, set()):
|
|
if sub.matches(event):
|
|
handlers.append((sub.priority.value, sub))
|
|
|
|
# Get wildcard handlers
|
|
for sub in self._wildcard_subscribers:
|
|
if sub.filter is None or sub.filter.matches(event):
|
|
handlers.append((sub.priority.value, sub))
|
|
|
|
# Sort by priority (highest first)
|
|
handlers.sort(key=lambda x: -x[0])
|
|
|
|
# Execute handlers
|
|
once_subs = []
|
|
|
|
for _, sub in handlers:
|
|
try:
|
|
if inspect.iscoroutinefunction(sub.callback):
|
|
await sub.callback(event)
|
|
else:
|
|
sub.callback(event)
|
|
|
|
if sub.once:
|
|
once_subs.append(sub)
|
|
|
|
except Exception as e:
|
|
self._logger.error(f"Error in event handler: {e}")
|
|
traceback.print_exc()
|
|
|
|
# Remove once subscriptions
|
|
for sub in once_subs:
|
|
self.unsubscribe(sub.id)
|
|
|
|
# ========== Utility Methods ==========
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all subscriptions and queued events."""
|
|
self._subscribers.clear()
|
|
self._wildcard_subscribers.clear()
|
|
self._queue.clear()
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""Get event bus statistics."""
|
|
return {
|
|
'total_events': self._event_count,
|
|
'dropped_events': self._dropped_count,
|
|
'queue_size': len(self._queue),
|
|
'subscription_count': sum(
|
|
len(subs) for subs in self._subscribers.values()
|
|
),
|
|
'wildcard_subscriptions': len(self._wildcard_subscribers),
|
|
}
|
|
|
|
def wait_for(
|
|
self,
|
|
event_type: str,
|
|
condition: Optional[Callable[[Event], bool]] = None,
|
|
timeout: Optional[float] = None
|
|
) -> asyncio.Future:
|
|
"""Wait for a specific event.
|
|
|
|
Args:
|
|
event_type: Event type to wait for
|
|
condition: Optional condition function
|
|
timeout: Optional timeout in seconds
|
|
|
|
Returns:
|
|
Future that resolves with the matching event
|
|
"""
|
|
future = asyncio.Future()
|
|
|
|
def handler(event: Event):
|
|
if condition and not condition(event):
|
|
return
|
|
if not future.done():
|
|
future.set_result(event)
|
|
return True # Unsubscribe
|
|
|
|
self.subscribe(event_type, handler, once=True)
|
|
|
|
if timeout:
|
|
async def timeout_coro():
|
|
await asyncio.sleep(timeout)
|
|
if not future.done():
|
|
future.set_exception(TimeoutError(f"Timeout waiting for {event_type}"))
|
|
|
|
asyncio.create_task(timeout_coro())
|
|
|
|
return future
|
|
|
|
|
|
# =============================================================================
|
|
# TYPED EVENTS
|
|
# =============================================================================
|
|
|
|
class TypedEventBus(EventBus):
|
|
"""Type-safe event bus with predefined event types."""
|
|
|
|
# Game events
|
|
GAME_CONNECTED = "game.connected"
|
|
GAME_DISCONNECTED = "game.disconnected"
|
|
GAME_FOCUS_CHANGED = "game.focus_changed"
|
|
|
|
# Loot events
|
|
LOOT_RECEIVED = "game.loot"
|
|
GLOBAL_HOF = "game.global"
|
|
|
|
# Skill events
|
|
SKILL_GAIN = "game.skill"
|
|
PROFESSION_GAIN = "game.profession"
|
|
|
|
# Chat events
|
|
CHAT_MESSAGE = "game.chat"
|
|
|
|
# System events
|
|
PLUGIN_LOADED = "system.plugin.loaded"
|
|
PLUGIN_UNLOADED = "system.plugin.unloaded"
|
|
ERROR_OCCURRED = "system.error"
|
|
|
|
def emit_game_connected(self, character_name: str) -> Event:
|
|
"""Emit game connected event."""
|
|
return self.emit(self.GAME_CONNECTED, {
|
|
'character_name': character_name
|
|
})
|
|
|
|
def emit_loot(self, item: str, value: float, quantity: int = 1) -> Event:
|
|
"""Emit loot received event."""
|
|
return self.emit(self.LOOT_RECEIVED, {
|
|
'item': item,
|
|
'value': value,
|
|
'quantity': quantity,
|
|
'timestamp': time.time(),
|
|
})
|
|
|
|
def emit_skill(self, skill_name: str, value: float, gain: float) -> Event:
|
|
"""Emit skill gain event."""
|
|
return self.emit(self.SKILL_GAIN, {
|
|
'skill': skill_name,
|
|
'value': value,
|
|
'gain': gain,
|
|
})
|
|
|
|
def emit_global(self, mob_name: str, value: float, player: str) -> Event:
|
|
"""Emit global/HoF event."""
|
|
return self.emit(self.GLOBAL_HOF, {
|
|
'mob': mob_name,
|
|
'value': value,
|
|
'player': player,
|
|
'is_hof': value >= 1000,
|
|
})
|
|
|
|
|
|
# =============================================================================
|
|
# EXPORTS
|
|
# =============================================================================
|
|
|
|
__all__ = [
|
|
'EventPriority',
|
|
'Event',
|
|
'EventFilter',
|
|
'Subscription',
|
|
'EventBus',
|
|
'TypedEventBus',
|
|
]
|