using System; using System.Collections.Generic; using System.Linq; 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. private event EventHandler Event; /// Writes messages to the log. private readonly IMonitor Monitor; /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; /// The display names for the mods which added each delegate. private readonly IDictionary, IModMetadata> SourceMods = new Dictionary, IModMetadata>(); /// The cached invocation list. private EventHandler[] CachedInvocationList; /// Tracks performance metrics. private readonly PerformanceMonitor PerformanceMonitor; /********* ** 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. /// Writes messages to the log. /// 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, IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; } /// Get whether anything is listening to the event. public bool HasListeners() { return this.CachedInvocationList?.Length > 0; } /// Add an event handler. /// The event handler. public void Add(EventHandler handler) { this.Add(handler, this.ModRegistry.GetFromStack()); } /// Add an event handler. /// The event handler. /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { this.Event -= handler; this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { if (this.Event == null) return; this.PerformanceMonitor.Track(this.EventName, () => { foreach (EventHandler handler in this.CachedInvocationList) { try { this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(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.Event == null) return; foreach (EventHandler handler in this.CachedInvocationList) { if (match(this.GetSourceMod(handler))) { try { 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(EventHandler handler) { IModMetadata mod = this.GetSourceMod(handler); if (mod == null) return Constants.GamePerformanceCounterName; return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; } /// Track an event handler. /// The mod which added the handler. /// The event handler. /// The updated event invocation list. protected void AddTracking(IModMetadata mod, EventHandler handler, IEnumerable> invocationList) { this.SourceMods[handler] = mod; this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; } /// Remove tracking for an event handler. /// The event handler. /// The updated event invocation list. protected void RemoveTracking(EventHandler handler, IEnumerable> invocationList) { this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } /// Get the mod which registered the given event handler, if available. /// The event handler. protected IModMetadata GetSourceMod(EventHandler handler) { return this.SourceMods.TryGetValue(handler, out IModMetadata mod) ? mod : null; } /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. protected void LogError(EventHandler handler, Exception ex) { IModMetadata mod = this.GetSourceMod(handler); if (mod != null) mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); else this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } } }