using System; using System.Collections.Generic; using System.Linq; using Robust.Shared.Collections; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects { internal partial class EntityEventBus { private static void CollectBroadcastOrdered( EventSource source, EventData sub, ref ValueList found) { foreach (var handler in sub.BroadcastRegistrations.Span) { if ((handler.Mask & source) != 0) found.Add(new OrderedEventDispatch(handler.Handler, handler.Order)); } } private void RaiseLocalOrdered( EntityUid uid, Type eventType, EventData subs, ref Unit unitRef, bool broadcast) { if (!subs.OrderingUpToDate) UpdateOrderSeq(eventType, subs); var found = new ValueList(); if (broadcast) CollectBroadcastOrdered(EventSource.Local, subs, ref found); EntCollectOrdered(uid, eventType, ref found); DispatchOrderedEvents(ref unitRef, ref found); } private static void DispatchOrderedEvents(ref Unit eventArgs, ref ValueList found) { found.Sort(OrderedEventDispatchComparer.Instance); foreach (var (handler, _) in found.Span) { handler(ref eventArgs); } } /// /// Calculate sequence order for all event subscriptions, broadcast and directed. /// private void UpdateOrderSeq(Type eventType, EventData sub) { DebugTools.Assert(sub.IsOrdered); // Collect all subscriptions, broadcast and ordered. IEnumerable regs = sub.BroadcastRegistrations; if (_eventSubsInv.TryGetValue(eventType, out var comps)) { regs = regs.Concat(comps .Select(c => _eventSubs[c.Value]) .Where(c => c != null) .Select(c => c![eventType])); } // A hard problem in EventBus' design is that ordering is keyed on a single Type instance // (probably just the type of the EntitySystem). // This is problematic if a system listens to the same directed event multiple times for the same component, // as there are now two distinct subscriptions with the same key. // To solve this, I decided that *this is allowed*, but all ordering on the same key must be the same. // So you can't have different Before/After on two subscriptions to an event on the same system. // // Group by ordering types, also filter out un-ordered ones. var groups = regs.Where(r => r.Ordering != null).GroupBy(b => b.Ordering!.OrderType).ToArray(); // Verify that all groups of order types have the same ordering info for Before/After. foreach (var group in groups) { var firstOrder = group.First().Ordering; if (!group.All(e => e.Ordering!.Equals(firstOrder))) { throw new InvalidOperationException( $"{group.Key} uses different ordering constraints for different subscriptions to the same event {eventType}. " + "All subscriptions to the same event from the same registrar must use the same ordering."); } } // Set up topological sort. var nodes = TopologicalSort.FromBeforeAfter( groups.Select(g => g.ToArray()), n => n[0].Ordering!.OrderType, n => n, n => n[0].Ordering!.Before, n => n[0].Ordering!.After, allowMissing: true); // Start at 1, if only so events with no Ordering data at all have a distinct position. // Doesn't really matter. var i = 1; foreach (var group in TopologicalSort.Sort(nodes)) { // Assign indices to all registrations in order of the topological sort. foreach (var registration in group) { registration.Order = i++; } } sub.OrderingUpToDate = true; // Sort the broadcast registrations ahead of time. // This means ordered broadcast events have no overhead from unordered ones. sub.BroadcastRegistrations.Sort(RegistrationOrderComparer.Instance); // We still need Order indices on the OrderedRegistrations for directed ordered events. // Since we sort those at submission time. // Still, it's a single .Sort() now for those instead of the whole topological short shebang. } internal sealed record OrderingData(Type OrderType, Type[] Before, Type[] After) { public bool Equals(OrderingData? other) { if (other == null) return false; return other.OrderType == OrderType && Before.AsSpan().SequenceEqual(other.Before) && After.AsSpan().SequenceEqual(other.After); } public override int GetHashCode() { var hc = new HashCode(); hc.Add(OrderType); hc.AddArray(Before); hc.AddArray(After); return hc.ToHashCode(); } } private sealed class RegistrationOrderComparer : IComparer { public static readonly RegistrationOrderComparer Instance = new(); public int Compare(OrderedRegistration? x, OrderedRegistration? y) { return x!.Order.CompareTo(y!.Order); } } private record struct OrderedEventDispatch(RefEventHandler Handler, int Order); private sealed class OrderedEventDispatchComparer : IComparer { public static readonly OrderedEventDispatchComparer Instance = new(); public int Compare(OrderedEventDispatch x, OrderedEventDispatch y) { return x.Order.CompareTo(y.Order); } } /// /// Base type for directed and broadcast subscriptions. Contains ordering data. /// internal abstract class OrderedRegistration { public int Order; public readonly OrderingData? Ordering; protected OrderedRegistration(OrderingData? ordering) { Ordering = ordering; } } /// /// Ensure all ordered events are sorted out and verified. /// /// /// Internal sorting for ordered events is normally deferred until raised /// (since broadcast event subscriptions can be edited at any time). /// /// Calling this gets all the sorting done ahead of time, /// and makes sure that there are no problems with cycles and such. /// public void CalcOrdering() { foreach (var (type, sub) in _eventData) { if (sub.IsOrdered && !sub.OrderingUpToDate) { UpdateOrderSeq(type, sub); } } } private OrderingData CreateOrderingData(Type orderType, Type[]? before, Type[]? after) { AddChildrenTypes(ref before); AddChildrenTypes(ref after); return new OrderingData(orderType, before ?? [], after ?? []); } private void AddChildrenTypes(ref Type[]? original) { if (original == null || original.Length == 0) return; _childrenTypesTemp.Clear(); foreach (var beforeType in original) { foreach (var child in _reflection.GetAllChildren(beforeType)) { _childrenTypesTemp.Add(child); } } if (_childrenTypesTemp.Count > 0) { Array.Resize(ref original, original.Length + _childrenTypesTemp.Count); _childrenTypesTemp.CopyTo(original, original.Length - _childrenTypesTemp.Count); } } } }