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 mod registry with which to identify mods.
protected readonly ModRegistry ModRegistry;
/// Tracks performance metrics.
private readonly PerformanceMonitor PerformanceMonitor;
/// The underlying event handlers.
private readonly List> Handlers = new List>();
/// A cached snapshot of , or null to rebuild it next raise.
private ManagedEventHandler[] CachedHandlers = new ManagedEventHandler[0];
/// The total number of event handlers registered for this events, regardless of whether they're still registered.
private int RegistrationIndex;
/// Whether new handlers were added since the last raise.
private bool HasNewHandlers;
/*********
** 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.Handlers.Count > 0;
}
/// 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.HasNewHandlers = true;
}
}
/// 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;
break;
}
}
}
/// 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 Raise(TEventArgs args, Func match = null)
{
// skip if no handlers
if (this.Handlers.Count == 0)
return;
// update cached data
// (This is debounced here to avoid repeatedly sorting when handlers are added/removed,
// and keeping a separate cached list allows changes during enumeration.)
var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event, which will set this field to null
if (handlers == null)
{
lock (this.Handlers)
{
if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal))
this.Handlers.Sort();
this.CachedHandlers = handlers = this.Handlers.ToArray();
this.HasNewHandlers = false;
}
}
// raise event
this.PerformanceMonitor.Track(this.EventName, () =>
{
foreach (ManagedEventHandler handler in handlers)
{
if (match != null && !match(handler.SourceMod))
continue;
try
{
this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => 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);
}
}
}