using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Tertius.Simulation; /// /// Reusable storage for an . The scheduler keeps one per worker thread. Once the buffer has /// grown to the longest list it has held, filling it again never allocates. /// /// Creates a buffer for events. It grows when needed. public sealed class EventBuffer(int capacity = 32) { internal Event[] Items = new Event[Math.Max(capacity, 4)]; internal int Count; internal EventMask Mask; internal int Version; /// The number of events the buffer holds before it grows. public int Capacity => Items.Length; /// Clears the buffer and returns a list over it. Events that the list adds target . public EventList Begin(Entity owner = default) { Clear(); return new(this, owner); } /// A list over the events already in the buffer, without clearing them. For benchmarks and tests. internal EventList View(Entity owner = default) => new(this, owner); internal void Clear() { // Drop the Extra references so that the buffer does not keep garbage alive. Array.Clear(Items, 0, Count); Count = 0; Mask = default; Version++; } internal ref Event Append() { if (Count == Items.Length) Array.Resize(ref Items, Items.Length * 2); Version++; return ref Items[Count++]; } // ---- The scans that every event query runs --------------------------------------------------------------------- /// The index of the first event that matches, or -1. internal int FirstMatch(TFilter filter) where TFilter : struct, IEventFilter { if (!filter.MayMatch(Mask)) return -1; Span items = Items.AsSpan(0, Count); for (int i = 0; i < items.Length; i++) if (filter.Matches(ref items[i])) return i; return -1; } /// A copy of the payload of the first event that matches. Null if none matches. internal T? First(TFilter filter) where TFilter : struct, IEventFilter where T : unmanaged, IEventPayload { int index = FirstMatch(filter); return index >= 0 ? Unsafe.As(ref Items[index].Payload) : null; } /// A copy of the first event that matches. Null if none matches. internal Event? First(TFilter filter) where TFilter : struct, IEventFilter { int index = FirstMatch(filter); return index >= 0 ? Items[index] : null; } /// The number of events that matches. internal int CountMatches(TFilter filter) where TFilter : struct, IEventFilter { if (!filter.MayMatch(Mask)) return 0; int n = 0; Span items = Items.AsSpan(0, Count); for (int i = 0; i < items.Length; i++) if (filter.Matches(ref items[i])) n++; return n; } /// /// Removes the events that matches and returns their number. The other events keep their /// order. If the filter throws, the events from the one it threw on are kept unchanged. /// internal int RemoveMatches(TFilter filter) where TFilter : struct, IEventFilter { if (!filter.MayMatch(Mask)) return 0; Event[] items = Items; int count = Count, write = 0, read = 0; try { for (; read < count; read++) { ref Event e = ref items[read]; if (filter.Matches(ref e)) continue; if (write != read) items[write] = e; write++; } } finally { Compact(count, read, write); } return count - Count; } // Finishes a removal pass. If the filter threw at 'read', the events from there on are kept unchanged. private void Compact(int count, int read, int write) { if (read < count && write != read) Array.Copy( sourceArray: Items, sourceIndex: read, destinationArray: Items, destinationIndex: write, length: count - read ); int newCount = write + (count - read); if (newCount < count) { Array.Clear(Items, newCount, count - newCount); Count = newCount; RecomputeMask(); } Version++; } internal void RecomputeMask() { EventMask mask = default; for (int i = 0; i < Count; i++) mask.Add(Items[i].Type); Mask = mask; } } /// /// The events of one entity run, in delivery order. Components edit the list in place: they change payloads, remove /// events and add events. An added event is visible only to the components after the one that added it. /// The list never allocates in steady state. A query picks events with , From and /// Where, and an ending runs it. Enumerating gives references into the payloads: /// /// foreach (ref var damage in events.Of<Damage>()) /// damage.Amount = MathF.Max(0, damage.Amount - rating); /// events.Of<Damage>().Where(static (ref Damage d) => d.Amount == 0).Remove(); /// /// public readonly ref struct EventList { private readonly EventBuffer buffer; internal EventList(EventBuffer buffer, Entity owner) { this.buffer = buffer; Owner = owner; } /// The number of events in the list. public readonly int Count => buffer.Count; /// The running entity. Events that creates target it. public Entity Owner { get; } /// /// A conservative superset of the event types in the list. Adds set bits, and removals recompute it, so after /// adds and removals it is exact. The scheduler skips a component whose wants do not overlap it. /// public readonly EventMask Mask => buffer.Mask; /// /// A reference to an event, to read or edit it in place. The mask does not see a change of /// made through this reference; to turn an event into another type, remove it and add the new one. /// public readonly ref Event this[int index] { get { if ((uint)index >= (uint)buffer.Count) ThrowIndex(index); return ref buffer.Items[index]; } } /// The events as a span. It is valid until the list changes. public readonly Span AsSpan() => buffer.Items.AsSpan(0, buffer.Count); /// Appends an event with this payload. Its target is the running entity. public readonly void Add(in T payload, Entity sender = default) where T : unmanaged, IEventPayload { int type = EventType.Id; ref Event e = ref buffer.Append(); e = default; e.Target = Owner; e.Sender = sender; e.Type = type; Unsafe.As(ref e.Payload) = payload; buffer.Mask.Add(type); } /// Appends a copy of an event, with its own target and sender. public readonly void Add(in Event e) { int type = e.Type; buffer.Append() = e; buffer.Mask.Add(type); } /// Removes the event at . The other events keep their order. public readonly void RemoveAt(int index) { int count = buffer.Count; if ((uint)index >= (uint)count) ThrowIndex(index); Event[] items = buffer.Items; if (index < count - 1) Array.Copy( sourceArray: items, sourceIndex: index + 1, destinationArray: items, destinationIndex: index, length: count - index - 1 ); items[count - 1] = default; buffer.Count = count - 1; buffer.Version++; buffer.RecomputeMask(); } /// /// The events of type , as a query: From and Where narrow it, and an ending runs /// it. A query for a type that lacks scans nothing. Do not add or remove events while enumerating. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly EventsOf Of() where T : unmanaged, IEventPayload => Find(new EventQuery()); /// The events that a stored query matches. It gives the same chain as with the same words. public readonly EventsOf Find(EventQuery query) where T : unmanaged, IEventPayload => new(buffer, query); /// The events that a stored query with a state predicate matches. public readonly EventsOf Find(EventQuery query) where T : unmanaged, IEventPayload => new(buffer, query); /// The events of every type that pass . Use a static lambda, so that it allocates nothing. public readonly EventsOfAnyType Where(RefPredicate predicate) => new(buffer, new AnyTypeFilter().Where(predicate)); /// The events of every type that sent. keeps the events without a sender. public readonly EventsOfAnyType From(Entity sender) => new(buffer, new AnyTypeFilter().From(sender)); [DoesNotReturn] private readonly void ThrowIndex(int index) => throw new ArgumentOutOfRangeException(nameof(index), index, $"The event list has {buffer.Count} events."); }