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);
}
}
}