diff options
Diffstat (limited to 'src/SMAPI/Framework/Events')
-rw-r--r-- | src/SMAPI/Framework/Events/EventManager.cs | 34 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/IManagedEvent.cs | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ManagedEvent.cs | 101 |
3 files changed, 79 insertions, 60 deletions
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 41540047..b21d5c7d 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.Events public EventManager(ModRegistry modRegistry) { // create shortcut initializers - ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName, bool isPerformanceCritical = false) + ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) { - return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry, isPerformanceCritical); + return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry); } // init events @@ -210,21 +210,21 @@ namespace StardewModdingAPI.Framework.Events this.LocaleChanged = ManageEventOf<LocaleChangedEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged)); this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); - this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); - this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true); - this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true); - this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true); - this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true); - this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true); - this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true); - this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true); + this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); + this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); + this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); + this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); + this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); + this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); + this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); + this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); this.WindowResized = ManageEventOf<WindowResizedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); this.GameLaunched = ManageEventOf<GameLaunchedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); - this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true); - this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true); - this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true); - this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true); + this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); + this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); + this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); + this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); this.SaveCreating = ManageEventOf<SaveCreatingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); this.SaveCreated = ManageEventOf<SaveCreatedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); this.Saving = ManageEventOf<SavingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); @@ -238,7 +238,7 @@ namespace StardewModdingAPI.Framework.Events this.ButtonsChanged = ManageEventOf<ButtonsChangedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonsChanged)); this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true); + this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); @@ -261,8 +261,8 @@ namespace StardewModdingAPI.Framework.Events this.FurnitureListChanged = ManageEventOf<FurnitureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.FurnitureListChanged)); this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); - this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true); - this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true); + this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking)); + this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked)); } } } diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs index e4e3ca08..55994c04 100644 --- a/src/SMAPI/Framework/Events/IManagedEvent.cs +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Framework.Events /// <summary>A human-readable name for the event.</summary> string EventName { get; } - /// <summary>Whether the event is typically called at least once per second.</summary> - bool IsPerformanceCritical { get; } + /// <summary>Whether any handlers are listening to the event.</summary> + bool HasListeners { get; } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 4b8a770d..8a3ca839 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -20,14 +20,17 @@ namespace StardewModdingAPI.Framework.Events /// <summary>The underlying event handlers.</summary> private readonly List<ManagedEventHandler<TEventArgs>> Handlers = new(); - /// <summary>A cached snapshot of <see cref="Handlers"/>, or <c>null</c> to rebuild it next raise.</summary> + /// <summary>A cached snapshot of the <see cref="Handlers"/> sorted by event priority, or <c>null</c> to rebuild it next raise.</summary> private ManagedEventHandler<TEventArgs>[]? CachedHandlers = Array.Empty<ManagedEventHandler<TEventArgs>>(); /// <summary>The total number of event handlers registered for this events, regardless of whether they're still registered.</summary> private int RegistrationIndex; - /// <summary>Whether new handlers were added since the last raise.</summary> - private bool HasNewHandlers; + /// <summary>Whether handlers were removed since the last raise.</summary> + private bool HasRemovedHandlers; + + /// <summary>Whether any of the handlers have a custom priority.</summary> + private bool HasPriorities; /********* @@ -37,7 +40,7 @@ namespace StardewModdingAPI.Framework.Events public string EventName { get; } /// <inheritdoc /> - public bool IsPerformanceCritical { get; } + public bool HasListeners { get; private set; } /********* @@ -46,18 +49,10 @@ namespace StardewModdingAPI.Framework.Events /// <summary>Construct an instance.</summary> /// <param name="eventName">A human-readable name for the event.</param> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - /// <param name="isPerformanceCritical">Whether the event is typically called at least once per second.</param> - public ManagedEvent(string eventName, ModRegistry modRegistry, bool isPerformanceCritical = false) + public ManagedEvent(string eventName, ModRegistry modRegistry) { this.EventName = eventName; this.ModRegistry = modRegistry; - this.IsPerformanceCritical = isPerformanceCritical; - } - - /// <summary>Get whether anything is listening to the event.</summary> - public bool HasListeners() - { - return this.Handlers.Count > 0; } /// <summary>Add an event handler.</summary> @@ -72,7 +67,8 @@ namespace StardewModdingAPI.Framework.Events this.Handlers.Add(managedHandler); this.CachedHandlers = null; - this.HasNewHandlers = true; + this.HasListeners = true; + this.HasPriorities |= priority != EventPriority.Normal; } } @@ -90,6 +86,8 @@ namespace StardewModdingAPI.Framework.Events this.Handlers.RemoveAt(i); this.CachedHandlers = null; + this.HasListeners = this.Handlers.Count != 0; + this.HasRemovedHandlers = true; break; } } @@ -97,46 +95,40 @@ namespace StardewModdingAPI.Framework.Events /// <summary>Raise the event and notify all handlers.</summary> /// <param name="args">The event arguments to pass.</param> - /// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param> - public void Raise(TEventArgs args, Func<IModMetadata, bool>? match = null) - { - this.Raise((_, invoke) => invoke(args), match); - } - - /// <summary>Raise the event and notify all handlers.</summary> - /// <param name="invoke">Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it.</param> - /// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param> - public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke, Func<IModMetadata, bool>? match = null) + public void Raise(TEventArgs args) { // 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) + // raise event + foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers()) { - lock (this.Handlers) + try { - if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) - this.Handlers.Sort(); - - this.CachedHandlers = handlers = this.Handlers.ToArray(); - this.HasNewHandlers = false; + handler.Handler(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); } } + } + + /// <summary>Raise the event and notify all handlers.</summary> + /// <param name="invoke">Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it.</param> + public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke) + { + // skip if no handlers + if (this.Handlers.Count == 0) + return; // raise event - foreach (ManagedEventHandler<TEventArgs> handler in handlers) + foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers()) { - if (match != null && !match(handler.SourceMod)) - continue; - try { - invoke(handler.SourceMod, args => handler.Handler.Invoke(null, args)); + invoke(handler.SourceMod, args => handler.Handler(null, args)); } catch (Exception ex) { @@ -152,9 +144,36 @@ namespace StardewModdingAPI.Framework.Events /// <summary>Log an exception from an event handler.</summary> /// <param name="handler">The event handler instance.</param> /// <param name="ex">The exception that was raised.</param> - protected void LogError(ManagedEventHandler<TEventArgs> handler, Exception ex) + private void LogError(ManagedEventHandler<TEventArgs> handler, Exception ex) { handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } + + /// <summary>Get cached copy of the sorted handlers to invoke.</summary> + /// <remarks>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.</remarks> + private ManagedEventHandler<TEventArgs>[] GetHandlers() + { + ManagedEventHandler<TEventArgs>[]? 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; + } } } |