using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects { public interface IEventBus : IDirectedEventBus, IBroadcastEventBus { } public interface IDirectedEventBus { void RaiseLocalEvent(EntityUid uid, TEvent args, bool broadcast = true) where TEvent : notnull; void RaiseLocalEvent(EntityUid uid, object args, bool broadcast = true); void SubscribeLocalEvent(ComponentEventHandler handler) where TComp : IComponent where TEvent : notnull; void SubscribeLocalEvent( ComponentEventHandler handler, Type orderType, Type[]? before = null, Type[]? after = null) where TComp : IComponent where TEvent : notnull; #region Ref Subscriptions void RaiseLocalEvent(EntityUid uid, ref TEvent args, bool broadcast = true) where TEvent : notnull; void RaiseLocalEvent(EntityUid uid, ref object args, bool broadcast = true); void SubscribeLocalEvent(ComponentEventRefHandler handler) where TComp : IComponent where TEvent : notnull; void SubscribeLocalEvent( ComponentEventRefHandler handler, Type orderType, Type[]? before = null, Type[]? after = null) where TComp : IComponent where TEvent : notnull; #endregion void UnsubscribeLocalEvent() where TComp : IComponent where TEvent : notnull; /// /// Dispatches an event directly to a specific component. /// /// /// This has a very specific purpose, and has massive potential to be abused. /// DO NOT EXPOSE THIS TO CONTENT. /// /// Event to dispatch. /// Component receiving the event. /// Event arguments for the event. internal void RaiseComponentEvent(IComponent component, TEvent args) where TEvent : notnull; /// /// Dispatches an event directly to a specific component, by-ref. /// /// /// This has a very specific purpose, and has massive potential to be abused. /// DO NOT EXPOSE THIS TO CONTENT. /// /// Event to dispatch. /// Component receiving the event. /// Event arguments for the event. internal void RaiseComponentEvent(IComponent component, ref TEvent args) where TEvent : notnull; } internal partial class EntityEventBus : IDirectedEventBus, IEventBus, IDisposable { private delegate void DirectedEventHandler(EntityUid uid, IComponent comp, ref Unit args); private delegate void DirectedEventHandler(EntityUid uid, IComponent comp, ref TEvent args) where TEvent : notnull; private IEntityManager _entMan; private EventTables _eventTables; /// /// Constructs a new instance of . /// /// The entity manager to watch for entity/component events. public EntityEventBus(IEntityManager entMan) { _entMan = entMan; _eventTables = new EventTables(_entMan); } /// void IDirectedEventBus.RaiseComponentEvent(IComponent component, TEvent args) { ref var unitRef = ref Unsafe.As(ref args); _eventTables.DispatchComponent(component.Owner, component, ref unitRef, false); } /// void IDirectedEventBus.RaiseComponentEvent(IComponent component, ref TEvent args) { ref var unitRef = ref Unsafe.As(ref args); _eventTables.DispatchComponent(component.Owner, component, ref unitRef, true); } /// public void RaiseLocalEvent(EntityUid uid, TEvent args, bool broadcast = true) where TEvent : notnull { var type = typeof(TEvent); ref var unitRef = ref Unsafe.As(ref args); RaiseLocalEventCore(uid, ref unitRef, type, broadcast, false); } /// public void RaiseLocalEvent(EntityUid uid, object args, bool broadcast = true) { var type = args.GetType(); ref var unitRef = ref Unsafe.As(ref args); RaiseLocalEventCore(uid, ref unitRef, type, broadcast, false); } public void RaiseLocalEvent(EntityUid uid, ref TEvent args, bool broadcast = true) where TEvent : notnull { var type = typeof(TEvent); ref var unitRef = ref Unsafe.As(ref args); RaiseLocalEventCore(uid, ref unitRef, type, broadcast, true); } public void RaiseLocalEvent(EntityUid uid, ref object args, bool broadcast = true) { var type = args.GetType(); ref var unitRef = ref Unsafe.As(ref args); RaiseLocalEventCore(uid, ref unitRef, type, broadcast, true); } private void RaiseLocalEventCore(EntityUid uid, ref Unit unitRef, Type type, bool broadcast, bool byRef) { if (_orderedEvents.Contains(type)) { RaiseLocalOrdered(uid, type, ref unitRef, broadcast, byRef); return; } _eventTables.Dispatch(uid, type, ref unitRef, byRef); // we also broadcast it so the call site does not have to. if (broadcast) ProcessSingleEvent(EventSource.Local, ref unitRef, type, byRef); } /// public void SubscribeLocalEvent(ComponentEventHandler handler) where TComp : IComponent where TEvent : notnull { void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) => handler(uid, (TComp)comp, args); _eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler, null, false); } public void SubscribeLocalEvent( ComponentEventHandler handler, Type orderType, Type[]? before = null, Type[]? after = null) where TComp : IComponent where TEvent : notnull { void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) => handler(uid, (TComp)comp, args); var orderData = new OrderingData(orderType, before, after); _eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler, orderData, false); HandleOrderRegistration(typeof(TEvent), orderData); } public void SubscribeLocalEvent(ComponentEventRefHandler handler) where TComp : IComponent where TEvent : notnull { void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) => handler(uid, (TComp)comp, ref args); _eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler, null, true); } public void SubscribeLocalEvent(ComponentEventRefHandler handler, Type orderType, Type[]? before = null, Type[]? after = null) where TComp : IComponent where TEvent : notnull { void EventHandler(EntityUid uid, IComponent comp, ref TEvent args) => handler(uid, (TComp)comp, ref args); var orderData = new OrderingData(orderType, before, after); _eventTables.Subscribe(typeof(TComp), typeof(TEvent), EventHandler, orderData, true); HandleOrderRegistration(typeof(TEvent), orderData); } /// public void UnsubscribeLocalEvent() where TComp : IComponent where TEvent : notnull { _eventTables.Unsubscribe(typeof(TComp), typeof(TEvent)); } private sealed class EventTables : IDisposable { private const string ValueDispatchError = "Tried to dispatch a value event to a by-reference subscription."; private const string RefDispatchError = "Tried to dispatch a ref event to a by-value subscription."; private IEntityManager _entMan; private IComponentFactory _comFac; // eUid -> EventType -> { CompType1, ... CompTypeN } private Dictionary>> _eventTables; // EventType -> CompType -> Handler private Dictionary> _subscriptions; // prevents shitcode, get your subscriptions figured out before you start spawning entities private bool _subscriptionLock; public EventTables(IEntityManager entMan) { _entMan = entMan; _comFac = entMan.ComponentFactory; _entMan.EntityAdded += OnEntityAdded; _entMan.EntityDeleted += OnEntityDeleted; _entMan.ComponentAdded += OnComponentAdded; _entMan.ComponentRemoved += OnComponentRemoved; _eventTables = new(); _subscriptions = new(); _subscriptionLock = false; } private void OnEntityAdded(object? sender, EntityUid e) { AddEntity(e); } private void OnEntityDeleted(object? sender, EntityUid e) { RemoveEntity(e); } private void OnComponentAdded(object? sender, ComponentEventArgs e) { _subscriptionLock = true; AddComponent(e.Owner, e.Component.GetType()); } private void OnComponentRemoved(object? sender, ComponentEventArgs e) { RemoveComponent(e.Owner, e.Component.GetType()); } private void AddSubscription(Type compType, Type eventType, DirectedRegistration registration) { if (_subscriptionLock) throw new InvalidOperationException("Subscription locked."); var referenceEvent = eventType.HasCustomAttribute(); if (referenceEvent != registration.ReferenceEvent) throw new InvalidOperationException( $"Attempted to subscribe by-ref and by-value to the same directed event! comp={compType.Name}, event={eventType.Name} eventIsByRef={referenceEvent} subscriptionIsByRef={registration.ReferenceEvent}"); if (!_subscriptions.TryGetValue(compType, out var compSubs)) { compSubs = new Dictionary(); _subscriptions.Add(compType, compSubs); } if (compSubs.ContainsKey(eventType)) throw new InvalidOperationException( $"Duplicate Subscriptions for comp={compType.Name}, event={eventType.Name}"); compSubs.Add(eventType, registration); } public void Subscribe(Type compType, Type eventType, DirectedEventHandler handler, OrderingData? order, bool byReference) where TEvent : notnull { AddSubscription(compType, eventType, new DirectedRegistration(handler, order, (EntityUid uid, IComponent comp, ref Unit ev) => { ref var tev = ref Unsafe.As(ref ev); handler(uid, comp, ref tev); }, byReference)); } public void Unsubscribe(Type compType, Type eventType) { if (_subscriptionLock) throw new InvalidOperationException("Subscription locked."); if (!_subscriptions.TryGetValue(compType, out var compSubs)) return; compSubs.Remove(eventType); } private void AddEntity(EntityUid euid) { // odds are at least 1 component will subscribe to an event on the entity, so just // preallocate the table now. Dispatch does not need to check this later. _eventTables.Add(euid, new Dictionary>()); } private void RemoveEntity(EntityUid euid) { _eventTables.Remove(euid); } private void AddComponent(EntityUid euid, Type compType) { var eventTable = _eventTables[euid]; var enumerator = GetReferences(compType); while (enumerator.MoveNext(out var type)) { if (!_subscriptions.TryGetValue(type, out var compSubs)) continue; foreach (var kvSub in compSubs) { if (!eventTable.TryGetValue(kvSub.Key, out var subscribedComps)) { subscribedComps = new HashSet(); eventTable.Add(kvSub.Key, subscribedComps); } subscribedComps.Add(type); } } } private void RemoveComponent(EntityUid euid, Type compType) { var eventTable = _eventTables[euid]; var enumerator = GetReferences(compType); while (enumerator.MoveNext(out var type)) { if (!_subscriptions.TryGetValue(type, out var compSubs)) continue; foreach (var kvSub in compSubs) { if (!eventTable.TryGetValue(kvSub.Key, out var subscribedComps)) return; subscribedComps.Remove(type); } } } public void Dispatch(EntityUid euid, Type eventType, ref Unit args, bool dispatchByReference) { if (!TryGetSubscriptions(eventType, euid, out var enumerator)) return; while (enumerator.MoveNext(out var tuple)) { var (component, reg) = tuple.Value; if (reg.ReferenceEvent != dispatchByReference) ThrowByRefMisMatch(); reg.Handler(euid, component, ref args); } } public void CollectOrdered( EntityUid euid, Type eventType, List<(RefEventHandler, OrderingData?)> found, bool byRef) { var eventTable = _eventTables[euid]; if (!eventTable.TryGetValue(eventType, out var subscribedComps)) return; foreach (var compType in subscribedComps) { if (!_subscriptions.TryGetValue(compType, out var compSubs)) return; if (!compSubs.TryGetValue(eventType, out var reg)) return; if (reg.ReferenceEvent != byRef) ThrowByRefMisMatch(); var component = _entMan.GetComponent(euid, compType); found.Add(((ref Unit ev) => reg.Handler(euid, component, ref ev), reg.Ordering)); } } public void DispatchComponent(EntityUid euid, IComponent component, ref Unit args, bool dispatchByReference) where TEvent : notnull { var enumerator = GetReferences(component.GetType()); while (enumerator.MoveNext(out var type)) { if (!_subscriptions.TryGetValue(type, out var compSubs)) continue; if (!compSubs.TryGetValue(typeof(TEvent), out var reg)) continue; if (reg.ReferenceEvent != dispatchByReference) ThrowByRefMisMatch(); reg.Handler(euid, component, ref args); } } public void ClearEntities() { _eventTables = new(); _subscriptionLock = false; } public void Clear() { ClearEntities(); _subscriptions = new(); } public void Dispose() { _entMan.EntityAdded -= OnEntityAdded; _entMan.EntityDeleted -= OnEntityDeleted; _entMan.ComponentAdded -= OnComponentAdded; _entMan.ComponentRemoved -= OnComponentRemoved; // punishment for use-after-free _entMan = null!; _eventTables = null!; _subscriptions = null!; } /// /// Enumerates the type's component references, returning the type itself last. /// private ReferencesEnumerator GetReferences(Type type) { return new(type, _comFac.GetRegistration(type).References); } /// /// Enumerates all subscriptions for an event on a specific entity, returning the component instances and registrations. /// private bool TryGetSubscriptions(Type eventType, EntityUid euid, [NotNullWhen(true)] out SubscriptionsEnumerator enumerator) { if (!_eventTables.TryGetValue(euid, out var eventTable)) { enumerator = default!; return false; } // No subscriptions to this event type, return null. if (!eventTable.TryGetValue(eventType, out var subscribedComps)) { enumerator = default; return false; } enumerator = new(eventType, subscribedComps.GetEnumerator(), _subscriptions, euid, _entMan); return true; } private struct ReferencesEnumerator { private readonly Type _baseType; private readonly IReadOnlyList _list; private readonly int _totalLength; private int _idx; public ReferencesEnumerator(Type baseType, IReadOnlyList list) { _baseType = baseType; _list = list; _totalLength = list.Count; _idx = 0; } public bool MoveNext([NotNullWhen(true)] out Type? type) { if (_idx >= _totalLength) { if (_idx++ == _totalLength) { type = _baseType; return true; } type = null; return false; } type = _list[_idx++]; if (type == _baseType) return MoveNext(out type); return true; } } private struct SubscriptionsEnumerator : IDisposable { private readonly Type _eventType; private HashSet.Enumerator _enumerator; private readonly IReadOnlyDictionary> _subscriptions; private readonly EntityUid _uid; private readonly IEntityManager _entityManager; public SubscriptionsEnumerator(Type eventType, HashSet.Enumerator enumerator, IReadOnlyDictionary> subscriptions, EntityUid uid, IEntityManager entityManager) { _eventType = eventType; _enumerator = enumerator; _subscriptions = subscriptions; _entityManager = entityManager; _uid = uid; } public bool MoveNext( [NotNullWhen(true)] out (IComponent Component, DirectedRegistration Registration)? tuple) { _enumerator.MoveNext(); // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (_enumerator.Current == null) { tuple = null; return false; } var compType = _enumerator.Current; if (!_subscriptions.TryGetValue(compType, out var compSubs)) { tuple = null; return false; } if (!compSubs.TryGetValue(_eventType, out var registration)) { tuple = null; return false; } tuple = (_entityManager.GetComponent(_uid, compType), registration); return true; } public void Dispose() { _enumerator.Dispose(); } } } /// public void ClearEventTables() { _eventTables.Clear(); } public void Dispose() { _eventTables.Dispose(); _eventTables = null!; _entMan = null!; } private readonly struct DirectedRegistration { public readonly Delegate Original; public readonly OrderingData? Ordering; public readonly DirectedEventHandler Handler; public readonly bool ReferenceEvent; public DirectedRegistration(Delegate original, OrderingData? ordering, DirectedEventHandler handler, bool referenceEvent) { Original = original; Ordering = ordering; Handler = handler; ReferenceEvent = referenceEvent; } } } public delegate void ComponentEventHandler(EntityUid uid, TComp component, TEvent args) where TComp : IComponent where TEvent : notnull; public delegate void ComponentEventRefHandler(EntityUid uid, TComp component, ref TEvent args) where TComp : IComponent where TEvent : notnull; }