using System; using System.Linq; using System.Runtime.CompilerServices; using JetBrains.Annotations; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects { /// /// Provides a central event bus that EntitySystems can subscribe to. This is the main way that /// EntitySystems communicate with each other. /// [PublicAPI] public interface IBroadcastEventBus { /// /// Subscribes an event handler for an event type. /// /// Event type to subscribe to. /// /// Subscriber that owns the handler. /// Delegate that handles the event. /// // [Obsolete("Subscribe to the event by ref instead (EntityEventRefHandler)")] void SubscribeEvent(EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler eventHandler) where T : notnull; /// // [Obsolete("Subscribe to the event by ref instead (EntityEventRefHandler)")] void SubscribeEvent( EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler eventHandler, Type orderType, Type[]? before = null, Type[]? after = null) where T : notnull; void SubscribeEvent(EventSource source, IEntityEventSubscriber subscriber, EntityEventRefHandler eventHandler) where T : notnull; void SubscribeEvent( EventSource source, IEntityEventSubscriber subscriber, EntityEventRefHandler eventHandler, Type orderType, Type[]? before = null, Type[]? after = null) where T : notnull; /// /// Unsubscribes all event handlers of a given type. /// /// Event type being unsubscribed from. /// /// Subscriber that owns the handlers. void UnsubscribeEvent(EventSource source, IEntityEventSubscriber subscriber) where T : notnull; /// /// Immediately raises an event onto the bus. /// /// /// Event being raised. void RaiseEvent(EventSource source, object toRaise); void RaiseEvent(EventSource source, T toRaise) where T : notnull; void RaiseEvent(EventSource source, ref T toRaise) where T : notnull; /// /// Queues an event to be raised at a later time. /// /// /// Event being raised. void QueueEvent(EventSource source, EntityEventArgs toRaise); /// /// Unsubscribes all event handlers for a given subscriber. /// /// Owner of the handlers being removed. void UnsubscribeEvents(IEntityEventSubscriber subscriber); } /// internal interface IBroadcastEventBusInternal : IBroadcastEventBus { /// /// Raises all queued events onto the event bus. This needs to be called often. /// void ProcessEventQueue(); } [Flags] public enum EventSource : byte { None = 0b0000, Local = 0b0001, Network = 0b0010, All = Local | Network, } /// /// Implements the event broadcast functions. /// internal sealed partial class EntityEventBus : IBroadcastEventBusInternal { // Inside this class we pass a lot of things around as "ref Unit unitRef". // The idea behind this is to avoid using type arguments in core dispatch that only needs to pass around a ref* // Type arguments require the JIT to compile a new method implementation for every event type, // which would start to weigh a LOT. private delegate void RefEventHandler(ref Unit ev); /// public void UnsubscribeEvents(IEntityEventSubscriber subscriber) { if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); if (!_inverseEventSubscriptions.TryGetValue(subscriber, out var val)) return; // UnsubscribeEvent modifies _inverseEventSubscriptions, requires val to be cached foreach (var (type, tuple) in val.ToList()) { UnsubscribeEvent(type, tuple, subscriber); } } /// public void ProcessEventQueue() { while (_eventQueue.Count != 0) { var (source, args) = _eventQueue.Dequeue(); var type = args.GetType(); ref var unitRef = ref ExtractUnitRef(ref args, type); ProcessSingleEvent(source, ref unitRef, type); } } /// public void SubscribeEvent( EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler eventHandler) where T : notnull { if (eventHandler == null) throw new ArgumentNullException(nameof(eventHandler)); SubscribeEventCommon(source, subscriber, (ref Unit ev) => eventHandler(Unsafe.As(ref ev)), eventHandler, null, false); } public void SubscribeEvent( EventSource source, IEntityEventSubscriber subscriber, EntityEventHandler eventHandler, Type orderType, Type[]? before = null, Type[]? after = null) where T : notnull { if (eventHandler == null) throw new ArgumentNullException(nameof(eventHandler)); var order = CreateOrderingData(orderType, before, after); SubscribeEventCommon(source, subscriber, (ref Unit ev) => eventHandler(Unsafe.As(ref ev)), eventHandler, order, false); } public void SubscribeEvent(EventSource source, IEntityEventSubscriber subscriber, EntityEventRefHandler eventHandler) where T : notnull { SubscribeEventCommon(source, subscriber, (ref Unit ev) => { ref var tev = ref Unsafe.As(ref ev); eventHandler(ref tev); }, eventHandler, null, true); } public void SubscribeEvent(EventSource source, IEntityEventSubscriber subscriber, EntityEventRefHandler eventHandler, Type orderType, Type[]? before = null, Type[]? after = null) where T : notnull { var order = CreateOrderingData(orderType, before, after); SubscribeEventCommon(source, subscriber, (ref Unit ev) => { ref var tev = ref Unsafe.As(ref ev); eventHandler(ref tev); }, eventHandler, order, true); } private void SubscribeEventCommon( EventSource source, IEntityEventSubscriber subscriber, RefEventHandler handler, object equalityToken, OrderingData? order, bool byRef) where T : notnull { if (source == EventSource.None) throw new ArgumentOutOfRangeException(nameof(source)); if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); var eventType = typeof(T); var eventReference = eventType.HasCustomAttribute(); if (eventReference != byRef) throw new InvalidOperationException( $"Attempted to subscribe by-ref and by-value to the same broadcast event! event={eventType} eventIsByRef={eventReference} subscriptionIsByRef={byRef}"); var subscriptionTuple = new BroadcastRegistration(source, handler, equalityToken, order, byRef); RegisterCommon(eventType, order, out var subscriptions); if (!subscriptions.BroadcastRegistrations.Contains(subscriptionTuple)) subscriptions.BroadcastRegistrations.Add(subscriptionTuple); var inverseSubscription = _inverseEventSubscriptions.GetOrNew(subscriber); if (!inverseSubscription.TryAdd(eventType, subscriptionTuple)) { // If this needs to be supported in the future, then event subscribing needs to be updated. // Also, we need to ensure that separate local + network subs dont break anything. If the event handlers // are identical, local+network subs should be combined into a single sub anyways. Separate subs are // probably just erroneous. var name = subscriber.GetType().Name; throw new InvalidOperationException( $"{name} attempted to subscribe twice to the same event: {eventType.Name}"); } } /// public void UnsubscribeEvent(EventSource source, IEntityEventSubscriber subscriber) where T : notnull { if (source == EventSource.None) throw new ArgumentOutOfRangeException(nameof(source)); if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); var eventType = typeof(T); if (_inverseEventSubscriptions.TryGetValue(subscriber, out var inverse) && inverse.TryGetValue(eventType, out var tuple)) UnsubscribeEvent(eventType, tuple, subscriber); } /// public void RaiseEvent(EventSource source, object toRaise) { if (source == EventSource.None) throw new ArgumentOutOfRangeException(nameof(source)); var eventType = toRaise.GetType(); ref var unitRef = ref ExtractUnitRef(ref toRaise, eventType); ProcessSingleEvent(source, ref unitRef, eventType); } public void RaiseEvent(EventSource source, T toRaise) where T : notnull { if (source == EventSource.None) throw new ArgumentOutOfRangeException(nameof(source)); ProcessSingleEvent(source, ref Unsafe.As(ref toRaise), typeof(T)); } public void RaiseEvent(EventSource source, ref T toRaise) where T : notnull { if (source == EventSource.None) throw new ArgumentOutOfRangeException(nameof(source)); ProcessSingleEvent(source, ref Unsafe.As(ref toRaise), typeof(T)); } /// public void QueueEvent(EventSource source, EntityEventArgs toRaise) { if (source == EventSource.None) throw new ArgumentOutOfRangeException(nameof(source)); if (toRaise == null) throw new ArgumentNullException(nameof(toRaise)); _eventQueue.Enqueue((source, toRaise)); } private void UnsubscribeEvent(Type eventType, BroadcastRegistration tuple, IEntityEventSubscriber subscriber) { if (_subscriptionLock) throw new InvalidOperationException("Subscription locked."); if (_eventDataUnfrozen.TryGetValue(eventType, out var subscriptions) && subscriptions.BroadcastRegistrations.Contains(tuple)) subscriptions.BroadcastRegistrations.Remove(tuple); if (_inverseEventSubscriptions.TryGetValue(subscriber, out var inverse) && inverse.ContainsKey(eventType)) inverse.Remove(eventType); } private void ProcessSingleEvent(EventSource source, ref Unit unitRef, Type eventType) { if (!_eventData!.TryGetValue(eventType, out var subs)) return; if (subs.IsOrdered && !subs.OrderingUpToDate) { UpdateOrderSeq(eventType, subs); // For ordered events, the Registrations list in the sub list is already sorted to be the correct order. // This means ordered broadcast events have no overhead over non-ordered ones. } ProcessSingleEventCore(source, ref unitRef, subs); } private static void ProcessSingleEventCore( EventSource source, ref Unit unitRef, EventData subs) { foreach (var handler in subs.BroadcastRegistrations.Span) { if ((handler.Mask & source) != 0) handler.Handler(ref unitRef); } } private sealed class BroadcastRegistration : OrderedRegistration, IEquatable { public readonly object EqualityToken; public readonly RefEventHandler Handler; public readonly EventSource Mask; public readonly bool ReferenceEvent; public BroadcastRegistration( EventSource mask, RefEventHandler handler, object equalityToken, OrderingData? ordering, bool referenceEvent) : base(ordering) { Mask = mask; Handler = handler; EqualityToken = equalityToken; ReferenceEvent = referenceEvent; } public bool Equals(BroadcastRegistration? other) { return other != null && Mask == other.Mask && Equals(EqualityToken, other.EqualityToken); } public override bool Equals(object? obj) { return obj is BroadcastRegistration other && Equals(other); } public override int GetHashCode() => HashCode.Combine(Mask, EqualityToken); } } }