using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Internal; 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 mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; /// The underlying event handlers. private readonly List> Handlers = new(); /// A cached snapshot of the sorted by event priority, or null to rebuild it next raise. private ManagedEventHandler[]? CachedHandlers = Array.Empty>(); /// The total number of event handlers registered for this events, regardless of whether they're still registered. private int RegistrationIndex; /// Whether handlers were removed since the last raise. private bool HasRemovedHandlers; /// Whether any of the handlers have a custom priority. private bool HasPriorities; /********* ** Accessors *********/ /// public string EventName { get; } /// public bool HasListeners { get; private set; } /********* ** Public methods *********/ /// Construct an instance. /// A human-readable name for the event. /// The mod registry with which to identify mods. public ManagedEvent(string eventName, ModRegistry modRegistry) { this.EventName = eventName; this.ModRegistry = modRegistry; } /// Add an event handler. /// The event handler. /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { lock (this.Handlers) { EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); this.Handlers.Add(managedHandler); this.CachedHandlers = null; this.HasListeners = true; this.HasPriorities |= priority != EventPriority.Normal; } } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { lock (this.Handlers) { // match C# events: if a handler is listed multiple times, remove the last one added for (int i = this.Handlers.Count - 1; i >= 0; i--) { if (this.Handlers[i].Handler != handler) continue; this.Handlers.RemoveAt(i); this.CachedHandlers = null; this.HasListeners = this.Handlers.Count != 0; this.HasRemovedHandlers = true; break; } } } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { // skip if no handlers if (this.Handlers.Count == 0) return; // raise event foreach (ManagedEventHandler handler in this.GetHandlers()) { Context.HeuristicModsRunningCode.Push(handler.SourceMod); try { handler.Handler(null, args); } catch (Exception ex) { this.LogError(handler, ex); } finally { Context.HeuristicModsRunningCode.TryPop(out _); } } } /// Raise the event and notify all handlers. /// Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it. public void Raise(Action> invoke) { // skip if no handlers if (this.Handlers.Count == 0) return; // raise event foreach (ManagedEventHandler handler in this.GetHandlers()) { Context.HeuristicModsRunningCode.Push(handler.SourceMod); try { invoke(handler.SourceMod, args => handler.Handler(null, args)); } catch (Exception ex) { this.LogError(handler, ex); } finally { Context.HeuristicModsRunningCode.TryPop(out _); } } } /********* ** Private methods *********/ /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. private void LogError(ManagedEventHandler handler, Exception ex) { handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } /// Get cached copy of the sorted handlers to invoke. /// This returns the handlers sorted by priority, and allows iterating the list even if a mod adds/removes handlers while handling it. This is debounced when requested to avoid repeatedly sorting when handlers are added/removed. private ManagedEventHandler[] GetHandlers() { ManagedEventHandler[]? handlers = this.CachedHandlers; if (handlers == null) { lock (this.Handlers) { // recheck priorities if (this.HasRemovedHandlers) this.HasPriorities = this.Handlers.Any(p => p.Priority != EventPriority.Normal); // sort by priority if needed if (this.HasPriorities) this.Handlers.Sort(); // update cache this.CachedHandlers = handlers = this.Handlers.ToArray(); this.HasRemovedHandlers = false; } } return handlers; } } }