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