diff --git a/Hypercube.Client/Graphics/Event/MainWindowClosedEvent.cs b/Hypercube.Client/Graphics/Event/MainWindowClosedEvent.cs index d24029c..1372c1e 100644 --- a/Hypercube.Client/Graphics/Event/MainWindowClosedEvent.cs +++ b/Hypercube.Client/Graphics/Event/MainWindowClosedEvent.cs @@ -1,8 +1,9 @@ using Hypercube.Client.Graphics.Windows; +using Hypercube.Shared.EventBus.Events; namespace Hypercube.Client.Graphics.Event; -public readonly struct MainWindowClosedEvent(WindowRegistration registration) +public readonly struct MainWindowClosedEvent(WindowRegistration registration) : IEventArgs { public readonly WindowRegistration Registration = registration; } \ No newline at end of file diff --git a/Hypercube.Client/Graphics/Event/WindowClosed.cs b/Hypercube.Client/Graphics/Event/WindowClosed.cs index f7d2b31..dccaab7 100644 --- a/Hypercube.Client/Graphics/Event/WindowClosed.cs +++ b/Hypercube.Client/Graphics/Event/WindowClosed.cs @@ -1,8 +1,9 @@ using Hypercube.Client.Graphics.Windows; +using Hypercube.Shared.EventBus.Events; namespace Hypercube.Client.Graphics.Event; -public readonly struct WindowClosedEvent(WindowRegistration registration) +public readonly struct WindowClosedEvent(WindowRegistration registration) : IEventArgs { public readonly WindowRegistration Registration = registration; } \ No newline at end of file diff --git a/Hypercube.Client/Graphics/Event/WindowFocusChangedEvent.cs b/Hypercube.Client/Graphics/Event/WindowFocusChangedEvent.cs index 3628687..6fb3e33 100644 --- a/Hypercube.Client/Graphics/Event/WindowFocusChangedEvent.cs +++ b/Hypercube.Client/Graphics/Event/WindowFocusChangedEvent.cs @@ -1,5 +1,6 @@ using Hypercube.Client.Graphics.Windows; +using Hypercube.Shared.EventBus.Events; namespace Hypercube.Client.Graphics.Event; -public readonly record struct WindowFocusChangedEvent(WindowRegistration Registration, bool Focused); \ No newline at end of file +public readonly record struct WindowFocusChangedEvent(WindowRegistration Registration, bool Focused) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Client/Graphics/Rendering/Renderer.Render.cs b/Hypercube.Client/Graphics/Rendering/Renderer.Render.cs index dc5a1c6..3ded839 100644 --- a/Hypercube.Client/Graphics/Rendering/Renderer.Render.cs +++ b/Hypercube.Client/Graphics/Rendering/Renderer.Render.cs @@ -65,7 +65,7 @@ private void OnLoad() _logger.EngineInfo("Loaded"); } - private void OnFrameUpdate(UpdateFrameEvent args) + private void OnFrameUpdate(ref UpdateFrameEvent args) { #if DEBUG _windowManager.WindowSetTitle(MainWindow, $"FPS: {_timing.Fps} | RealTime: {_timing.RealTime} | cPos: {_cameraManager.MainCamera?.Position ?? null} | cRot: {_cameraManager.MainCamera?.Rotation ?? null}"); @@ -74,7 +74,7 @@ private void OnFrameUpdate(UpdateFrameEvent args) _cameraManager.UpdateInput(_cameraManager.MainCamera, args.DeltaSeconds); } - private void OnFrameRender(RenderFrameEvent args) + private void OnFrameRender(ref RenderFrameEvent args) { BatchClear(); diff --git a/Hypercube.Client/Graphics/Rendering/Renderer.Window.cs b/Hypercube.Client/Graphics/Rendering/Renderer.Window.cs index 0033816..2f15b1d 100644 --- a/Hypercube.Client/Graphics/Rendering/Renderer.Window.cs +++ b/Hypercube.Client/Graphics/Rendering/Renderer.Window.cs @@ -39,11 +39,11 @@ public void DestroyWindow(WindowRegistration registration) public void CloseWindow(WindowRegistration registration) { - _eventBus.Invoke(new WindowClosedEvent(registration)); + _eventBus.Raise(new WindowClosedEvent(registration)); if (registration.Id == _mainWindowId) { - _eventBus.Invoke(new MainWindowClosedEvent(registration)); + _eventBus.Raise(new MainWindowClosedEvent(registration)); return; } @@ -84,6 +84,6 @@ private bool InitMainWindow(ContextInfo? context, WindowCreateSettings settings) public void OnFocusChanged(WindowRegistration window, bool focused) { - _eventBus.Invoke(new WindowFocusChangedEvent(window, focused)); + _eventBus.Raise(new WindowFocusChangedEvent(window, focused)); } } \ No newline at end of file diff --git a/Hypercube.Client/Graphics/Rendering/Renderer.cs b/Hypercube.Client/Graphics/Rendering/Renderer.cs index a350fa7..bf775e4 100644 --- a/Hypercube.Client/Graphics/Rendering/Renderer.cs +++ b/Hypercube.Client/Graphics/Rendering/Renderer.cs @@ -6,6 +6,7 @@ using Hypercube.Client.Graphics.Windows.Manager; using Hypercube.Shared.Dependency; using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; using Hypercube.Shared.Logging; using Hypercube.Shared.Resources.Manager; using Hypercube.Shared.Runtimes.Event; @@ -17,7 +18,7 @@ namespace Hypercube.Client.Graphics.Rendering; -public sealed partial class Renderer : IRenderer, IPostInject +public sealed partial class Renderer : IRenderer, IPostInject, IEventSubscriber { [Dependency] private readonly IEventBus _eventBus = default!; [Dependency] private readonly ITextureManager _textureManager = default!; @@ -68,19 +69,19 @@ public sealed partial class Renderer : IRenderer, IPostInject public void PostInject() { - _eventBus.Subscribe(OnInitialization); - _eventBus.Subscribe(OnStartup); - _eventBus.Subscribe(OnFrameUpdate); - _eventBus.Subscribe(OnFrameRender); + _eventBus.Subscribe(this, OnInitialization); + _eventBus.Subscribe(this, OnStartup); + _eventBus.Subscribe(this, OnFrameUpdate); + _eventBus.Subscribe(this, OnFrameRender); } - private void OnInitialization(RuntimeInitializationEvent args) + private void OnInitialization(ref RuntimeInitializationEvent args) { _windowManager = CreateWindowManager(); _bindingsContext = new BindingsContext(_windowManager); } - private void OnStartup(RuntimeStartupEvent args) + private void OnStartup(ref RuntimeStartupEvent args) { _currentThread = Thread.CurrentThread; _logger.EngineInfo($"Working thread {_currentThread.Name}"); diff --git a/Hypercube.Client/Graphics/Windows/Manager/GlfwWindowManager.Callbacks.cs b/Hypercube.Client/Graphics/Windows/Manager/GlfwWindowManager.Callbacks.cs index 22e20f1..a11c77b 100644 --- a/Hypercube.Client/Graphics/Windows/Manager/GlfwWindowManager.Callbacks.cs +++ b/Hypercube.Client/Graphics/Windows/Manager/GlfwWindowManager.Callbacks.cs @@ -94,4 +94,12 @@ private void OnWindowFocusChanged(Window* window, bool focused) _renderer.OnFocusChanged(registration, focused); } + private void OnWindowFocused(Window* window, bool focused) + { + if (!TryGetWindow(window, out var registration)) + return; + + _renderer.OnFocusChanged(registration, focused); + } + } \ No newline at end of file diff --git a/Hypercube.Client/Runtimes/Loop/RuntimeLoop.cs b/Hypercube.Client/Runtimes/Loop/RuntimeLoop.cs index 163a78e..1caf37d 100644 --- a/Hypercube.Client/Runtimes/Loop/RuntimeLoop.cs +++ b/Hypercube.Client/Runtimes/Loop/RuntimeLoop.cs @@ -20,10 +20,10 @@ public void Run() _timing.StartFrame(); var deltaTime = (float)_timing.RealFrameTime.TotalSeconds; - _eventBus.Invoke(new InputFrameEvent(deltaTime)); - _eventBus.Invoke(new TickFrameEvent(deltaTime)); - _eventBus.Invoke(new UpdateFrameEvent(deltaTime)); - _eventBus.Invoke(new RenderFrameEvent(deltaTime)); + _eventBus.Raise(new InputFrameEvent(deltaTime)); + _eventBus.Raise(new TickFrameEvent(deltaTime)); + _eventBus.Raise(new UpdateFrameEvent(deltaTime)); + _eventBus.Raise(new RenderFrameEvent(deltaTime)); } } diff --git a/Hypercube.Client/Runtimes/Runtime.cs b/Hypercube.Client/Runtimes/Runtime.cs index 1473661..7627118 100644 --- a/Hypercube.Client/Runtimes/Runtime.cs +++ b/Hypercube.Client/Runtimes/Runtime.cs @@ -1,15 +1,14 @@ -using Hypercube.Client.Graphics; -using Hypercube.Client.Graphics.Event; -using Hypercube.Client.Graphics.Rendering; +using Hypercube.Client.Graphics.Event; using Hypercube.Client.Runtimes.Loop; using Hypercube.Shared.Dependency; using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; using Hypercube.Shared.Logging; using Hypercube.Shared.Runtimes.Event; namespace Hypercube.Client.Runtimes; -public sealed partial class Runtime(DependenciesContainer dependenciesContainer) : IPostInject +public sealed partial class Runtime(DependenciesContainer dependenciesContainer) : IPostInject, IEventSubscriber { [Dependency] private readonly IEventBus _eventBus = default!; [Dependency] private readonly IRuntimeLoop _loop = default!; @@ -18,7 +17,7 @@ public sealed partial class Runtime(DependenciesContainer dependenciesContainer) public void PostInject() { - _eventBus.Subscribe(OnMainWindowClosed); + _eventBus.Subscribe(this, OnMainWindowClosed); } /// @@ -43,24 +42,24 @@ private void Shutdown(string? reason = null) reason = reason is null ? "Shutting down" : $"Shutting down, reason: {reason}"; _logger.EngineInfo(reason); - _eventBus.Invoke(new RuntimeShutdownEvent(reason)); + _eventBus.Raise(new RuntimeShutdownEvent(reason)); _loop.Shutdown(); } private void RunLoop() { _logger.EngineInfo("Startup"); - _eventBus.Invoke(new RuntimeStartupEvent()); + _eventBus.Raise(new RuntimeStartupEvent()); _loop.Run(); } private void Initialize() { _logger.EngineInfo("Initialize"); - _eventBus.Invoke(new RuntimeInitializationEvent()); + _eventBus.Raise(new RuntimeInitializationEvent()); } - private void OnMainWindowClosed(MainWindowClosedEvent obj) + private void OnMainWindowClosed(ref MainWindowClosedEvent obj) { Shutdown("Main window closed"); } diff --git a/Hypercube.Shared/Entities/Realisation/EventBus/EntitiesEventBus.cs b/Hypercube.Shared/Entities/Realisation/EventBus/EntitiesEventBus.cs index 7038b0b..72552d2 100644 --- a/Hypercube.Shared/Entities/Realisation/EventBus/EntitiesEventBus.cs +++ b/Hypercube.Shared/Entities/Realisation/EventBus/EntitiesEventBus.cs @@ -1,18 +1,32 @@ -namespace Hypercube.Shared.Entities.Realisation.EventBus; +using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; +using Hypercube.Shared.EventBus.Handlers; + +namespace Hypercube.Shared.Entities.Realisation.EventBus; public sealed class EntitiesEventBus : IEntitiesEventBus { - public void Subscribe(Action callback) + public void Unsubscribe(IEventSubscriber subscriber) where T : IEventArgs + { + throw new NotImplementedException(); + } + + public void Subscribe(IEventSubscriber subscriber, EventRefHandler refHandler) where T : IEventArgs + { + throw new NotImplementedException(); + } + + public void Raise(IEventArgs receiver) { throw new NotImplementedException(); } - public void Unsubscribe(Action callback) + public void Raise(ref T receiver) where T : IEventArgs { throw new NotImplementedException(); } - public void Invoke(T signal) + public void Raise(T receiver) where T : IEventArgs { throw new NotImplementedException(); } diff --git a/Hypercube.Shared/Entities/Realisation/Events/ComponentAdded.cs b/Hypercube.Shared/Entities/Realisation/Events/ComponentAdded.cs index 1919f2e..8cb3204 100644 --- a/Hypercube.Shared/Entities/Realisation/Events/ComponentAdded.cs +++ b/Hypercube.Shared/Entities/Realisation/Events/ComponentAdded.cs @@ -1,5 +1,6 @@ using Hypercube.Shared.Entities.Realisation.Components; +using Hypercube.Shared.EventBus.Events; namespace Hypercube.Shared.Entities.Realisation.Events; -public readonly record struct ComponentAdded(EntityUid EntityUid, IComponent Component); \ No newline at end of file +public readonly record struct ComponentAdded(EntityUid EntityUid, IComponent Component) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Entities/Realisation/Events/EntityAdded.cs b/Hypercube.Shared/Entities/Realisation/Events/EntityAdded.cs index b4bfa6a..26b9ccb 100644 --- a/Hypercube.Shared/Entities/Realisation/Events/EntityAdded.cs +++ b/Hypercube.Shared/Entities/Realisation/Events/EntityAdded.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Entities.Realisation.Events; +using Hypercube.Shared.EventBus.Events; -public readonly record struct EntityAdded(EntityUid EntityUid); \ No newline at end of file +namespace Hypercube.Shared.Entities.Realisation.Events; + +public readonly record struct EntityAdded(EntityUid EntityUid) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Entities/Realisation/Events/EntityRemoved.cs b/Hypercube.Shared/Entities/Realisation/Events/EntityRemoved.cs index edaa062..5f48b66 100644 --- a/Hypercube.Shared/Entities/Realisation/Events/EntityRemoved.cs +++ b/Hypercube.Shared/Entities/Realisation/Events/EntityRemoved.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Entities.Realisation.Events; +using Hypercube.Shared.EventBus.Events; -public readonly record struct EntityRemoved(EntityUid EntityUid); \ No newline at end of file +namespace Hypercube.Shared.Entities.Realisation.Events; + +public readonly record struct EntityRemoved(EntityUid EntityUid) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Entities/Realisation/Manager/EntitiesComponentManager.cs b/Hypercube.Shared/Entities/Realisation/Manager/EntitiesComponentManager.cs index 2ce8261..9a0f049 100644 --- a/Hypercube.Shared/Entities/Realisation/Manager/EntitiesComponentManager.cs +++ b/Hypercube.Shared/Entities/Realisation/Manager/EntitiesComponentManager.cs @@ -4,12 +4,13 @@ using Hypercube.Shared.Entities.Realisation.Components; using Hypercube.Shared.Entities.Realisation.Events; using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; using Hypercube.Shared.Runtimes.Event; using Hypercube.Shared.Utilities.Helpers; namespace Hypercube.Shared.Entities.Realisation.Manager; -public sealed class EntitiesComponentManager : IEntitiesComponentManager, IPostInject +public sealed class EntitiesComponentManager : IEntitiesComponentManager, IPostInject, IEventSubscriber { private static readonly Type BaseComponentType = typeof(IComponent); @@ -20,10 +21,10 @@ public sealed class EntitiesComponentManager : IEntitiesComponentManager, IPostI public void PostInject() { - _eventBus.Subscribe(OnInitialized); + _eventBus.Subscribe(this, OnInitialized); } - private void OnInitialized(RuntimeInitializationEvent args) + private void OnInitialized(ref RuntimeInitializationEvent args) { _components = ReflectionHelper.GetAllInstantiableSubclassOf(BaseComponentType); @@ -72,7 +73,7 @@ private object AddComponent(EntityUid entityUid, Type type) var instance = (IComponent)constructor.Invoke(Array.Empty()) ?? throw new NullReferenceException(); components.Add(entityUid, instance); - _eventBus.Invoke(new ComponentAdded(entityUid, instance)); + _eventBus.Raise(new ComponentAdded(entityUid, instance)); return instance; } diff --git a/Hypercube.Shared/Entities/Realisation/Manager/EntitiesManager.cs b/Hypercube.Shared/Entities/Realisation/Manager/EntitiesManager.cs index fce2822..4350701 100644 --- a/Hypercube.Shared/Entities/Realisation/Manager/EntitiesManager.cs +++ b/Hypercube.Shared/Entities/Realisation/Manager/EntitiesManager.cs @@ -31,7 +31,7 @@ public EntityUid Create(string name, SceneCoordinates coordinates) var transformComponent = _entitiesComponentManager.AddComponent(newEntity); _entities.Add(newEntity); - _eventBus.Invoke(new EntityAdded(newEntity)); + _eventBus.Raise(new EntityAdded(newEntity)); return newEntity; } @@ -39,6 +39,6 @@ public EntityUid Create(string name, SceneCoordinates coordinates) public void Delete(EntityUid entityUid) { _entities.Remove(entityUid); - _eventBus.Invoke(new EntityRemoved(entityUid)); + _eventBus.Raise(new EntityRemoved(entityUid)); } } \ No newline at end of file diff --git a/Hypercube.Shared/Entities/Realisation/Manager/EntitiesSystemManager.cs b/Hypercube.Shared/Entities/Realisation/Manager/EntitiesSystemManager.cs index 4d48682..2ff5357 100644 --- a/Hypercube.Shared/Entities/Realisation/Manager/EntitiesSystemManager.cs +++ b/Hypercube.Shared/Entities/Realisation/Manager/EntitiesSystemManager.cs @@ -2,13 +2,14 @@ using Hypercube.Shared.Dependency; using Hypercube.Shared.Entities.Realisation.Systems; using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; using Hypercube.Shared.Runtimes.Event; using Hypercube.Shared.Runtimes.Loop.Event; using Hypercube.Shared.Utilities.Helpers; namespace Hypercube.Shared.Entities.Realisation.Manager; -public class EntitiesSystemManager : IEntitiesSystemManager, IPostInject +public class EntitiesSystemManager : IEntitiesSystemManager, IPostInject, IEventSubscriber { [Dependency] private readonly IEventBus _eventBus = default!; @@ -19,14 +20,14 @@ public class EntitiesSystemManager : IEntitiesSystemManager, IPostInject public void PostInject() { - _eventBus.Subscribe(OnInitialization); - _eventBus.Subscribe(OnStartup); + _eventBus.Subscribe(this, OnInitialization); + _eventBus.Subscribe(this, OnStartup); - _eventBus.Subscribe(OnFrameUpdate); - _eventBus.Subscribe(OnShutdown); + _eventBus.Subscribe(this, OnFrameUpdate); + _eventBus.Subscribe(this, OnShutdown); } - private void OnInitialization(RuntimeInitializationEvent args) + private void OnInitialization(ref RuntimeInitializationEvent args) { // Auto creating all systems var types = GetAllSystemTypes(); @@ -45,7 +46,7 @@ private void OnInitialization(RuntimeInitializationEvent args) _system = system.ToFrozenSet(); } - private void OnStartup(RuntimeStartupEvent args) + private void OnStartup(ref RuntimeStartupEvent args) { foreach (var instance in _system) { @@ -53,7 +54,7 @@ private void OnStartup(RuntimeStartupEvent args) } } - private void OnFrameUpdate(UpdateFrameEvent args) + private void OnFrameUpdate(ref UpdateFrameEvent args) { foreach (var instance in _system) { @@ -61,7 +62,7 @@ private void OnFrameUpdate(UpdateFrameEvent args) } } - private void OnShutdown(RuntimeShutdownEvent args) + private void OnShutdown(ref RuntimeShutdownEvent args) { foreach (var instance in _system) { diff --git a/Hypercube.Shared/EventBus/Broadcast/BroadcastRegistration.cs b/Hypercube.Shared/EventBus/Broadcast/BroadcastRegistration.cs new file mode 100644 index 0000000..4e32d84 --- /dev/null +++ b/Hypercube.Shared/EventBus/Broadcast/BroadcastRegistration.cs @@ -0,0 +1,30 @@ +using Hypercube.Shared.EventBus.Handlers; + +namespace Hypercube.Shared.EventBus.Broadcast; + +public sealed class BroadcastRegistration : IEquatable +{ + public RefHandler Handler { get; } + public object Equality { get; } + + public BroadcastRegistration(RefHandler refHandler, object equalityObj) + { + Handler = refHandler; + Equality = equalityObj; + } + + public bool Equals(BroadcastRegistration? other) + { + return other is not null && Equals(other.Equality, Equality); + } + + public override bool Equals(object? obj) + { + return obj is BroadcastRegistration registration && Equals(registration); + } + + public override int GetHashCode() + { + return Equality.GetHashCode(); + } +} \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/EventBus.cs b/Hypercube.Shared/EventBus/EventBus.cs index ffbf916..00cfba7 100644 --- a/Hypercube.Shared/EventBus/EventBus.cs +++ b/Hypercube.Shared/EventBus/EventBus.cs @@ -1,36 +1,130 @@ -namespace Hypercube.Shared.EventBus; +using System.Runtime.CompilerServices; +using Hypercube.Shared.EventBus.Broadcast; +using Hypercube.Shared.EventBus.Events; +using Hypercube.Shared.EventBus.Exceptions; +using Hypercube.Shared.EventBus.Handlers; +using Hypercube.Shared.Utilities; +using Hypercube.Shared.Utilities.Units; -// TODO: BURNING IN HELL SHIT!!!! Rewrite it -public class EventBus : IEventBus +namespace Hypercube.Shared.EventBus; + +public sealed class EventBus : IEventBus { - private readonly Dictionary> _callbacks = new(); + private readonly Dictionary _eventRegistration = new(); + private readonly Dictionary> _inverseEventSubscriptions = new(); + + public void Raise(ref T receiver) where T : IEventArgs + { + ProcessEvent(ref Unsafe.As(ref receiver), typeof(T)); + } + + public void Raise(T receiver) where T : IEventArgs + { + ProcessEvent(ref Unsafe.As(ref receiver), typeof(T)); + } + + public void Raise(IEventArgs eventArgs) + { + var eventType = eventArgs.GetType(); + ref var unitRef = ref UnitHelper.ExtractUnitRef(ref eventArgs, eventType); + + ProcessEvent(ref unitRef, eventType); + } + + public void Subscribe(IEventSubscriber subscriber, EventRefHandler refHandler) where T : IEventArgs + { + SubscribeEventCommon(subscriber, (ref Unit ev) => + { + ref var tev = ref Unsafe.As(ref ev); + refHandler(ref tev); + }, refHandler); + } - public void Subscribe(Action callback) + /// Throws when subscriber is null + /// + private void SubscribeEventCommon(IEventSubscriber subscriber, RefHandler refHandler, object equality) + where T : IEventArgs { - GetListeners().Add(callback); + ArgumentNullException.ThrowIfNull(subscriber); + + var eventType = typeof(T); + var subscription = new BroadcastRegistration(refHandler, equality); + + var subscriptions = GetEventRegistration(eventType); + subscriptions.Add(subscription); + + var inverseSubscriptions = GetEventInverseSubscription(subscriber); + if (inverseSubscriptions.TryAdd(eventType, subscription)) + return; + + throw new InvalidOperationException(); } - public void Unsubscribe(Action callback) + /// Throws when subscriber is null + /// + public void Unsubscribe(IEventSubscriber subscriber) where T : IEventArgs { - GetListeners().Remove(callback); + ArgumentNullException.ThrowIfNull(subscriber); + + var eventType = typeof(T); + + var inverseSubscriptions = GetEventInverseSubscription(subscriber); + if (!inverseSubscriptions.TryGetValue(eventType, out var registration)) + throw new InvalidOperationException(); + + Unsubscribe(eventType, registration, subscriber); } - public void Invoke(T signal) + /// + /// + private void Unsubscribe(Type eventType, BroadcastRegistration registration, IEventSubscriber subscriber) + { + var eventRegistration = GetEventRegistration(eventType, false); + eventRegistration.Remove(registration); + + var inverseSubscriptions = GetEventInverseSubscription(subscriber, false); + inverseSubscriptions.Remove(eventType); + } + + private void ProcessEvent(ref Unit unitRef, Type eventType) + { + if (!_eventRegistration.TryGetValue(eventType, out var registration)) + return; + + ProcessEventCore(ref unitRef, registration); + } + + private void ProcessEventCore(ref Unit unitRef, EventRegistration registration) { - foreach (var reference in GetListeners()) + foreach (var handler in registration.BroadcastRegistrations) { - if (reference is not Action action) - continue; - - action.Invoke(signal); + handler.Handler(ref unitRef); } } + + /// Type of event whose registration we want to receive. + /// Allows you to control the automatic registration of an event if it does not exist. + /// If autoRegistration is false, it will throw an exception if registration is not found. + private EventRegistration GetEventRegistration(Type eventType, bool autoRegistration = true) + { + if (_eventRegistration.TryGetValue(eventType, out var found)) + return found; + + if (!autoRegistration) + throw new UnregisteredEventException(eventType); + + return _eventRegistration[eventType] = new EventRegistration(); + } - private HashSet GetListeners() + /// + private Dictionary GetEventInverseSubscription(IEventSubscriber subscriber, bool creating = true) { - if (_callbacks.TryGetValue(typeof(T), out var listeners)) - return listeners; + if (_inverseEventSubscriptions.TryGetValue(subscriber, out var subscriptions)) + return subscriptions; - return _callbacks[typeof(T)] = new HashSet(); + if (!creating) + throw new InvalidOperationException(); + + return _inverseEventSubscriptions[subscriber] = new Dictionary(); } } \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/EventRegistration.cs b/Hypercube.Shared/EventBus/EventRegistration.cs new file mode 100644 index 0000000..89edfa9 --- /dev/null +++ b/Hypercube.Shared/EventBus/EventRegistration.cs @@ -0,0 +1,31 @@ +using Hypercube.Shared.EventBus.Broadcast; + +namespace Hypercube.Shared.EventBus; + +/// +/// Saves information about a specific event. +/// +public readonly struct EventRegistration() +{ + private readonly HashSet _broadcastRegistrations = new(); + + public IReadOnlySet BroadcastRegistrations => _broadcastRegistrations; + + /// + public void Add(BroadcastRegistration registration) + { + if (_broadcastRegistrations.Contains(registration)) + throw new InvalidOperationException(); + + _broadcastRegistrations.Add(registration); + } + + /// + public void Remove(BroadcastRegistration registration) + { + if (!_broadcastRegistrations.Contains(registration)) + throw new InvalidOperationException(); + + _broadcastRegistrations.Remove(registration); + } +} \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/Events/CancellableEventArgs.cs b/Hypercube.Shared/EventBus/Events/CancellableEventArgs.cs new file mode 100644 index 0000000..0b7457e --- /dev/null +++ b/Hypercube.Shared/EventBus/Events/CancellableEventArgs.cs @@ -0,0 +1,16 @@ +namespace Hypercube.Shared.EventBus.Events; + +public abstract class CancellableEventArgs : EventArgs +{ + public bool Cancelled { get; private set; } + + public void Cancel() + { + Cancelled = true; + } + + public void UnCancel() + { + Cancelled = false; + } +} \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/Events/EventArgs.cs b/Hypercube.Shared/EventBus/Events/EventArgs.cs new file mode 100644 index 0000000..ca430bc --- /dev/null +++ b/Hypercube.Shared/EventBus/Events/EventArgs.cs @@ -0,0 +1,3 @@ +namespace Hypercube.Shared.EventBus.Events; + +public abstract class EventArgs : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/Events/IEventArgs.cs b/Hypercube.Shared/EventBus/Events/IEventArgs.cs new file mode 100644 index 0000000..8ed8b7e --- /dev/null +++ b/Hypercube.Shared/EventBus/Events/IEventArgs.cs @@ -0,0 +1,3 @@ +namespace Hypercube.Shared.EventBus.Events; + +public interface IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/Exceptions/UnregisteredEventException.cs b/Hypercube.Shared/EventBus/Exceptions/UnregisteredEventException.cs new file mode 100644 index 0000000..1c4dc87 --- /dev/null +++ b/Hypercube.Shared/EventBus/Exceptions/UnregisteredEventException.cs @@ -0,0 +1,4 @@ +namespace Hypercube.Shared.EventBus.Exceptions; + +public sealed class UnregisteredEventException(Type registrationType) : + Exception($"Attempted to resolve unregistered event {registrationType.FullName}."); \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/Handlers/EventRefHandler.cs b/Hypercube.Shared/EventBus/Handlers/EventRefHandler.cs new file mode 100644 index 0000000..8c26b80 --- /dev/null +++ b/Hypercube.Shared/EventBus/Handlers/EventRefHandler.cs @@ -0,0 +1,3 @@ +namespace Hypercube.Shared.EventBus.Handlers; + +public delegate void EventRefHandler(ref T ev); \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/Handlers/RefHandler.cs b/Hypercube.Shared/EventBus/Handlers/RefHandler.cs new file mode 100644 index 0000000..7559924 --- /dev/null +++ b/Hypercube.Shared/EventBus/Handlers/RefHandler.cs @@ -0,0 +1,5 @@ +using Hypercube.Shared.Utilities.Units; + +namespace Hypercube.Shared.EventBus.Handlers; + +public delegate void RefHandler(ref Unit ev); \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/IEventBus.cs b/Hypercube.Shared/EventBus/IEventBus.cs index c2e0b95..99b970c 100644 --- a/Hypercube.Shared/EventBus/IEventBus.cs +++ b/Hypercube.Shared/EventBus/IEventBus.cs @@ -1,8 +1,17 @@ -namespace Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; +using Hypercube.Shared.EventBus.Handlers; +namespace Hypercube.Shared.EventBus; + +/// +/// EventBus providing interaction between loosely coupled +/// components according to the principle of "event publisher -> event subscriber". +/// public interface IEventBus { - void Subscribe(Action callback); - void Unsubscribe(Action callback); - void Invoke(T signal); + void Raise(ref T receiver) where T : IEventArgs; + void Raise(T receiver) where T : IEventArgs; + void Raise(IEventArgs eventArgs); + void Subscribe(IEventSubscriber subscriber, EventRefHandler refHandler) where T : IEventArgs; + void Unsubscribe(IEventSubscriber subscriber) where T : IEventArgs; } \ No newline at end of file diff --git a/Hypercube.Shared/EventBus/IEventSubscriber.cs b/Hypercube.Shared/EventBus/IEventSubscriber.cs new file mode 100644 index 0000000..edfb654 --- /dev/null +++ b/Hypercube.Shared/EventBus/IEventSubscriber.cs @@ -0,0 +1,3 @@ +namespace Hypercube.Shared.EventBus; + +public interface IEventSubscriber; \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Event/RuntimeInitializationEvent.cs b/Hypercube.Shared/Runtimes/Event/RuntimeInitializationEvent.cs index a0691f0..bffad53 100644 --- a/Hypercube.Shared/Runtimes/Event/RuntimeInitializationEvent.cs +++ b/Hypercube.Shared/Runtimes/Event/RuntimeInitializationEvent.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Runtimes.Event; +using Hypercube.Shared.EventBus.Events; -public readonly record struct RuntimeInitializationEvent; \ No newline at end of file +namespace Hypercube.Shared.Runtimes.Event; + +public readonly record struct RuntimeInitializationEvent : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Event/RuntimeShutdownEvent.cs b/Hypercube.Shared/Runtimes/Event/RuntimeShutdownEvent.cs index e3c97bf..e89ee29 100644 --- a/Hypercube.Shared/Runtimes/Event/RuntimeShutdownEvent.cs +++ b/Hypercube.Shared/Runtimes/Event/RuntimeShutdownEvent.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Runtimes.Event; +using Hypercube.Shared.EventBus.Events; -public readonly record struct RuntimeShutdownEvent(string Reason); \ No newline at end of file +namespace Hypercube.Shared.Runtimes.Event; + +public readonly record struct RuntimeShutdownEvent(string Reason) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Event/RuntimeStartupEvent.cs b/Hypercube.Shared/Runtimes/Event/RuntimeStartupEvent.cs index 3985d95..7f3b673 100644 --- a/Hypercube.Shared/Runtimes/Event/RuntimeStartupEvent.cs +++ b/Hypercube.Shared/Runtimes/Event/RuntimeStartupEvent.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Runtimes.Event; +using Hypercube.Shared.EventBus.Events; -public readonly record struct RuntimeStartupEvent; \ No newline at end of file +namespace Hypercube.Shared.Runtimes.Event; + +public readonly record struct RuntimeStartupEvent : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Loop/Event/InputFrameEvent.cs b/Hypercube.Shared/Runtimes/Loop/Event/InputFrameEvent.cs index b58cc1f..4b67281 100644 --- a/Hypercube.Shared/Runtimes/Loop/Event/InputFrameEvent.cs +++ b/Hypercube.Shared/Runtimes/Loop/Event/InputFrameEvent.cs @@ -1,6 +1,8 @@ -namespace Hypercube.Shared.Runtimes.Loop.Event; +using Hypercube.Shared.EventBus.Events; -public readonly struct InputFrameEvent(float deltaSeconds) +namespace Hypercube.Shared.Runtimes.Loop.Event; + +public readonly struct InputFrameEvent(float deltaSeconds) : IEventArgs { public readonly float DeltaSeconds = deltaSeconds; } \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Loop/Event/RenderFrameEvent.cs b/Hypercube.Shared/Runtimes/Loop/Event/RenderFrameEvent.cs index cf76f7d..05d25ed 100644 --- a/Hypercube.Shared/Runtimes/Loop/Event/RenderFrameEvent.cs +++ b/Hypercube.Shared/Runtimes/Loop/Event/RenderFrameEvent.cs @@ -1,6 +1,8 @@ -namespace Hypercube.Shared.Runtimes.Loop.Event; +using Hypercube.Shared.EventBus.Events; -public readonly struct RenderFrameEvent(float deltaSeconds) +namespace Hypercube.Shared.Runtimes.Loop.Event; + +public readonly struct RenderFrameEvent(float deltaSeconds) : IEventArgs { public readonly float DeltaSeconds = deltaSeconds; } \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Loop/Event/TickFrameEvent.cs b/Hypercube.Shared/Runtimes/Loop/Event/TickFrameEvent.cs index 9a5e387..b74eaf0 100644 --- a/Hypercube.Shared/Runtimes/Loop/Event/TickFrameEvent.cs +++ b/Hypercube.Shared/Runtimes/Loop/Event/TickFrameEvent.cs @@ -1,6 +1,8 @@ -namespace Hypercube.Shared.Runtimes.Loop.Event; +using Hypercube.Shared.EventBus.Events; -public readonly struct TickFrameEvent(float deltaSeconds) +namespace Hypercube.Shared.Runtimes.Loop.Event; + +public readonly struct TickFrameEvent(float deltaSeconds) : IEventArgs { public readonly float DeltaSeconds = deltaSeconds; } \ No newline at end of file diff --git a/Hypercube.Shared/Runtimes/Loop/Event/UpdateFrameEvent.cs b/Hypercube.Shared/Runtimes/Loop/Event/UpdateFrameEvent.cs index 1251058..4c948c8 100644 --- a/Hypercube.Shared/Runtimes/Loop/Event/UpdateFrameEvent.cs +++ b/Hypercube.Shared/Runtimes/Loop/Event/UpdateFrameEvent.cs @@ -1,6 +1,8 @@ -namespace Hypercube.Shared.Runtimes.Loop.Event; +using Hypercube.Shared.EventBus.Events; -public readonly struct UpdateFrameEvent(float deltaSeconds) +namespace Hypercube.Shared.Runtimes.Loop.Event; + +public readonly struct UpdateFrameEvent(float deltaSeconds) : IEventArgs { public readonly float DeltaSeconds = deltaSeconds; } \ No newline at end of file diff --git a/Hypercube.Shared/Scenes/Events/SceneAdded.cs b/Hypercube.Shared/Scenes/Events/SceneAdded.cs index c77641e..3cbf68c 100644 --- a/Hypercube.Shared/Scenes/Events/SceneAdded.cs +++ b/Hypercube.Shared/Scenes/Events/SceneAdded.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Scenes.Events; +using Hypercube.Shared.EventBus.Events; -public readonly record struct SceneAdded(Scene Scene); \ No newline at end of file +namespace Hypercube.Shared.Scenes.Events; + +public readonly record struct SceneAdded(Scene Scene) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Scenes/Events/SceneDeleted.cs b/Hypercube.Shared/Scenes/Events/SceneDeleted.cs index 746e073..5d92020 100644 --- a/Hypercube.Shared/Scenes/Events/SceneDeleted.cs +++ b/Hypercube.Shared/Scenes/Events/SceneDeleted.cs @@ -1,3 +1,5 @@ -namespace Hypercube.Shared.Scenes.Events; +using Hypercube.Shared.EventBus.Events; -public readonly record struct SceneDeleted(Scene Scene); \ No newline at end of file +namespace Hypercube.Shared.Scenes.Events; + +public readonly record struct SceneDeleted(Scene Scene) : IEventArgs; \ No newline at end of file diff --git a/Hypercube.Shared/Scenes/Manager/SceneManager.cs b/Hypercube.Shared/Scenes/Manager/SceneManager.cs index 82fabcd..1be5252 100644 --- a/Hypercube.Shared/Scenes/Manager/SceneManager.cs +++ b/Hypercube.Shared/Scenes/Manager/SceneManager.cs @@ -21,7 +21,7 @@ public SceneId Create() var scene = new Scene(id); _scenes.Add(id, scene); - _eventBus.Invoke(new SceneAdded(scene)); + _eventBus.Raise(new SceneAdded(scene)); return id; } @@ -30,7 +30,7 @@ public void Delete(SceneId sceneId) { var scene = _scenes[sceneId]; _scenes.Remove(sceneId); - _eventBus.Invoke(new SceneDeleted(scene)); + _eventBus.Raise(new SceneDeleted(scene)); } public bool HasScene(SceneId sceneId) diff --git a/Hypercube.Shared/Utilities/Helpers/PathHelpers.cs b/Hypercube.Shared/Utilities/Helpers/PathHelpers.cs index 3067606..8817bcb 100644 --- a/Hypercube.Shared/Utilities/Helpers/PathHelpers.cs +++ b/Hypercube.Shared/Utilities/Helpers/PathHelpers.cs @@ -1,6 +1,4 @@ -using System.Reflection; - -namespace Hypercube.Shared.Utilities.Helpers; +namespace Hypercube.Shared.Utilities.Helpers; public static class PathHelpers { diff --git a/Hypercube.Shared/Utilities/UnitHelper.cs b/Hypercube.Shared/Utilities/UnitHelper.cs new file mode 100644 index 0000000..44926be --- /dev/null +++ b/Hypercube.Shared/Utilities/UnitHelper.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; +using Hypercube.Shared.Utilities.Units; + +namespace Hypercube.Shared.Utilities; + +public static class UnitHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref Unit ExtractUnitRef(ref T eventArgs, Type objType) + { + return ref Unsafe.As(ref eventArgs); + } +} \ No newline at end of file diff --git a/Hypercube.Shared/Utilities/Units/Unit.cs b/Hypercube.Shared/Utilities/Units/Unit.cs new file mode 100644 index 0000000..090528e --- /dev/null +++ b/Hypercube.Shared/Utilities/Units/Unit.cs @@ -0,0 +1,8 @@ +using System.Runtime.CompilerServices; + +namespace Hypercube.Shared.Utilities.Units; + +/// +/// Whenever you see this struct it some other object, it should be resolved using Unsafe.As() +/// +public readonly struct Unit; \ No newline at end of file diff --git a/Hypercube.Shared/Utilities/Units/UnitBox.cs b/Hypercube.Shared/Utilities/Units/UnitBox.cs new file mode 100644 index 0000000..1b64f2f --- /dev/null +++ b/Hypercube.Shared/Utilities/Units/UnitBox.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace Hypercube.Shared.Utilities.Units; + +/// +/// Whenever you see this it is some other object, it should be resolved using Unsafe.As() +/// +/// +/// Should be used whenever we want to pass value by ref +/// > +[StructLayout(LayoutKind.Sequential)] +public sealed class UnitBox +{ + public Unit Value; +} \ No newline at end of file diff --git a/Hypercube.UnitTests/EventBus/EventBusRaiseTests.cs b/Hypercube.UnitTests/EventBus/EventBusRaiseTests.cs new file mode 100644 index 0000000..3af5229 --- /dev/null +++ b/Hypercube.UnitTests/EventBus/EventBusRaiseTests.cs @@ -0,0 +1,56 @@ +using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; + +namespace Hypercube.UnitTests.EventBus; + +public static class EventBusRaiseTests +{ + [Test] + public static void Raising() + { + var eventBus = new Shared.EventBus.EventBus(); + + var subscriber = new TestEventSubscriber(eventBus); + subscriber.Subscribe(); + + eventBus.Raise(new TestSubEventClass()); + eventBus.Raise(new TestSubEventStruct()); + + subscriber.AssertPassed(); + } + + private sealed class TestEventSubscriber(IEventBus eventBus) : IEventSubscriber + { + private bool _classPassed; + private bool _structPassed; + + public void Subscribe() + { + eventBus.Subscribe(this, OnClass); + eventBus.Subscribe(this, OnStruct); + } + + private void OnClass(ref TestSubEventClass args) + { + _classPassed = true; + } + + private void OnStruct(ref TestSubEventStruct args) + { + _structPassed = true; + } + + public void AssertPassed() + { + Assert.Multiple(() => + { + Assert.That(_classPassed, Is.True); + Assert.That(_structPassed, Is.True); + }); + } + } + + private readonly record struct TestSubEventStruct : IEventArgs; + + private sealed class TestSubEventClass : IEventArgs; +} \ No newline at end of file diff --git a/Hypercube.UnitTests/EventBus/EventBusRefClassTests.cs b/Hypercube.UnitTests/EventBus/EventBusRefClassTests.cs new file mode 100644 index 0000000..babd170 --- /dev/null +++ b/Hypercube.UnitTests/EventBus/EventBusRefClassTests.cs @@ -0,0 +1,58 @@ +using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; +using EventArgs = Hypercube.Shared.EventBus.Events.EventArgs; + +namespace Hypercube.UnitTests.EventBus; + +public sealed class EventBusRefClassTests +{ + [Test] + public static void RefClass() + { + var eventBus = new Shared.EventBus.EventBus(); + + var subscriber1 = new TestRefClassSubscriber1(eventBus); + var subscriber2 = new TestRefClassSubscriber2(eventBus); + + subscriber1.Subscribe(); + subscriber2.Subscribe(); + + var args = new TestEventClass(); + eventBus.Raise(args); + + Assert.That(args.Counter, Is.EqualTo(2)); + Assert.Pass("All subscribers handled correctly"); + } + + private sealed class TestEventClass : Shared.EventBus.Events.EventArgs + { + public int Counter { get; set; } + } + + private sealed class TestRefClassSubscriber1(IEventBus bus) : IEventSubscriber + { + public void Subscribe() + { + bus.Subscribe(this, RefMethod2); + } + + private void RefMethod2(ref TestEventClass args) + { + args.Counter++; + } + } + + private sealed class TestRefClassSubscriber2(IEventBus bus) : IEventSubscriber + { + public void Subscribe() + { + bus.Subscribe(this, RefMethod1); + } + + private void RefMethod1(ref TestEventClass args) + { + Assert.That(args.Counter, Is.EqualTo(1)); + args.Counter++; + } + } +} \ No newline at end of file diff --git a/Hypercube.UnitTests/EventBus/EventBusRefStructTests.cs b/Hypercube.UnitTests/EventBus/EventBusRefStructTests.cs new file mode 100644 index 0000000..f54d7cf --- /dev/null +++ b/Hypercube.UnitTests/EventBus/EventBusRefStructTests.cs @@ -0,0 +1,54 @@ +using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; + +namespace Hypercube.UnitTests.EventBus; + +public static class EventBusRefStructTests +{ + [Test] + public static void RefStruct() + { + var eventBus = new Shared.EventBus.EventBus(); + + var subscriber1 = new TestRefStructSubscriber1(eventBus); + var subscriber2 = new TestRefStructSubscriber2(eventBus); + + subscriber1.Subscribe(); + subscriber2.Subscribe(); + + var args = new TestEventStruct(); + eventBus.Raise(ref args); + + Assert.That(args.Counter, Is.EqualTo(2)); + Assert.Pass("All subscribers handled correctly"); + } + + private sealed class TestRefStructSubscriber1(IEventBus eventBus) : IEventSubscriber + { + public void Subscribe() + { + eventBus.Subscribe(this, RefMethod); + } + + private void RefMethod(ref TestEventStruct args) + { + args.Counter++; + } + } + + private sealed class TestRefStructSubscriber2(IEventBus eventBus) : IEventSubscriber + { + public void Subscribe() + { + eventBus.Subscribe(this, RefMethod); + } + + private void RefMethod(ref TestEventStruct args) + { + Assert.That(args.Counter, Is.EqualTo(1)); + args.Counter++; + } + } + + private record struct TestEventStruct(int Counter = 0) : IEventArgs; +} \ No newline at end of file diff --git a/Hypercube.UnitTests/EventBus/EventBusUnsubscribeTests.cs b/Hypercube.UnitTests/EventBus/EventBusUnsubscribeTests.cs new file mode 100644 index 0000000..6d63474 --- /dev/null +++ b/Hypercube.UnitTests/EventBus/EventBusUnsubscribeTests.cs @@ -0,0 +1,67 @@ +using Hypercube.Shared.EventBus; +using Hypercube.Shared.EventBus.Events; + +namespace Hypercube.UnitTests.EventBus; + +public class EventBusUnsubscribeTests +{ + [Test] + public void Unsubscribing() + { + var eventBus = new Shared.EventBus.EventBus(); + var subscriber = new TestEventSubscriber(eventBus); + + subscriber.Subscribe(); + + eventBus.Raise(new TestUnsubEventClass()); + eventBus.Raise(new TestUnsubEventStruct()); + + subscriber.Unsubscribe(); + + eventBus.Raise(new TestUnsubEventClass()); + eventBus.Raise(new TestUnsubEventStruct()); + + subscriber.AssertPassed(); + } + + private class TestEventSubscriber(IEventBus eventBus) : IEventSubscriber + { + private bool _classPassed; + private bool _structPassed; + + public void Subscribe() + { + eventBus.Subscribe(this, OnClass); + eventBus.Subscribe(this, OnStruct); + } + + public void Unsubscribe() + { + eventBus.Unsubscribe(this); + eventBus.Unsubscribe(this); + } + + private void OnClass(ref TestUnsubEventClass args) + { + _classPassed = !_classPassed; + } + + private void OnStruct(ref TestUnsubEventStruct args) + { + _structPassed = !_structPassed; + } + + public void AssertPassed() + { + Assert.Multiple(() => + { + Assert.That(_classPassed, Is.True); + Assert.That(_structPassed, Is.True); + }); + } + } + + private record struct TestUnsubEventStruct : IEventArgs; + + private sealed class TestUnsubEventClass : IEventArgs; +} \ No newline at end of file