Skip to content

Commit

Permalink
Add dynamic Event source / handler system
Browse files Browse the repository at this point in the history
Co-authored-by: Colin Barndt <[email protected]>
  • Loading branch information
Banane9 and ColinTimBarndt committed Apr 11, 2024
1 parent bb471b3 commit a9b4b11
Show file tree
Hide file tree
Showing 14 changed files with 565 additions and 5 deletions.
65 changes: 65 additions & 0 deletions MonkeyLoader/AnyMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonkeyLoader
{
/// <summary>
/// Represents a type safe dictionary of Types to objects of the type.
/// </summary>
public sealed class AnyMap
{
private readonly Dictionary<Type, object?> _dict = new();

public IEnumerable<Type> Keys => _dict.Keys;

Check warning on line 16 in MonkeyLoader/AnyMap.cs

View workflow job for this annotation

GitHub Actions / ci

Missing XML comment for publicly visible type or member 'AnyMap.Keys'

public void Add<T>(T value) => _dict.Add(typeof(T), value);

Check warning on line 18 in MonkeyLoader/AnyMap.cs

View workflow job for this annotation

GitHub Actions / ci

Missing XML comment for publicly visible type or member 'AnyMap.Add<T>(T)'

public void Clear() => _dict.Clear();

Check warning on line 20 in MonkeyLoader/AnyMap.cs

View workflow job for this annotation

GitHub Actions / ci

Missing XML comment for publicly visible type or member 'AnyMap.Clear()'

public bool ContainsKey<T>() => _dict.ContainsKey(typeof(T));

Check warning on line 22 in MonkeyLoader/AnyMap.cs

View workflow job for this annotation

GitHub Actions / ci

Missing XML comment for publicly visible type or member 'AnyMap.ContainsKey<T>()'

public IEnumerable<T> GetCastableValues<T>()
=> _dict.Values.SelectCastable<object, T>();

public T GetOrCreateValue<T>() where T : new()
{
if (TryGetValue<T>(out var value))
return value!;

value = new T();
Add(value);

return value;
}

public T GetOrCreateValue<T>(Func<T> valueFactory)
{
if (TryGetValue<T>(out var value))
return value!;

value = valueFactory();
Add(value);

return value;
}

public T GetValue<T>() => (T)_dict[typeof(T)]!;

public void Remove<T>() => _dict.Remove(typeof(T));

public bool TryGetValue<T>(out T? value)
{
if (_dict.TryGetValue(typeof(T), out var obj))
{
value = (T)obj!;
return true;
}

value = default;
return false;
}
}
}
27 changes: 25 additions & 2 deletions MonkeyLoader/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,29 @@ public static string Format(this Exception ex)
};
}

public static TValue GetOrCreateValue<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TValue> valueFactory)
{
if (dictionary.TryGetValue(key, out var value))
return value;

value = valueFactory();
dictionary.Add(key, value);

return value;
}

public static TValue GetOrCreateValue<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
where TValue : new()
{
if (dictionary.TryGetValue(key, out var value))
return value;

value = new TValue();
dictionary.Add(key, value);

return value;
}

