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