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);
}
}
}