/// <summary>
/// Filters a source sequence of <see cref="Type"/>s to only contain the ones instantiable
/// without parameters and assignable to <typeparamref name="TInstance"/>.
Expand Down Expand Up @@ -189,8 +212,8 @@ public static IEnumerable<Type> Instantiable<TInstance>(this IEnumerable<Type> t
/// <typeparam name="TFrom">The items in the source sequence.</typeparam>
/// <typeparam name="TTo">The items in the result sequence.</typeparam>
/// <param name="source">The items to try and cast.</param>
/// <returns>All items from the source that were castable to <typeparamref name="TTo"/>.</returns>
public static IEnumerable<TTo> SelectCastable<TFrom, TTo>(this IEnumerable<TFrom> source)
/// <returns>All items from the source that were castable to <typeparamref name="TTo"/> and not <c>null</c>.</returns>
public static IEnumerable<TTo> SelectCastable<TFrom, TTo>(this IEnumerable<TFrom?> source)
{
foreach (var item in source)
{
Expand Down
165 changes: 165 additions & 0 deletions MonkeyLoader/Events/EventDispatcher.cs
Original file line number Diff line number Diff line change
@@ -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<TEvent, TTarget>
: EventDispatcherBase<ICancelableEventSource<TEvent, TTarget>, ICancelableEventHandler<TEvent, TTarget>>
where TEvent : ICancelableEvent<TTarget>
{
public CancelableEventDispatcher(EventManager manager) : base(manager)
{ }

protected override void AddSource(ICancelableEventSource<TEvent, TTarget> eventSource)
=> eventSource.Dispatched += DispatchEvents;

protected override void RemoveSource(ICancelableEventSource<TEvent, TTarget> 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<TEvent, TTarget>
: EventDispatcherBase<IEventSource<TEvent, TTarget>, IEventHandler<TEvent, TTarget>>
where TEvent : IEvent<TTarget>
{
public EventDispatcher(EventManager manager) : base(manager)
{ }

protected override void AddSource(IEventSource<TEvent, TTarget> eventSource)
=> eventSource.Dispatched += DispatchEvents;

protected override void RemoveSource(IEventSource<TEvent, TTarget> 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<TSource, THandler> : IEventDispatcher
where THandler : IPrioritizable
{
protected readonly SortedCollection<THandler> handlers = new((IComparer<THandler>)PriorityHelper.Comparer);

private readonly Dictionary<Mod, HashSet<THandler>> _handlersByMod = new();
private readonly EventManager _manager;
private readonly Dictionary<Mod, HashSet<TSource>> _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);
}
}
76 changes: 76 additions & 0 deletions MonkeyLoader/Events/EventManager.cs
Original file line number Diff line number Diff line change
@@ -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<TEvent, TTarget>(Mod mod, IEventHandler<TEvent, TTarget> eventHandler)
where TEvent : IEvent<TTarget>
{
ValidateLoader(mod);

_eventDispatchers.GetOrCreateValue(CreateDispatcher<TEvent, TTarget>).AddHandler(mod, eventHandler);
}

internal void RegisterEventHandler<TEvent, TTarget>(Mod mod, ICancelableEventHandler<TEvent, TTarget> cancelableEventHandler)
where TEvent : ICancelableEvent<TTarget>
{
ValidateLoader(mod);

_eventDispatchers.GetOrCreateValue(CreateCancelableDispatcher<TEvent, TTarget>).AddHandler(mod, cancelableEventHandler);
}

internal void RegisterEventSource<TEvent, TTarget>(Mod mod, IEventSource<TEvent, TTarget> eventSource)
where TEvent : IEvent<TTarget>
{
ValidateLoader(mod);

_eventDispatchers.GetOrCreateValue(CreateDispatcher<TEvent, TTarget>).AddSource(mod, eventSource);
}

internal void RegisterEventSource<TEvent, TTarget>(Mod mod, ICancelableEventSource<TEvent, TTarget> cancelableEventSource)
where TEvent : ICancelableEvent<TTarget>
{
ValidateLoader(mod);

_eventDispatchers.GetOrCreateValue(CreateCancelableDispatcher<TEvent, TTarget>).AddSource(mod, cancelableEventSource);
}

internal void UnregisterMod(Mod mod)
{
ValidateLoader(mod);

foreach (var eventDispatcher in _eventDispatchers.GetCastableValues<IEventDispatcher>())
eventDispatcher.UnregisterMod(mod);
}

private CancelableEventDispatcher<TEvent, TTarget> CreateCancelableDispatcher<TEvent, TTarget>()
where TEvent : ICancelableEvent<TTarget> => new(this);

private EventDispatcher<TEvent, TTarget> CreateDispatcher<TEvent, TTarget>()
where TEvent : IEvent<TTarget> => new(this);

private void ValidateLoader(Mod mod)
{
if (mod.Loader != _loader)
throw new InvalidOperationException("Can't register event handler of mod from another loader!");
}
}
}
18 changes: 18 additions & 0 deletions MonkeyLoader/Events/IEvent.cs
Original file line number Diff line number Diff line change
@@ -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<out TTarget> : IEvent<TTarget>
{
public bool Canceled { get; set; }
}

public interface IEvent<out TTarget>
{
public TTarget Target { get; }
}
}
22 changes: 22 additions & 0 deletions MonkeyLoader/Events/IEventHandler.cs
Original file line number Diff line number Diff line change
@@ -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<in TEvent, out TTarget> : IPrioritizable
where TEvent : ICancelableEvent<TTarget>
{
public bool SkipCanceled { get; }

public void Handle(TEvent eventArgs);
}

public interface IEventHandler<in TEvent, out TTarget> : IPrioritizable
where TEvent : IEvent<TTarget>
{
public void Handle(TEvent eventArgs);
}
}
Loading

0 comments on commit a9b4b11

Please sign in to comment.