From a9b4b1160db7b1ea74ea3d9272a42d94226c7110 Mon Sep 17 00:00:00 2001 From: Arne Kiesewetter Date: Thu, 11 Apr 2024 03:09:00 +0200 Subject: [PATCH] Add dynamic Event source / handler system Co-authored-by: Colin Barndt --- MonkeyLoader/AnyMap.cs | 65 ++++++++++ MonkeyLoader/EnumerableExtensions.cs | 27 +++- MonkeyLoader/Events/EventDispatcher.cs | 165 +++++++++++++++++++++++++ MonkeyLoader/Events/EventManager.cs | 76 ++++++++++++ MonkeyLoader/Events/IEvent.cs | 18 +++ MonkeyLoader/Events/IEventHandler.cs | 22 ++++ MonkeyLoader/Events/IEventSource.cs | 18 +++ MonkeyLoader/IPrioritizable.cs | 9 ++ MonkeyLoader/Meta/IShutdown.cs | 18 +++ MonkeyLoader/Meta/Mod.cs | 57 +++++++++ MonkeyLoader/MonkeyLoader.cs | 38 +++++- MonkeyLoader/Patching/MonkeyBase.cs | 35 ++++++ MonkeyLoader/PriorityHelper.cs | 18 +++ MonkeyLoader/SortedCollection.cs | 4 +- 14 files changed, 565 insertions(+), 5 deletions(-) create mode 100644 MonkeyLoader/AnyMap.cs create mode 100644 MonkeyLoader/Events/EventDispatcher.cs create mode 100644 MonkeyLoader/Events/EventManager.cs create mode 100644 MonkeyLoader/Events/IEvent.cs create mode 100644 MonkeyLoader/Events/IEventHandler.cs create mode 100644 MonkeyLoader/Events/IEventSource.cs create mode 100644 MonkeyLoader/IPrioritizable.cs create mode 100644 MonkeyLoader/PriorityHelper.cs diff --git a/MonkeyLoader/AnyMap.cs b/MonkeyLoader/AnyMap.cs new file mode 100644 index 0000000..acda527 --- /dev/null +++ b/MonkeyLoader/AnyMap.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader +{ + /// + /// Represents a type safe dictionary of Types to objects of the type. + /// + public sealed class AnyMap + { + private readonly Dictionary _dict = new(); + + public IEnumerable Keys => _dict.Keys; + + public void Add(T value) => _dict.Add(typeof(T), value); + + public void Clear() => _dict.Clear(); + + public bool ContainsKey() => _dict.ContainsKey(typeof(T)); + + public IEnumerable GetCastableValues() + => _dict.Values.SelectCastable(); + + public T GetOrCreateValue() where T : new() + { + if (TryGetValue(out var value)) + return value!; + + value = new T(); + Add(value); + + return value; + } + + public T GetOrCreateValue(Func valueFactory) + { + if (TryGetValue(out var value)) + return value!; + + value = valueFactory(); + Add(value); + + return value; + } + + public T GetValue() => (T)_dict[typeof(T)]!; + + public void Remove() => _dict.Remove(typeof(T)); + + public bool TryGetValue(out T? value) + { + if (_dict.TryGetValue(typeof(T), out var obj)) + { + value = (T)obj!; + return true; + } + + value = default; + return false; + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/EnumerableExtensions.cs b/MonkeyLoader/EnumerableExtensions.cs index 43bf89a..a9c360f 100644 --- a/MonkeyLoader/EnumerableExtensions.cs +++ b/MonkeyLoader/EnumerableExtensions.cs @@ -162,6 +162,29 @@ public static string Format(this Exception ex) }; } + public static TValue GetOrCreateValue(this IDictionary dictionary, TKey key, Func valueFactory) + { + if (dictionary.TryGetValue(key, out var value)) + return value; + + value = valueFactory(); + dictionary.Add(key, value); + + return value; + } + + public static TValue GetOrCreateValue(this IDictionary dictionary, TKey key) + where TValue : new() + { + if (dictionary.TryGetValue(key, out var value)) + return value; + + value = new TValue(); + dictionary.Add(key, value); + + return value; + } + /// /// Filters a source sequence of s to only contain the ones instantiable /// without parameters and assignable to . @@ -189,8 +212,8 @@ public static IEnumerable Instantiable(this IEnumerable t /// The items in the source sequence. /// The items in the result sequence. /// The items to try and cast. - /// All items from the source that were castable to . - public static IEnumerable SelectCastable(this IEnumerable source) + /// All items from the source that were castable to and not null. + public static IEnumerable SelectCastable(this IEnumerable source) { foreach (var item in source) { diff --git a/MonkeyLoader/Events/EventDispatcher.cs b/MonkeyLoader/Events/EventDispatcher.cs new file mode 100644 index 0000000..70d4f44 --- /dev/null +++ b/MonkeyLoader/Events/EventDispatcher.cs @@ -0,0 +1,165 @@ +using MonkeyLoader.Logging; +using MonkeyLoader.Meta; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader.Events +{ + internal sealed class CancelableEventDispatcher + : EventDispatcherBase, ICancelableEventHandler> + where TEvent : ICancelableEvent + { + public CancelableEventDispatcher(EventManager manager) : base(manager) + { } + + protected override void AddSource(ICancelableEventSource eventSource) + => eventSource.Dispatched += DispatchEvents; + + protected override void RemoveSource(ICancelableEventSource eventSource) + => eventSource.Dispatched -= DispatchEvents; + + private void DispatchEvents(TEvent eventArgs) + { + foreach (var handler in handlers) + { + if (eventArgs.Canceled && handler.SkipCanceled) + { + Logger.Trace(() => $"Skipping event handler [{handler.GetType()}] for canceled event [{eventArgs}]!"); + continue; + } + + try + { + handler.Handle(eventArgs); + } + catch (Exception ex) + { + Logger.Warn(() => ex.Format($"Event handler [{handler.GetType()}] threw an exception for event [{eventArgs}]:")); + } + } + } + } + + internal sealed class EventDispatcher + : EventDispatcherBase, IEventHandler> + where TEvent : IEvent + { + public EventDispatcher(EventManager manager) : base(manager) + { } + + protected override void AddSource(IEventSource eventSource) + => eventSource.Dispatched += DispatchEvents; + + protected override void RemoveSource(IEventSource eventSource) + => eventSource.Dispatched -= DispatchEvents; + + private void DispatchEvents(TEvent eventArgs) + { + foreach (var handler in handlers) + { + try + { + handler.Handle(eventArgs); + } + catch (Exception ex) + { + Logger.Warn(() => ex.Format($"Event handler [{handler.GetType()}] threw an exception for event [{eventArgs}]:")); + } + } + } + } + + internal abstract class EventDispatcherBase : IEventDispatcher + where THandler : IPrioritizable + { + protected readonly SortedCollection handlers = new((IComparer)PriorityHelper.Comparer); + + private readonly Dictionary> _handlersByMod = new(); + private readonly EventManager _manager; + private readonly Dictionary> _sourcesByMod = new(); + + protected Logger Logger => _manager.Logger; + + protected EventDispatcherBase(EventManager manager) + { + _manager = manager; + } + + public bool AddHandler(Mod mod, THandler handler) + { + if (_handlersByMod.GetOrCreateValue(mod).Add(handler)) + { + handlers.Add(handler); + return true; + } + + return false; + } + + public bool AddSource(Mod mod, TSource source) + { + if (_sourcesByMod.GetOrCreateValue(mod).Add(source)) + { + AddSource(source); + return true; + } + + return false; + } + + public bool RemoveHandler(Mod mod, THandler handler) + { + if (_handlersByMod.TryGetValue(mod, out var modHandlers)) + { + modHandlers.Remove(handler); + handlers.Remove(handler); + + return true; + } + + return false; + } + + public bool RemoveSource(Mod mod, TSource source) + { + if (_sourcesByMod.TryGetValue(mod, out var modSources)) + { + modSources.Remove(source); + return true; + } + + return false; + } + + public void UnregisterMod(Mod mod) + { + if (_sourcesByMod.TryGetValue(mod, out var modSources)) + { + foreach (var source in modSources) + RemoveSource(source); + } + + _sourcesByMod.Remove(mod); + + if (_handlersByMod.TryGetValue(mod, out var modHandlers)) + { + foreach (var handler in modHandlers) + handlers.Remove(handler); + } + + _handlersByMod.Remove(mod); + } + + protected abstract void AddSource(TSource eventSource); + + protected abstract void RemoveSource(TSource eventSource); + } + + internal interface IEventDispatcher + { + public void UnregisterMod(Mod mod); + } +} \ No newline at end of file diff --git a/MonkeyLoader/Events/EventManager.cs b/MonkeyLoader/Events/EventManager.cs new file mode 100644 index 0000000..1366493 --- /dev/null +++ b/MonkeyLoader/Events/EventManager.cs @@ -0,0 +1,76 @@ +using MonkeyLoader.Logging; +using MonkeyLoader.Meta; +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader.Events +{ + internal sealed class EventManager + { + private readonly AnyMap _eventDispatchers = new(); + private readonly MonkeyLoader _loader; + internal Logger Logger { get; } + + internal EventManager(MonkeyLoader loader) + { + _loader = loader; + Logger = new(loader.Logger, "EventManager"); + } + + internal void RegisterEventHandler(Mod mod, IEventHandler eventHandler) + where TEvent : IEvent + { + ValidateLoader(mod); + + _eventDispatchers.GetOrCreateValue(CreateDispatcher).AddHandler(mod, eventHandler); + } + + internal void RegisterEventHandler(Mod mod, ICancelableEventHandler cancelableEventHandler) + where TEvent : ICancelableEvent + { + ValidateLoader(mod); + + _eventDispatchers.GetOrCreateValue(CreateCancelableDispatcher).AddHandler(mod, cancelableEventHandler); + } + + internal void RegisterEventSource(Mod mod, IEventSource eventSource) + where TEvent : IEvent + { + ValidateLoader(mod); + + _eventDispatchers.GetOrCreateValue(CreateDispatcher).AddSource(mod, eventSource); + } + + internal void RegisterEventSource(Mod mod, ICancelableEventSource cancelableEventSource) + where TEvent : ICancelableEvent + { + ValidateLoader(mod); + + _eventDispatchers.GetOrCreateValue(CreateCancelableDispatcher).AddSource(mod, cancelableEventSource); + } + + internal void UnregisterMod(Mod mod) + { + ValidateLoader(mod); + + foreach (var eventDispatcher in _eventDispatchers.GetCastableValues()) + eventDispatcher.UnregisterMod(mod); + } + + private CancelableEventDispatcher CreateCancelableDispatcher() + where TEvent : ICancelableEvent => new(this); + + private EventDispatcher CreateDispatcher() + where TEvent : IEvent => new(this); + + private void ValidateLoader(Mod mod) + { + if (mod.Loader != _loader) + throw new InvalidOperationException("Can't register event handler of mod from another loader!"); + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Events/IEvent.cs b/MonkeyLoader/Events/IEvent.cs new file mode 100644 index 0000000..3dd359d --- /dev/null +++ b/MonkeyLoader/Events/IEvent.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader.Events +{ + public interface ICancelableEvent : IEvent + { + public bool Canceled { get; set; } + } + + public interface IEvent + { + public TTarget Target { get; } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Events/IEventHandler.cs b/MonkeyLoader/Events/IEventHandler.cs new file mode 100644 index 0000000..bf121ca --- /dev/null +++ b/MonkeyLoader/Events/IEventHandler.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader.Events +{ + public interface ICancelableEventHandler : IPrioritizable + where TEvent : ICancelableEvent + { + public bool SkipCanceled { get; } + + public void Handle(TEvent eventArgs); + } + + public interface IEventHandler : IPrioritizable + where TEvent : IEvent + { + public void Handle(TEvent eventArgs); + } +} \ No newline at end of file diff --git a/MonkeyLoader/Events/IEventSource.cs b/MonkeyLoader/Events/IEventSource.cs new file mode 100644 index 0000000..833aceb --- /dev/null +++ b/MonkeyLoader/Events/IEventSource.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader.Events +{ + public interface ICancelableEventSource where TEvent : ICancelableEvent + { + public event Action Dispatched; + } + + public interface IEventSource where TEvent : IEvent + { + public event Action Dispatched; + } +} \ No newline at end of file diff --git a/MonkeyLoader/IPrioritizable.cs b/MonkeyLoader/IPrioritizable.cs new file mode 100644 index 0000000..14ae232 --- /dev/null +++ b/MonkeyLoader/IPrioritizable.cs @@ -0,0 +1,9 @@ +using HarmonyLib; + +namespace MonkeyLoader +{ + public interface IPrioritizable + { + public int Priority { get; } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/IShutdown.cs b/MonkeyLoader/Meta/IShutdown.cs index 2200c34..33a9a58 100644 --- a/MonkeyLoader/Meta/IShutdown.cs +++ b/MonkeyLoader/Meta/IShutdown.cs @@ -6,6 +6,14 @@ namespace MonkeyLoader.Meta { + /// + /// Called when something shuts down. + /// + /// The object that's shutting down. + /// Whether the shutdown was caused by the application exiting. + /// true if it ran successfully; otherwise, false. + public delegate void ShutdownHandler(IShutdown source, bool applicationExiting); + /// /// Contains extension methods for collections of instances. /// @@ -52,5 +60,15 @@ public interface IShutdown /// true if it ran successfully; otherwise, false. /// If it gets called more than once. public bool Shutdown(bool applicationExiting); + + /// + /// Called when something has shut down. + /// + public event ShutdownHandler? ShutdownDone; + + /// + /// Called when something is about to shut down. + /// + public event ShutdownHandler? ShuttingDown; } } \ No newline at end of file diff --git a/MonkeyLoader/Meta/Mod.cs b/MonkeyLoader/Meta/Mod.cs index 9b81438..b85188b 100644 --- a/MonkeyLoader/Meta/Mod.cs +++ b/MonkeyLoader/Meta/Mod.cs @@ -1,5 +1,6 @@ using HarmonyLib; using MonkeyLoader.Configuration; +using MonkeyLoader.Events; using MonkeyLoader.Logging; using MonkeyLoader.NuGet; using MonkeyLoader.Patching; @@ -8,6 +9,7 @@ using NuGet.Versioning; using System; using System.Collections.Generic; +using System.Diagnostics.Tracing; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -319,6 +321,22 @@ public bool DependsOn(string otherId) /// true if the given tag is listed for this mod; otherwise, false. public bool HasTag(string tag) => tags.Contains(tag); + public void RegisterEventHandler(IEventHandler eventHandler) + where TEvent : IEvent + => Loader.EventManager.RegisterEventHandler(this, eventHandler); + + public void RegisterEventHandler(ICancelableEventHandler cancelableEventHandler) + where TEvent : ICancelableEvent + => Loader.EventManager.RegisterEventHandler(this, cancelableEventHandler); + + public void RegisterEventSource(IEventSource eventSource) + where TEvent : IEvent + => Loader.EventManager.RegisterEventSource(this, eventSource); + + public void RegisterEventSource(ICancelableEventSource cancelableEventSource) + where TEvent : ICancelableEvent + => Loader.EventManager.RegisterEventSource(this, cancelableEventSource); + /// /// Lets this mod cleanup and shutdown.
/// Must only be called once. @@ -331,6 +349,12 @@ public bool Shutdown(bool applicationExiting) ShutdownRan = true; + Logger.Debug(() => "Running OnShutdown!"); + OnShuttingDown(applicationExiting); + + if (!applicationExiting) + Loader.EventManager.UnregisterMod(this); + try { if (!OnShutdown(applicationExiting)) @@ -345,6 +369,9 @@ public bool Shutdown(bool applicationExiting) Logger.Error(() => ex.Format("OnShutdown threw an Exception:")); } + OnShutdownDone(applicationExiting); + Logger.Debug(() => "OnShutdown done!"); + return !ShutdownFailed; } @@ -430,6 +457,36 @@ protected virtual bool OnShutdown(bool applicationExiting) return true; } + private void OnShutdownDone(bool applicationExiting) + { + try + { + ShutdownDone?.TryInvokeAll(this, applicationExiting); + } + catch (AggregateException ex) + { + Logger.Error(() => ex.Format($"Some {nameof(ShutdownDone)} event subscriber(s) threw an exception:")); + } + } + + private void OnShuttingDown(bool applicationExiting) + { + try + { + ShuttingDown?.TryInvokeAll(this, applicationExiting); + } + catch (AggregateException ex) + { + Logger.Error(() => ex.Format($"Some {nameof(ShuttingDown)} event subscriber(s) threw an exception:")); + } + } + + /// + public event ShutdownHandler? ShutdownDone; + + /// + public event ShutdownHandler? ShuttingDown; + private sealed class ModComparer : IComparer { private readonly int _factor; diff --git a/MonkeyLoader/MonkeyLoader.cs b/MonkeyLoader/MonkeyLoader.cs index 14ec1c9..d23b645 100644 --- a/MonkeyLoader/MonkeyLoader.cs +++ b/MonkeyLoader/MonkeyLoader.cs @@ -1,4 +1,5 @@ using MonkeyLoader.Configuration; +using MonkeyLoader.Events; using MonkeyLoader.Logging; using MonkeyLoader.Meta; using MonkeyLoader.NuGet; @@ -145,6 +146,7 @@ private set ///
public bool ShutdownRan => Phase >= ExecutionPhase.ShuttingDown; + internal EventManager EventManager { get; } internal AssemblyPool GameAssemblyPool { get; } internal AssemblyPool PatcherAssemblyPool { get; } @@ -201,6 +203,7 @@ public MonkeyLoader(string configPath = "MonkeyLoader/MonkeyLoader.json", Loggin PatcherAssemblyPool.AddFallbackPool(GameAssemblyPool); Phase = ExecutionPhase.Initialized; + EventManager = new(this); } /// @@ -665,10 +668,11 @@ public bool Shutdown(bool applicationExiting = true) return !ShutdownFailed; } + Logger.Warn(() => $"The loader's shutdown routine was triggered! Triggering shutdown for all {_allMods.Count} mods!"); Phase = ExecutionPhase.ShuttingDown; + OnShuttingDown(applicationExiting); var sw = Stopwatch.StartNew(); - Logger.Warn(() => $"The loader's shutdown routine was triggered! Triggering shutdown for all {_allMods.Count} mods!"); ShutdownFailed |= !ShutdownMods(_allMods, applicationExiting); @@ -685,7 +689,9 @@ public bool Shutdown(bool applicationExiting = true) } Logger.Info(() => $"Processed shutdown in {sw.ElapsedMilliseconds}ms!"); + Phase = ExecutionPhase.Shutdown; + OnShutdownDone(applicationExiting); return !ShutdownFailed; } @@ -887,6 +893,30 @@ internal void OnAnyConfigChanged(IConfigKeyChangedEventArgs configChangedEvent) } } + private void OnShutdownDone(bool applicationExiting) + { + try + { + ShutdownDone?.TryInvokeAll(this, applicationExiting); + } + catch (AggregateException ex) + { + Logger.Error(() => ex.Format($"Some {nameof(ShutdownDone)} event subscriber(s) threw an exception:")); + } + } + + private void OnShuttingDown(bool applicationExiting) + { + try + { + ShuttingDown?.TryInvokeAll(this, applicationExiting); + } + catch (AggregateException ex) + { + Logger.Error(() => ex.Format($"Some {nameof(ShuttingDown)} event subscriber(s) threw an exception:")); + } + } + private bool ShutdownMonkeys(IEarlyMonkey[] earlyMonkeys, IMonkey[] monkeys, bool applicationExiting) { var success = true; @@ -936,6 +966,12 @@ private bool TryLoadMod(string path, [NotNullWhen(true)] out NuGetPackageMod? mo /// public event ModsChangedEventHandler? ModsShuttingDown; + /// + public event ShutdownHandler? ShutdownDone; + + /// + public event ShutdownHandler? ShuttingDown; + /// /// Denotes the different stages of the loader's execution.
/// Some actions may only work before, in, or after certain phases. diff --git a/MonkeyLoader/Patching/MonkeyBase.cs b/MonkeyLoader/Patching/MonkeyBase.cs index 3a55bca..7b44b3a 100644 --- a/MonkeyLoader/Patching/MonkeyBase.cs +++ b/MonkeyLoader/Patching/MonkeyBase.cs @@ -22,6 +22,7 @@ public abstract partial class MonkeyBase : IMonkey private readonly Lazy _featurePatches; private readonly Lazy _harmony; private Mod _mod = null!; + /// /// public AssemblyName AssemblyName { get; } @@ -123,7 +124,9 @@ public bool Shutdown(bool applicationExiting) throw new InvalidOperationException("A monkey's Shutdown() method must only be called once!"); ShutdownRan = true; + Logger.Debug(() => "Running OnShutdown!"); + OnShuttingDown(applicationExiting); try { @@ -139,6 +142,9 @@ public bool Shutdown(bool applicationExiting) Logger.Error(() => ex.Format("OnShutdown threw an Exception:")); } + OnShutdownDone(applicationExiting); + Logger.Debug(() => "OnShutdown done!"); + return !ShutdownFailed; } @@ -193,6 +199,35 @@ protected void ThrowIfRan() if (Ran) throw new InvalidOperationException("A monkey's Run() method must only be called once!"); } + + private void OnShutdownDone(bool applicationExiting) + { + try + { + ShutdownDone?.TryInvokeAll(this, applicationExiting); + } + catch (AggregateException ex) + { + Logger.Error(() => ex.Format($"Some {nameof(ShutdownDone)} event subscriber(s) threw an exception:")); + } + } + + private void OnShuttingDown(bool applicationExiting) + { + try + { + ShuttingDown?.TryInvokeAll(this, applicationExiting); + } + catch (AggregateException ex) + { + Logger.Error(() => ex.Format($"Some {nameof(ShuttingDown)} event subscriber(s) threw an exception:")); + } + } + + public event ShutdownHandler? ShutdownDone; + + /// + public event ShutdownHandler? ShuttingDown; } /// diff --git a/MonkeyLoader/PriorityHelper.cs b/MonkeyLoader/PriorityHelper.cs new file mode 100644 index 0000000..706c9f8 --- /dev/null +++ b/MonkeyLoader/PriorityHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MonkeyLoader +{ + public static class PriorityHelper + { + public static IComparer Comparer { get; } = new PrioritizableComparer(); + + private sealed class PrioritizableComparer : IComparer + { + public int Compare(IPrioritizable x, IPrioritizable y) => y.Priority - x.Priority; + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/SortedCollection.cs b/MonkeyLoader/SortedCollection.cs index 946ca5e..1016486 100644 --- a/MonkeyLoader/SortedCollection.cs +++ b/MonkeyLoader/SortedCollection.cs @@ -117,7 +117,7 @@ public SortedCollection GetRange(int index, int count) /// The first index of the item if it was found; otherwise, -1. public int IndexOf(T item) { - var i = ÍndexOfEqualityClass(item); + var i = IndexOfEqualityClass(item); if (i < 0) return -1; @@ -137,7 +137,7 @@ public int IndexOf(T item) ///
/// The item to search for. /// The last index if equal items are found; otherwise, -1. - public int ÍndexOfEqualityClass(T needle) + public int IndexOfEqualityClass(T needle) { var i = IndexInEqualityClass(needle);