using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. internal class ManagedEvent : IManagedEvent { /********* ** Fields *********/ /// The underlying event handlers. private readonly List> EventHandlers = new List>(); /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; /// Tracks performance metrics. private readonly PerformanceMonitor PerformanceMonitor; /// The total number of event handlers registered for this events, regardless of whether they're still registered. private int RegistrationIndex; /// Whether any registered event handlers have a custom priority value. private bool HasCustomPriorities; /// Whether event handlers should be sorted before the next invocation. private bool NeedsSort; /********* ** Accessors *********/ /// A human-readable name for the event. public string EventName { get; } /// Whether the event is typically called at least once per second. public bool IsPerformanceCritical { get; } /********* ** Public methods *********/ /// Construct an instance. /// A human-readable name for the event. /// The mod registry with which to identify mods. /// Tracks performance metrics. /// Whether the event is typically called at least once per second. public ManagedEvent(string eventName, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) { this.EventName = eventName; this.ModRegistry = modRegistry; this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; } /// Get whether anything is listening to the event. public bool HasListeners() { return this.EventHandlers.Count > 0; } /// Add an event handler. /// The event handler. /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); this.EventHandlers.Add(managedHandler); this.HasCustomPriorities = this.HasCustomPriorities || managedHandler.HasCustomPriority(); if (this.HasCustomPriorities) this.NeedsSort = true; } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { this.EventHandlers.RemoveAll(p => p.Handler == handler); this.HasCustomPriorities = this.HasCustomPriorities && this.EventHandlers.Any(p => p.HasCustomPriority()); } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { // sort event handlers by priority // (This is done here to avoid repeatedly sorting when handlers are added/removed.) if (this.NeedsSort) { this.NeedsSort = false; this.EventHandlers.Sort(); } // raise if (this.EventHandlers.Count == 0) return; this.PerformanceMonitor.Track(this.EventName, () => { foreach (ManagedEventHandler handler in this.EventHandlers) { try { this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args)); } catch (Exception ex) { this.LogError(handler, ex); } } }); } /// Raise the event and notify all handlers. /// The event arguments to pass. /// A lambda which returns true if the event should be raised for the given mod. public void RaiseForMods(TEventArgs args, Func match) { if (this.EventHandlers.Count == 0) return; foreach (ManagedEventHandler handler in this.EventHandlers) { if (match(handler.SourceMod)) { try { handler.Handler.Invoke(null, args); } catch (Exception ex) { this.LogError(handler, ex); } } } } /********* ** Private methods *********/ /// Get the mod name for a given event handler to display in performance monitoring reports. /// The event handler. private string GetModNameForPerformanceCounters(ManagedEventHandler handler) { IModMetadata mod = handler.SourceMod; return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; } /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. protected void LogError(ManagedEventHandler handler, Exception ex) { handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } } }