diff --git a/src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs b/src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs new file mode 100644 index 00000000..e993b79c --- /dev/null +++ b/src/EndlessEscapade/Core/Collections/_Generic/DenseSet.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +namespace EndlessEscapade.Framework.Collections; + +public sealed class DenseSet +{ + private T[] _items; + + public DenseSet(int capacity = 4) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + _items = new T[capacity]; + } + + public ref T this[int index] + { + get + { + var locItems = _items; + if ((uint)index < (uint)_items.Length) + { + return ref locItems[index]; + } + + return ref ResizeAndGet(index); + } + } + + private ref T ResizeAndGet(int index) + { + Array.Resize(ref _items, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref _items[index]; + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs new file mode 100644 index 00000000..10a97a74 --- /dev/null +++ b/src/EndlessEscapade/Core/Collections/_Generic/FastStack.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using System.Collections; +using System; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.Security; + +namespace EndlessEscapade.Core.Collections; + +/// +/// This struct is meant to be used purely inside of classes as fields. +/// +/// It is a light wrapper over an array. It does not track versions. +public struct FastStack : IEnumerable, IEnumerable + where T : notnull +{ + private T[] _buffer; + private int _nextIndex; + + public FastStack() : this(4) + { + + } + + public FastStack(int initalCapacity) + { + ArgumentOutOfRangeException.ThrowIfNegative(initalCapacity); + _buffer = initalCapacity == 0 ? [] : new T[initalCapacity]; + _nextIndex = 0; + } + + public int Count => _nextIndex; + + public ref T this[int index] + { + get + { + if ((uint)index < (uint)_nextIndex) + { + return ref _buffer[index]; + } + + return ref Throw_OutOfRange(); + } + } + + public void Push(T item) + { + var buffer = _buffer; + if (_nextIndex < _buffer.Length) + { + buffer[_nextIndex++] = item; + return; + } + + ResizeAndPush(item); + } + + public T Pop() + { + var buffer = _buffer; + if ((uint)(--_nextIndex) < buffer.Length) + { + ref T item = ref buffer[_nextIndex]; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + var returnValue = item; + item = default!; + return returnValue; + } + return item; + } + + return Throw_EmptyStack(); + } + + public bool TryPop([NotNullWhen(true)] out T? item) + { + var buffer = _buffer; + if ((uint)(--_nextIndex) < buffer.Length) + { + ref T slot = ref buffer[_nextIndex]; + item = slot; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + item = default!; + return true; + } + + item = default; + return false; + } + + public Span AsSpan() => _buffer.AsSpan(0, _nextIndex); + + private void ResizeAndPush(in T item) + { + Array.Resize(ref _buffer, _buffer.Length + (_buffer.Length >> 1)); + _buffer[_nextIndex++] = item; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private T Throw_EmptyStack() + { + throw new InvalidOperationException("Stack is empty!"); + } + [MethodImpl(MethodImplOptions.NoInlining)] + private ref T Throw_OutOfRange() + { + throw new ArgumentOutOfRangeException(); + } + + public void Clear() + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + AsSpan().Clear(); + _nextIndex = 0; + } + + #region Enumerable + public Enumerator GetEnumerator() => new(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator(FastStack stack) : IEnumerator, IEnumerator + { + private T[] _buffer = stack._buffer; + private int _nextIndex = stack._nextIndex; + private int _currentIndex = -1; + public readonly T Current => _buffer[_currentIndex]; + readonly object IEnumerator.Current => Current; + + public readonly void Dispose() { } + public bool MoveNext() => ++_currentIndex < _nextIndex; + public void Reset() => _currentIndex = -1; + } + #endregion Enumerable +} diff --git a/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs b/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs index efab27f3..f3e0c302 100644 --- a/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs +++ b/src/EndlessEscapade/Core/Collections/_Generic/SparseSet.cs @@ -1,223 +1,211 @@ -using System.Collections; +using EndlessEscapade.Core; +using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; +using Terraria; +using Terraria.DataStructures; namespace EndlessEscapade.Framework.Collections; -public sealed class SparseSet : IEnumerable, IDisposable +public sealed class SparseSet : IEnumerable { /// /// Gets the number of elements that the can hold without resizing. /// - public int Capacity { get; private set; } - + public int Capacity => _dense.Length; + /// /// Gets the number of elements contained in the . /// - public int Count { get; private set; } + public int Count => _nextIndex; - private T[] data; + private int _nextIndex; - private int[] dense; - private int[] sparse; + private int _version; - public SparseSet(int capacity) - { - ArgumentOutOfRangeException.ThrowIfNegative(capacity, nameof(capacity)); - - Capacity = capacity; + private T[] _dense; - data = new T[capacity]; - dense = new int[capacity]; - sparse = new int[capacity]; - - Array.Fill(sparse, -1); - } + // this collection should never be empty + private int[] _sparse; + + private const string INVALID_ID = "ID not in sparse set!"; - public IEnumerator GetEnumerator() + public ref T this[int id] { - for (var i = 0; i < Capacity; i++) + get { - if (!Has(i)) - { - continue; - } - - yield return data[i]; + ref var index = ref EnsureSparseCapacityAndGetIndex(id); + + if (index == -1) + index = _nextIndex++; + + return ref EnsureDenseCapacityAndGetSlot(index); } } - IEnumerator IEnumerable.GetEnumerator() + public SparseSet(int capacity = 4) { - return GetEnumerator(); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + + _dense = new T[capacity]; + _sparse = new int[capacity]; } + public IEnumerator GetEnumerator() => new SparseSetEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public ref T Get(int id) { - if (id < 0 || id >= Count) - { - throw new IndexOutOfRangeException($"Index {id} is out of range."); + var localSparse = _sparse; + if(!((uint)id < (uint)localSparse.Length)) + {//out of range + ThrowHelper.Throw_ArgumentOutOfRange(INVALID_ID, id); } + var index = localSparse[id]; - return ref data[sparse[id]]; - } - - public bool TryGet(int id, [MaybeNullWhen(false)] out T entity) - { - entity = default; - - if (!Has(id)) + var localDense = _dense; + if (!((uint)index < (uint)localDense.Length)) { - return false; + ThrowHelper.Throw_ArgumentOutOfRange(INVALID_ID, id); } - entity = Get(id); - - return true; + return ref localDense[index]; } - public bool Add(int id, T value) + public bool TryGet(int id, [MaybeNullWhen(false)] out T value) { - if (Has(id)) - { - return false; - } - - EnsureCapacity(id + 1); + var localSparse = _sparse; + if (!((uint)id < (uint)localSparse.Length)) + goto doesntExist; - data[Count] = value; - sparse[id] = Count; - dense[Count] = id; - - Count++; + var index = localSparse[id]; - return true; - } + var localDense = _dense; + if (!((uint)index < (uint)localDense.Length)) + goto doesntExist; - public void Set(int id, T value) - { - EnsureCapacity(id + 1); + value = localDense[index]; + return false; - data[id] = value; + //saves a bit of code size + doesntExist: + value = default; + return false; } + public void SetOrAdd(int id, T item) => this[id] = item; + public bool Remove(int id) { - if (!Has(id)) - { - return false; - } + int moveDownIndex = --_nextIndex; - var denseIndex = sparse[id]; + var localSparse = _sparse; - if (denseIndex == -1) - { + if (!((uint)id < (uint)localSparse.Length)) return false; - } - var index = sparse[id]; + int moveIntoIndex = localSparse[id]; - var lastCount = Count - 1; - var lastIndex = dense[lastCount]; + var localDense = _dense; + if (!((uint)moveIntoIndex < (uint)localDense.Length)) + return false;//here, moveIntoIndex should really only ever be -1. We check against len to elide bounds check + + ref T from = ref localDense[moveDownIndex]; + localDense[moveIntoIndex] = from; - if (index != lastCount) - { - data[index] = data[lastCount]; - dense[index] = lastIndex; - sparse[lastIndex] = index; - } - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - data[lastCount] = default!; - } - - sparse[id] = -1; - - Count--; + from = default!; return true; } public bool Has(int id) { - return id >= 0 && id < Capacity && sparse[id] != -1; + var sparse = _sparse; + if (!((uint)id < (uint)sparse.Length)) + return false; + return sparse[id] != -1; } - public void Resize(int size) + public void EnsureCapacity(int capacity) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(size, nameof(size)); - - var shrinking = size < Count; - var growing = size > Count; - - if (shrinking) + if(_dense.Length < capacity) { - for (int i = size; i < Count; i++) - { - if (dense[i] < 0) - { - sparse[dense[i]] = -1; - } - } - - Count = size; + Array.Resize(ref _dense, capacity); } + } - Array.Resize(ref data, size); - Array.Resize(ref dense, size); - Array.Resize(ref sparse, size); + /// + /// Note: this span will become invalid on resize or add + /// + public Span AsSpan() => _dense.AsSpan(0, _nextIndex); - if (growing) - { - Array.Fill(sparse, -1, Count, size - Count); + public void Clear() + { + _nextIndex = 0; + _sparse.AsSpan().Fill(-1); + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + _dense.AsSpan().Clear(); + } - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - Array.Fill(dense, default, Count, size - Count); - } + private ref int EnsureSparseCapacityAndGetIndex(int id) + { + var localSparse = _sparse; + if((uint)id < (uint)localSparse.Length) + { + return ref localSparse[id]; } - Capacity = size; - } - - public void EnsureCapacity(int capacity) - { - ArgumentOutOfRangeException.ThrowIfNegative(capacity, nameof(capacity)); + return ref ResizeArrayAndGet(ref _sparse, id); - if (capacity <= Capacity) + static ref int ResizeArrayAndGet(ref int[] arr, int index) { - return; + int prevLen = arr.Length; + Array.Resize(ref arr, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + arr.AsSpan(prevLen).Fill(-1); + return ref arr[index]; } - - var newCapacity = Math.Max(1, Capacity); - - while (newCapacity <= capacity) + } + + private ref T EnsureDenseCapacityAndGetSlot(int index) + { + var localDense = _dense; + if ((uint)index < (uint)localDense.Length) { - newCapacity *= 2; + return ref localDense[index]; } - Resize(newCapacity); - } + return ref ResizeArrayAndGet(ref _dense, index); - public void Clear() - { - if (Count <= 0) + static ref T ResizeArrayAndGet(ref T[] arr, int index) { - return; + Array.Resize(ref arr, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref arr[index]; } - - Array.Clear(data); - Array.Fill(sparse, -1); - - Count = 0; } - public void Dispose() + public struct SparseSetEnumerator(SparseSet set) : IEnumerator { - data = null!; - dense = null!; - sparse = null!; + private readonly SparseSet _toEnumerate = set; + private readonly int _version = set._version; + private int _index = -1; + + public readonly ref T Current => ref _toEnumerate._dense[_index]; + + public bool MoveNext() + { + if (_version != _toEnumerate._version) + ThrowHelper.Throw_InvalidOperation("Collection has been modified, cannot continue iteration."); + return ++_index < _toEnumerate._nextIndex; + } + + public void Reset() => _index = -1; + + readonly object? IEnumerator.Current => Current; + readonly T IEnumerator.Current => _toEnumerate._dense[_index]; + public readonly void Dispose() { } } } \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Data/Archetype.cs b/src/EndlessEscapade/Core/ECS/Data/Archetype.cs new file mode 100644 index 00000000..c3ed2170 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/Archetype.cs @@ -0,0 +1,130 @@ +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Terraria; + +namespace EndlessEscapade.Core.ECS; + +public partial class Archetype +{ + private static int nextArchetypeID; + + static Archetype() + { + _ = GetArchetypeID([]); + } + + internal static FastStack<(ImmutableArray Components, byte[] IndexMap)> MetadataTable = new(); + private static readonly Dictionary<(ulong h1, ulong h2), ArchetypeID> ExistingArchetypeIDs = []; + + public static ArchetypeID GetArchetypeID(ReadOnlySpan types) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(types.Length, byte.MaxValue); + + ulong hash1 = 0; + ulong hash2 = 0; + foreach (var type in types) + { + hash1 ^= (ulong)type.GetHashCode() * 98317U; + hash2 += (ulong)type.GetHashCode() * 53U; + } + + ref ArchetypeID id = ref CollectionsMarshal.GetValueRefOrAddDefault(ExistingArchetypeIDs, (hash1, hash2), out bool exists); + if (exists) + return id; + + ushort newRawId = checked((ushort)nextArchetypeID++); + + int max = int.MinValue; + foreach(var type in types) + max = Math.Max(max, type.GetRawValue()); + + byte[] indexMap = new byte[max]; + indexMap.AsSpan().Fill(byte.MaxValue); + + for(int i = 0; i < types.Length; i++) + { + indexMap[types[i].GetRawValue()] = (byte)i; + } + + MetadataTable.Push((types.ToImmutableArray(), indexMap)); + + return id = ArchetypeID.CreateNew(newRawId); + } + + public static Archetype Create(ArchetypeID id) + { + ref var arr = ref MetadataTable[id.GetRawValue()]; + var components = arr.Components; + + ComponentStorage[] storages = new ComponentStorage[components.Length]; + for(int i = 0; i < components.Length; i++) + { + storages[i] = Component.Create(components[i]); + } + + return new Archetype(id, storages, arr.IndexMap); + } +} + +public partial class Archetype(ArchetypeID id, ComponentStorage[] storages, byte[] indexMap) +{ + internal readonly ArchetypeID ID = id; + internal readonly ComponentStorage[] Storages = storages; + internal readonly byte[] IndexMap = indexMap; + private readonly ComponentStorage _entities = new(); + private int _nextIndex; + public int Create(out Ref slot) + { + if (_nextIndex == _entities.Capacity) + Resize(_nextIndex << 1); + slot = new(ref _entities[_nextIndex]); + return _nextIndex++; + } + + private void Resize(int newSize) + { + foreach (var i in Storages) + i.Resize(newSize); + _entities.Resize(newSize); + } + + /// + /// Moves the top entity's components into and clears the top slot if needed. + /// + /// The entity id and version of the entity that was moved down + public EntityLight Delete(int index) + { + int capacity = _entities.Capacity; + foreach(var stor in Storages) + stor.Delete(index, capacity); + var @return = _entities[capacity - 1]; + _entities.Delete(index, capacity); + return @return; + } + + public EntityLight DeleteFromEntityStorageOnly(int index) + { + int capacity = _entities.Capacity; + var @return = _entities[capacity - 1]; + _entities.Delete(index, capacity); + return @return; + } + + public ref T GetComponent(int index) => ref ((ComponentStorage)Storages[IndexMap[Component.ID.GetRawValue()]])[index]; + public ref T GetComponentKnownComponentStorageIndex(int index, int storageIndex) => + ref ((ComponentStorage)Storages[storageIndex])[index]; + public bool HasComponent(out int index) => (index = IndexMap[Component.ID.GetRawValue()]) != byte.MaxValue; + public ComponentStorage GetComponentStorage() => (ComponentStorage)Storages[IndexMap[Component.ID.GetRawValue()]]; + public int Capacity => _entities.Capacity; + public int Count => _nextIndex; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Data/ArchetypeID.cs b/src/EndlessEscapade/Core/ECS/Data/ArchetypeID.cs new file mode 100644 index 00000000..3fd60bce --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/ArchetypeID.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Data; + +public struct ArchetypeID +{ + private ushort _value; + + public readonly ImmutableArray ComponentIDs => Archetype.MetadataTable[_value].Components; + + + internal ushort GetRawValue() => _value; + internal static ArchetypeID CreateNew(ushort value) => new() { _value = value }; +} diff --git a/src/EndlessEscapade/Core/ECS/Data/Component.cs b/src/EndlessEscapade/Core/ECS/Data/Component.cs new file mode 100644 index 00000000..b3a268ed --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/Component.cs @@ -0,0 +1,33 @@ +using EndlessEscapade.Core.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace EndlessEscapade.Core.ECS.Data; +internal class Component +{ + public static readonly ComponentID ID = Component.GetComponentID(); +} + +internal static class Component +{ + private static int nextComponentID = 1; + private static Dictionary existingComponentIDs = []; + internal static FastStack<(Type UnderlyingType, Func StorageFactory)> ComponentMetaDataTable = new(); + public static ComponentStorage Create(ComponentID componentID) => ComponentMetaDataTable[componentID.GetRawValue()].StorageFactory(); + + static Component() + { + ComponentMetaDataTable.Push((typeof(void), () => throw new NotSupportedException("Cannot create component storage of default(ComponentID)"))); + } + + public static ComponentID GetComponentID() + { + ref ComponentID elem = ref CollectionsMarshal.GetValueRefOrAddDefault(existingComponentIDs, typeof(T), out var exists); + if (exists) + return elem; + + ComponentMetaDataTable.Push((typeof(T), () => new ComponentStorage())); + + return elem = ComponentID.CreateFromRawValue((ushort)nextComponentID++); + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Data/ComponentID.cs b/src/EndlessEscapade/Core/ECS/Data/ComponentID.cs new file mode 100644 index 00000000..5bac6d79 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/ComponentID.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Data; + +public struct ComponentID : IEquatable +{ + private ushort _value; + public Type Type => Component.ComponentMetaDataTable[_value].UnderlyingType; + public bool Equals(ComponentID other) => other._value == _value; + public static bool operator ==(ComponentID left, ComponentID right) => left.Equals(right); + public static bool operator !=(ComponentID left, ComponentID right) => !left.Equals(right); + public ushort GetRawValue() => _value; + public static ComponentID CreateFromRawValue(ushort raw) => new() { _value = raw }; +} diff --git a/src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs b/src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs new file mode 100644 index 00000000..c43fac95 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Data/ComponentStorage.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Data; + +public abstract class ComponentStorage +{ + /// + /// Moves the top component into , clearing the top spot if needed. + /// + public abstract void Delete(int index, int capacity); + + /// + /// Returns and boxes the component at + /// + public abstract object? Box(int index); + + /// + /// Pulls a component from another component storage of the same type, and removes it from the other one. + /// This causes two copy operations and potentially one clear operation. + /// + public abstract void Pull(Archetype other, int otherIndex, int myIndex, int otherTop); + + public abstract void Resize(int size); +} + +public sealed class ComponentStorage : ComponentStorage +{ + public int Capacity => _items.Length; + + public override void Resize(int size) => Array.Resize(ref _items, size); + + public override void Pull(Archetype other, int otherIndex, int myIndex, int otherTop) + { + var typedComponentStorage = other.GetComponentStorage(); + + // key + // x == item, - == empty + + // this | other + // x | x + // x | x <- otherIndex/center + // x | x + // - <- myIndex| x <- otherTop + // - | - + + //center -> myIndex + //otherIndex -> center + //clear otherIndex + + var localOther = typedComponentStorage._items; + if (!((uint)otherIndex < (uint)localOther.Length && (uint)otherTop < (uint)localOther.Length)) + ThrowHelper.Throw_ArgumentOutOfRange("Invalid Index", otherIndex); + + ref var center = ref localOther[otherIndex]; + + this[myIndex] = center; + + + ref var top = ref localOther[otherTop]; + center = top; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + top = default; + } + + public override void Delete(int index, int capacity) + { + var local = _items; + if (!((uint)index < (uint)local.Length && (uint)capacity < (uint)local.Length)) + ThrowHelper.Throw_ArgumentOutOfRange("Invalid Index", index); + + ref var top = ref local[capacity]; + ref var bottom = ref local[index]; + + bottom = top; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + top = default; + } + + private T[] _items; + + public ComponentStorage() + { + _items = []; + } + + public ref T this[int index] + { + get + { + var locItems = _items; + if ((uint)index < (uint)locItems.Length) + { + return ref locItems[index]; + } + + return ref ResizeAndGet(index); + } + } + + private ref T ResizeAndGet(int index) + { + Array.Resize(ref _items, (int)BitOperations.RoundUpToPowerOf2((uint)index + 1)); + return ref _items[index]; + } + + public override object? Box(int index) => _items[index]; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Entity.cs b/src/EndlessEscapade/Core/ECS/Entity.cs new file mode 100644 index 00000000..1c0caf82 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Entity.cs @@ -0,0 +1,47 @@ +namespace EndlessEscapade.Core.ECS; + +public readonly struct Entity(int entityID, int entityVersion, World world) +{ + public readonly int EntityID = entityID; + public readonly int EntityVersion = entityVersion; + public readonly World World = world; + + public readonly bool Has() + { + var location = World.Table[EntityID]; + return location.Version != EntityVersion && location.Archetype.HasComponent(out _); + } + + public readonly ref T Get() + { + var location = World.Table[EntityID]; + if(location.Version != EntityVersion) + ThrowHelper.Throw_InvalidOperation(EntityIsDead); + return ref location.Archetype.GetComponent(location.Index); + } + + public readonly bool TryGet(out Ref item) + { + var location = World.Table[EntityID]; + if(location.Version != EntityVersion) + goto noComponent; + + if(!location.Archetype.HasComponent(out int storageIndex)) + goto noComponent; + + item = new(ref location.Archetype.GetComponentKnownComponentStorageIndex( + location.Index, + storageIndex)); + + return true; + + noComponent: + item = default!; + return false; + } + public bool Add(in T component) => World.Add(this, component); + public bool Remove() => World.Remove(this); + public bool Delete() => World.Delete(this); + + private const string EntityIsDead = "Entity is Dead"; +} diff --git a/src/EndlessEscapade/Core/ECS/EntityLight.cs b/src/EndlessEscapade/Core/ECS/EntityLight.cs new file mode 100644 index 00000000..8997f973 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/EntityLight.cs @@ -0,0 +1,2 @@ +namespace EndlessEscapade.Core.ECS; +public record struct EntityLight(int ID, int Version); \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/EntityTemplate.cs b/src/EndlessEscapade/Core/ECS/EntityTemplate.cs new file mode 100644 index 00000000..03f48e12 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/EntityTemplate.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; + +namespace EndlessEscapade.Core.ECS; + +public struct Add : IEntityTemplate + where TRest : IEntityTemplate +{ + public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID>(); + private static ArchetypeID? cache; + + + public World World => Rest.World; + public Entity Entity => World.SetComponents(ref this, ArchetypeID); + + + public TRest Rest; + public T Value; + + + public Add, TN> Set(TN value) => new() + { + Rest = this, + Value = value + }; + + public void AppendTypes(ref FastStack types) + { + types.Push(Component.ID); + Rest.AppendTypes(ref types); + } + + public void SetComponents(Archetype archetype, int index) + { + archetype.GetComponent(index) = Value; + Rest.SetComponents(archetype, index); + } +} + +public struct EntityTemplate : IEntityTemplate +{ + public readonly ArchetypeID ArchetypeID => cache ??= IEntityTemplate.CalculateArchetypeID(); + private static ArchetypeID? cache; + + public required World World { get; init; } + public Entity Entity => World.SetComponents(ref this, ArchetypeID); + + public Add Set(TN value) => new() + { + Rest = this, + Value = value, + }; + + public void AppendTypes(ref FastStack types) { } + public void SetComponents(Archetype archetype, int index) { } +} + +public interface IEntityTemplate +{ + public void AppendTypes(ref FastStack types); + public void SetComponents(Archetype archetype, int index); + public Entity Entity { get; } + public ArchetypeID ArchetypeID { get; } + public World World { get; } + + static ArchetypeID CalculateArchetypeID() + where T : struct, IEntityTemplate + { + ref FastStack types = ref sharedStack; + default(T)!.AppendTypes(ref types); + var id = Archetype.GetArchetypeID(types.AsSpan()); + types.Clear(); + return id; + } + + [ThreadStatic] + private static FastStack sharedStack = new(); +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Ref.cs b/src/EndlessEscapade/Core/ECS/Ref.cs new file mode 100644 index 00000000..6ce7c3a7 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Ref.cs @@ -0,0 +1,14 @@ +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Framework.Collections; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace EndlessEscapade.Core.ECS; + +public ref struct Ref(ref T r) +{ + public ref T R = ref r; +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Systems/ISystem.cs b/src/EndlessEscapade/Core/ECS/Systems/ISystem.cs new file mode 100644 index 00000000..fcad818a --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/ISystem.cs @@ -0,0 +1,11 @@ +namespace EndlessEscapade.Core.ECS.Systems; + +internal interface ISystem +{ + /// + /// Lower == executed earlier + /// + int Order => 0; + Query BuildQuery(World world); + public void Execute(Query query); +} diff --git a/src/EndlessEscapade/Core/ECS/Systems/Query.cs b/src/EndlessEscapade/Core/ECS/Systems/Query.cs new file mode 100644 index 00000000..7f5d62aa --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/Query.cs @@ -0,0 +1,46 @@ +using EndlessEscapade.Core.Collections; +using System.Collections.Immutable; + +namespace EndlessEscapade.Core.ECS.Systems; + +public class Query +{ + public World World { get; init; } + private FastStack _appliesTo; + private ImmutableArray _rules; + + public Query(World world, ImmutableArray rules) + { + World = world; + world.OnArchetypeAdded += a => + { + if(Applies(a, _rules.AsSpan())) + _appliesTo.Push(a); + }; + + _rules = rules; + + foreach(var archetype in world.Archetypes) + { + if(Applies(archetype, rules.AsSpan())) + { + _appliesTo.Push(archetype); + } + } + } + + public ReadOnlySpan Archetypes => _appliesTo.AsSpan(); + + private static bool Applies(Archetype archetype, ReadOnlySpan rules) + { + var indicies = archetype.IndexMap; + foreach(var item in rules) + { + if(!item.Applies(indicies)) + { + return false; + } + } + return true; + } +} diff --git a/src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs b/src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs new file mode 100644 index 00000000..b45ac6db --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/QueryTemplate.cs @@ -0,0 +1,29 @@ +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; +using System.Collections.Immutable; + +namespace EndlessEscapade.Core.ECS.Systems; + +public struct QueryBuilder(World world) +{ + public World World { get; } = world; + + private FastStack _rules = new(); + + public QueryBuilder With() + { + _rules.Push(new Rule(Rule.RuleType.Include, Component.ID)); + return this; + } + + public QueryBuilder Without() + { + _rules.Push(new Rule(Rule.RuleType.Exclude, Component.ID)); + return this; + } + + public Query Build() + { + return new Query(World, _rules.AsSpan().ToImmutableArray()); + } +} \ No newline at end of file diff --git a/src/EndlessEscapade/Core/ECS/Systems/Rule.cs b/src/EndlessEscapade/Core/ECS/Systems/Rule.cs new file mode 100644 index 00000000..21d9ef78 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/Rule.cs @@ -0,0 +1,27 @@ +using EndlessEscapade.Core.ECS.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EndlessEscapade.Core.ECS.Systems; +public readonly struct Rule(Rule.RuleType type, ComponentID operand) +{ + public readonly RuleType Operator = type; + public readonly ComponentID Operand = operand; + + public bool Applies(byte[] indexMapping) => + Operator switch + { + RuleType.Include => indexMapping[Operand.GetRawValue()] != byte.MaxValue, + RuleType.Exclude => indexMapping[Operand.GetRawValue()] == byte.MaxValue, + _ => throw new NotSupportedException(), + }; + + public enum RuleType + { + Include, + Exclude, + } +} diff --git a/src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs b/src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs new file mode 100644 index 00000000..e8c95edb --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/Systems/SystemGroup.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EndlessEscapade.Core.ECS.Systems; +internal class SystemGroup +{ + private SortedList _systems; + private World _world; + + public SystemGroup(World world, params ISystem[] systems) + { + _world = world; + _systems = new(systems.Length); + foreach(var item in systems) + _systems.Add(item.Order, (item, item.BuildQuery(world))); + } + + public void Add(ISystem system) => _systems.Add(system.Order, (system, system.BuildQuery(_world))); + + public void Execute() + { + foreach ((_, (var system, var query)) in _systems) + { + system.Execute(query); + } + } +} diff --git a/src/EndlessEscapade/Core/ECS/World.cs b/src/EndlessEscapade/Core/ECS/World.cs new file mode 100644 index 00000000..9c0ef171 --- /dev/null +++ b/src/EndlessEscapade/Core/ECS/World.cs @@ -0,0 +1,189 @@ +using EndlessEscapade.Core.Collections; +using EndlessEscapade.Core.ECS.Data; +using EndlessEscapade.Core.ECS.Systems; +using EndlessEscapade.Framework.Collections; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using Terraria; + +namespace EndlessEscapade.Core.ECS; + +public class World +{ + public Action? OnArchetypeAdded; + + //archetypeID -> Archetype + private SparseSet _archetypes = new(); + + private Dictionary _archetypeGraph = []; + private Dictionary, HashSet> QueryCache = []; + + internal DenseSet Table = new(); + private FastStack _recycledIDs = new FastStack(); + private int _nextID; + + public Entity SetComponents(ref readonly T template, ArchetypeID archetypeID) + where T : IEntityTemplate + { + (int id, int version) = _recycledIDs.TryPop(out var newId) ? newId : new(_nextID++, 1); + + var archetype = _archetypes[archetypeID.GetRawValue()] ??= InvokeArchetypeEvents(Archetype.Create(archetypeID)); + + var index = archetype.Create(out var entitySlot); + entitySlot.R.ID = id; + entitySlot.R.Version = id; + + Table[id] = new EntityLocation(archetype, index, version); + + template.SetComponents(archetype, index); + + return new Entity(id, version, this); + } + + public EntityTemplate Entity() => new() { World = this }; + + public bool Add(Entity entity, in T component) + { + Debug.Assert(entity.World == this); + + ref EntityLocation location = ref Table[entity.EntityID]; + if (location.Archetype.HasComponent(out _) || location.Version != entity.EntityVersion) + return false; + + var destination = CollectionsMarshal.GetValueRefOrAddDefault(_archetypeGraph, new ArchetypeGraphEdge(true, Component.ID, location.Archetype), out _) + ??= FindAdjacientArchetypeCold(true, location.Archetype, Component.ID); + + MoveEntityTo(entity, ref location, location.Archetype, destination); + + destination.GetComponent(location.Index) = component; + + var world = this; + + return true; + } + + public bool Remove(Entity entity) + { + Debug.Assert(entity.World == this); + + ref EntityLocation location = ref Table[entity.EntityID]; + if (!location.Archetype.HasComponent(out _) || location.Version != entity.EntityVersion) + return false; + + var destination = CollectionsMarshal.GetValueRefOrAddDefault(_archetypeGraph, new ArchetypeGraphEdge(false, Component.ID, location.Archetype), out _) + ??= FindAdjacientArchetypeCold(false, location.Archetype, Component.ID); + + location.Archetype.GetComponentStorage().Delete(location.Index, location.Archetype.Capacity); + MoveEntityTo(entity, ref location, location.Archetype, destination); + + + return true; + } + + private Archetype FindAdjacientArchetypeCold(bool isAdding, Archetype from, ComponentID type) + { + var types = from.ID.ComponentIDs.AsSpan(); + + ArchetypeID nextId; + + if (isAdding) + { + Span newIds = stackalloc ComponentID[types.Length + 1]; + types.CopyTo(newIds[..^1]); + newIds[^1] = type; + nextId = Archetype.GetArchetypeID(newIds); + } + else + { + Span newIds = stackalloc ComponentID[types.Length - 1]; + int index = 0; + foreach (var id in types) + if (id != type) + newIds[index++] = id; + Debug.Assert(index == newIds.Length); + nextId = Archetype.GetArchetypeID(newIds); + } + + return _archetypes[nextId.GetRawValue()] ??= InvokeArchetypeEvents(Archetype.Create(nextId)); + } + + private Archetype InvokeArchetypeEvents(Archetype archetype) + { + OnArchetypeAdded?.Invoke(archetype); + return archetype; + } + + /// + /// Moves the components of an entity from to . + /// If a component type is not in , it is not updated and needs to be done manually. + /// If a component type is not in , the destination skips it. + /// + private void MoveEntityTo(Entity entity, ref EntityLocation location, Archetype from, Archetype destination) + { + var fromIndex = location.Index; + var fromTop = location.Archetype.Count; + var destIndex = destination.Create(out var slot); + + foreach (var storage in destination.Storages) + { + storage.Pull(location.Archetype, fromIndex, destIndex, fromTop); + } + + //update entity records + + var movedEntity = from.DeleteFromEntityStorageOnly(fromIndex); + + ref var movedEntityLocation = ref Table[movedEntity.ID]; + + movedEntityLocation = location; + location.Archetype = destination; + location.Index = destIndex; + + slot.R.ID = entity.EntityID; + slot.R.Version = entity.EntityVersion; + + + } + + public bool Delete(Entity entity) + { + ref var location = ref Table[entity.EntityID]; + if(location.Version != entity.EntityVersion) + return false; + + var deletedEntity = location.Archetype.Delete(location.Index); + + ref var movedDownEntityLocation = ref Table[deletedEntity.ID]; + movedDownEntityLocation = location; + location = EntityLocation.Default; + + _recycledIDs.Push(new(entity.EntityID, entity.EntityVersion)); + + return true; + } + + public QueryBuilder Query() => new(this); + + internal struct EntityLocation(Archetype archetype, int index, int version) + { + internal Archetype Archetype = archetype; + internal int Index = index; + internal int Version = version; + + public static EntityLocation Default = new EntityLocation(null!, -1, -1); + } + + public ReadOnlySpan Archetypes => _archetypes.AsSpan(); + + internal record struct ArchetypeGraphEdge(bool IsAddAction, ComponentID Delta, Archetype Archetype); +} diff --git a/src/EndlessEscapade/Core/ThrowHelper.cs b/src/EndlessEscapade/Core/ThrowHelper.cs new file mode 100644 index 00000000..9b9e35cc --- /dev/null +++ b/src/EndlessEscapade/Core/ThrowHelper.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace EndlessEscapade.Core; + +internal static class ThrowHelper +{ + public static void Throw_ArgumentOutOfRange(string message, int index, [CallerArgumentExpression(nameof(index))] string? paramName = null) + { + throw new ArgumentOutOfRangeException(paramName, index, message); + } + + public static void Throw_InvalidOperation(string message) + { + throw new InvalidOperationException(message); + } +} diff --git a/src/EndlessEscapade/EndlessEscapade.csproj b/src/EndlessEscapade/EndlessEscapade.csproj index 4b3fbffc..8781cb4b 100644 --- a/src/EndlessEscapade/EndlessEscapade.csproj +++ b/src/EndlessEscapade/EndlessEscapade.csproj @@ -1,6 +1,6 @@  - + true @@ -14,7 +14,7 @@ - +