using System; using System.Collections.Generic; using System.Linq; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. internal class ManagedEvent { /********* ** Fields *********/ /// The underlying event. private event EventHandler Event; /// A human-readable name for the event. private readonly string EventName; /// 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; /********* ** 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. public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; } /// 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; foreach (EventHandler handler in this.CachedInvocationList) { try { 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 *********/ /// 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); } } }