using System;
using System.Collections.Generic;
using UnityEngine;
namespace PlatypusIdeas.AirPath.Runtime.Events {
/// <summary>
/// ScriptableObject-based event bus for the pathfinding system
/// Clean, modular design without inheritance requirements
/// </summary>
[CreateAssetMenu(fileName = "PathfindingEventBus", menuName = "EventSystem/PathfindingEventBus")]
public class PathfindingEventBus : ScriptableObject {
private static PathfindingEventBus _instance;
private static bool _applicationIsQuitting = false;
/// <summary>
/// Singleton instance of the event bus
/// Set by EventBusUpdateHandler on scene initialization
/// </summary>
public static PathfindingEventBus Instance {
get {
if (_applicationIsQuitting) {
return null;
}
if (!_instance) {
// Capture the stack trace to find what's calling this
Debug.LogError("[EventBus] PathfindingEventBus instance not initialized! " +
"Make sure you have an EventBusUpdateHandler component in your scene " +
"with the PathfindingEventBus asset assigned in the inspector.\n\n" +
"CALLED FROM:\n" + System.Environment.StackTrace);
}
return _instance;
}
}
/// <summary>
/// Initialize the singleton instance - called by EventBusUpdateHandler
/// </summary>
public static void SetInstance(PathfindingEventBus instance) {
if (_instance != null && _instance != instance) {
Debug.LogWarning("[EventBus] Replacing existing EventBus instance. " +
"Multiple EventBus assets detected - ensure only one EventBusUpdateHandler exists.");
}
_instance = instance;
if (instance != null) {
instance.Initialize();
}
}
/// <summary>
/// Check if an instance exists without creating one
/// </summary>
public static bool HasInstance => _instance != null && !_applicationIsQuitting;
// Event delegates - using NonSerialized to prevent serialization issues
[NonSerialized] private Dictionary<Type, Delegate> _eventHandlers;
[NonSerialized] private Queue<QueuedEvent> _eventQueue;
[NonSerialized] private List<EventSubscription> _subscriptions;
[NonSerialized] private bool _isInitialized = false;
[Header("Event Bus Settings")]
[SerializeField] private bool enableEventLogging = false;
[SerializeField] private bool processEventsImmediately = true;
[SerializeField] private int maxEventsPerFrame = 10;
private class QueuedEvent {
public PathfindingEventBase Event { get; set; }
public Type EventType { get; set; }
}
private class EventSubscription {
public Type EventType { get; set; }
public Delegate Handler { get; set; }
public WeakReference SubscriberReference { get; set; }
public int Priority { get; set; }
}
private void Initialize() {
if (_isInitialized) return;
_eventHandlers = new Dictionary<Type, Delegate>();
_eventQueue = new Queue<QueuedEvent>();
_subscriptions = new List<EventSubscription>();
_isInitialized = true;
_applicationIsQuitting = false;
if (enableEventLogging) {
Debug.Log("[EventBus] Initialized");
}
}
private void OnEnable() {
// Don't auto-initialize here - let EventBusUpdateHandler handle it
// This prevents initialization order issues
#if UNITY_EDITOR
// Only in editor for inspector preview
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) {
return;
}
#endif
Initialize();
}
private void OnDisable() {
if (_instance == this) {
Cleanup();
}
}
/// <summary>
/// Must be called from an Update method to process queued events
/// Call this from EventBusUpdateHandler or any MonoBehaviour's Update
/// </summary>
public void ProcessUpdate() {
if (!_isInitialized || _applicationIsQuitting) return;
if (!processEventsImmediately) {
ProcessQueuedEvents();
}
// Clean up dead subscriptions
CleanupDeadSubscriptions();
}
/// <summary>
/// Subscribe to an event type with a specific handler
/// </summary>
public void Subscribe<T>(Action<T> handler, object subscriber = null, int priority = 0)
where T : PathfindingEventBase {
if (_applicationIsQuitting || !_isInitialized) {
// Try to initialize if not done yet
if (!_isInitialized && !_applicationIsQuitting) {
Initialize();
} else {
return;
}
}
var eventType = typeof(T);
if (!_eventHandlers.ContainsKey(eventType)) {
_eventHandlers[eventType] = null;
}
// Add to delegate chain
_eventHandlers[eventType] = Delegate.Combine(_eventHandlers[eventType], handler);
// Track subscription for cleanup
_subscriptions.Add(new EventSubscription {
EventType = eventType,
Handler = handler,
SubscriberReference = subscriber != null ? new WeakReference(subscriber) : null,
Priority = priority
});
// Sort by priority
_subscriptions.Sort((a, b) => b.Priority.CompareTo(a.Priority));
if (enableEventLogging) {
Debug.Log($"[EventBus] Subscribed to {eventType.Name}");
}
}
/// <summary>
/// Unsubscribe from an event type
/// </summary>
public void Unsubscribe<T>(Action<T> handler) where T : PathfindingEventBase {
if (_applicationIsQuitting || !_isInitialized) {
return;
}
var eventType = typeof(T);
if (_eventHandlers != null && _eventHandlers.ContainsKey(eventType)) {
_eventHandlers[eventType] = Delegate.Remove(_eventHandlers[eventType], handler);
// Remove from subscriptions list
_subscriptions?.RemoveAll(s => s.EventType == eventType && s.Handler.Equals(handler));
if (enableEventLogging) {
Debug.Log($"[EventBus] Unsubscribed from {eventType.Name}");
}
}
}
/// <summary>
/// Publish an event to all subscribers
/// </summary>
public void Publish<T>(T eventToPublish) where T : PathfindingEventBase {
if (_applicationIsQuitting || !_isInitialized) {
// Try to initialize if not done yet
if (!_isInitialized && !_applicationIsQuitting) {
Initialize();
}
if (_applicationIsQuitting) {
return;
}
}
if (eventToPublish == null) {
Debug.LogError("[EventBus] Cannot publish null event");
return;
}
var eventType = typeof(T);
if (enableEventLogging) {
Debug.Log($"[EventBus] Publishing {eventType.Name} from {eventToPublish.Sender?.GetType().Name ?? "Unknown"}");
}
if (processEventsImmediately) {
InvokeEvent(eventToPublish, eventType);
} else {
_eventQueue.Enqueue(new QueuedEvent { Event = eventToPublish, EventType = eventType });
}
}
private void InvokeEvent(PathfindingEventBase eventToInvoke, Type eventType) {
if (_eventHandlers != null && _eventHandlers.ContainsKey(eventType) && _eventHandlers[eventType] != null) {
try {
_eventHandlers[eventType].DynamicInvoke(eventToInvoke);
} catch (Exception e) {
Debug.LogError($"[EventBus] Error invoking event {eventType.Name}: {e.Message}\n{e.StackTrace}");
}
}
// Also check for base class handlers
var baseType = eventType.BaseType;
while (baseType != null && baseType != typeof(object)) {
if (_eventHandlers != null && _eventHandlers.ContainsKey(baseType) && _eventHandlers[baseType] != null) {
try {
_eventHandlers[baseType].DynamicInvoke(eventToInvoke);
} catch (Exception e) {
Debug.LogError($"[EventBus] Error invoking base event {baseType.Name}: {e.Message}");
}
}
baseType = baseType.BaseType;
}
}
private void ProcessQueuedEvents() {
if (_eventQueue == null) return;
int eventsProcessed = 0;
while (_eventQueue.Count > 0 && eventsProcessed < maxEventsPerFrame) {
var queuedEvent = _eventQueue.Dequeue();
InvokeEvent(queuedEvent.Event, queuedEvent.EventType);
eventsProcessed++;
}
}
private void CleanupDeadSubscriptions() {
_subscriptions?.RemoveAll(s =>
s.SubscriberReference != null && !s.SubscriberReference.IsAlive);
}
/// <summary>
/// Clear all subscriptions
/// </summary>
public void ClearAllSubscriptions() {
if (!_isInitialized) return;
_eventHandlers?.Clear();
_subscriptions?.Clear();
_eventQueue?.Clear();
if (enableEventLogging) {
Debug.Log("[EventBus] Cleared all subscriptions");
}
}
/// <summary>
/// Get statistics about the event bus
/// </summary>
public EventBusStatistics GetStatistics() {
if (!_isInitialized) {
return new EventBusStatistics();
}
return new EventBusStatistics {
TotalSubscriptions = _subscriptions?.Count ?? 0,
QueuedEvents = _eventQueue?.Count ?? 0,
EventTypes = _eventHandlers?.Count ?? 0,
IsProcessingImmediately = processEventsImmediately
};
}
public struct EventBusStatistics {
public int TotalSubscriptions;
public int QueuedEvents;
public int EventTypes;
public bool IsProcessingImmediately;
}
/// <summary>
/// Called by EventBusUpdateHandler when application is quitting
/// </summary>
public static void SetApplicationQuitting() {
_applicationIsQuitting = true;
_instance?.Cleanup();
}
private void Cleanup() {
ClearAllSubscriptions();
_isInitialized = false;
}
#if UNITY_EDITOR
// Reset the static instance when entering play mode in the editor
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics() {
_instance = null;
_applicationIsQuitting = false;
}
#endif
}
/// <summary>
/// Extension methods for clean, safe event bus operations
/// No inheritance required - just use these extension methods
/// </summary>
public static class EventBusExtensions {
/// <summary>
/// Safely subscribe to an event
/// </summary>
public static void Subscribe<T>(this MonoBehaviour subscriber, Action<T> handler, int priority = 0)
where T : PathfindingEventBase {
if (PathfindingEventBus.HasInstance) {
PathfindingEventBus.Instance.Subscribe(handler, subscriber, priority);
}
}
/// <summary>
/// Safely unsubscribe from an event
/// </summary>
public static void Unsubscribe<T>(Action<T> handler)
where T : PathfindingEventBase {
if (PathfindingEventBus.HasInstance) {
PathfindingEventBus.Instance.Unsubscribe(handler);
}
}
/// <summary>
/// Safely publish an event
/// </summary>
public static void Publish<T>(T eventToPublish)
where T : PathfindingEventBase {
if (PathfindingEventBus.HasInstance) {
PathfindingEventBus.Instance.Publish(eventToPublish);
}
}
}
}