AStarHeightmapGrid / Assets / PlatypusIdeas / AirPath / Runtime / Events / PathfindingEventBus.cs
PathfindingEventBus.cs
Raw
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);
            }
        }
    }
}