From 948268e9bac7618591a306a81dcd11a521956ef0 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sun, 30 Apr 2023 11:47:26 +0200 Subject: [PATCH] WIP --- src/gaemstone.ECS/Component.cs | 80 +------- src/gaemstone.ECS/Entity+Bare.cs | 35 ++++ src/gaemstone.ECS/Entity.cs | 166 +++++++++++++-- src/gaemstone.ECS/EntityBuilder.cs | 61 +++--- src/gaemstone.ECS/EntityPath.cs | 73 +++---- src/gaemstone.ECS/EntityRef.cs | 193 ------------------ src/gaemstone.ECS/EntityType.cs | 18 +- src/gaemstone.ECS/Exceptions.cs | 31 +++ src/gaemstone.ECS/Filter.cs | 27 ++- src/gaemstone.ECS/Id+Bare.cs | 36 ++++ src/gaemstone.ECS/Id.cs | 71 ++++--- src/gaemstone.ECS/IdRef.cs | 53 ----- src/gaemstone.ECS/Internal/EntityAccess.cs | 117 +++++++++++ src/gaemstone.ECS/Internal/FlecsBuiltIn.cs | 11 + src/gaemstone.ECS/Internal/Iterator.cs | 110 ++++++++++ src/gaemstone.ECS/Internal/Lookup.cs | 9 + src/gaemstone.ECS/Iterator.cs | 186 ++++++----------- src/gaemstone.ECS/Observer.cs | 13 +- src/gaemstone.ECS/Query.cs | 31 ++- src/gaemstone.ECS/Rule.cs | 51 ++--- src/gaemstone.ECS/System.cs | 23 ++- src/gaemstone.ECS/Term.cs | 15 +- .../Utility}/Allocators.cs | 17 +- .../Utility}/CStringExtensions.cs | 2 +- .../Utility}/CallbackContextHelper.cs | 2 +- src/gaemstone.ECS/Utility/ReferenceHandle.cs | 71 +++++++ .../Utility}/SpanExtensions.cs | 2 +- src/gaemstone.ECS/World+Bare.cs | 73 +++++++ src/gaemstone.ECS/World+Lookup.cs | 38 ---- src/gaemstone.ECS/World.cs | 78 ++++--- src/gaemstone.Utility/FlecsException.cs | 26 --- 31 files changed, 990 insertions(+), 729 deletions(-) create mode 100644 src/gaemstone.ECS/Entity+Bare.cs delete mode 100644 src/gaemstone.ECS/EntityRef.cs create mode 100644 src/gaemstone.ECS/Exceptions.cs create mode 100644 src/gaemstone.ECS/Id+Bare.cs delete mode 100644 src/gaemstone.ECS/IdRef.cs create mode 100644 src/gaemstone.ECS/Internal/EntityAccess.cs create mode 100644 src/gaemstone.ECS/Internal/FlecsBuiltIn.cs create mode 100644 src/gaemstone.ECS/Internal/Iterator.cs create mode 100644 src/gaemstone.ECS/Internal/Lookup.cs rename src/{gaemstone.Utility => gaemstone.ECS/Utility}/Allocators.cs (91%) rename src/{gaemstone.Utility => gaemstone.ECS/Utility}/CStringExtensions.cs (97%) rename src/{gaemstone.Utility => gaemstone.ECS/Utility}/CallbackContextHelper.cs (94%) create mode 100644 src/gaemstone.ECS/Utility/ReferenceHandle.cs rename src/{gaemstone.Utility => gaemstone.ECS/Utility}/SpanExtensions.cs (97%) create mode 100644 src/gaemstone.ECS/World+Bare.cs delete mode 100644 src/gaemstone.ECS/World+Lookup.cs delete mode 100644 src/gaemstone.Utility/FlecsException.cs diff --git a/src/gaemstone.ECS/Component.cs b/src/gaemstone.ECS/Component.cs index 27cdffa..986d6b6 100644 --- a/src/gaemstone.ECS/Component.cs +++ b/src/gaemstone.ECS/Component.cs @@ -1,13 +1,13 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public static unsafe class ComponentExtensions +public unsafe partial struct Entity { - public static EntityRef InitComponent(this EntityRef entity) + public Entity InitComponent() { if (typeof(T).IsPrimitive) throw new ArgumentException( "Must not be primitive"); @@ -17,8 +17,8 @@ public static unsafe class ComponentExtensions var size = typeof(T).IsValueType ? Unsafe.SizeOf() : sizeof(ReferenceHandle); var typeInfo = new ecs_type_info_t { size = size, alignment = size }; - var componentDesc = new ecs_component_desc_t { entity = entity, type = typeInfo }; - ecs_component_init(entity.World, &componentDesc); + var componentDesc = new ecs_component_desc_t { entity = this, type = typeInfo }; + ecs_component_init(World, &componentDesc); if (!typeof(T).IsValueType) { // Set up component hooks for proper freeing of GCHandles. @@ -29,75 +29,9 @@ public static unsafe class ComponentExtensions move = new() { Data = new() { Pointer = &ReferenceHandle.Move } }, copy = new() { Data = new() { Pointer = &ReferenceHandle.Copy } }, }; - ecs_set_hooks_id(entity.World, entity, &typeHooks); + ecs_set_hooks_id(World, this, &typeHooks); } - return entity; - } -} - -public unsafe readonly struct ReferenceHandle - : IDisposable -{ - public static int NumActiveHandles { get; private set; } - - - private readonly nint _value; - - public object? Target => (_value != default) - ? ((GCHandle)_value).Target : null; - - private ReferenceHandle(nint value) - => _value = value; - - public static ReferenceHandle Alloc(object? target) - { - if (target == null) return default; - NumActiveHandles++; - return new((nint)GCHandle.Alloc(target)); - } - - public ReferenceHandle Clone() - => Alloc(Target); - - public void Dispose() - { - if (_value == default) return; - NumActiveHandles--; - ((GCHandle)_value).Free(); - } - - - [UnmanagedCallersOnly] - internal static void Construct(void* ptr, int count, ecs_type_info_t* _) - => new Span(ptr, count).Clear(); - - [UnmanagedCallersOnly] - internal static void Destruct(void* ptr, int count, ecs_type_info_t* _) - { - var span = new Span(ptr, count); - foreach (var handle in span) handle.Dispose(); - span.Clear(); - } - - [UnmanagedCallersOnly] - internal static void Move(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _) - { - var dst = new Span(dstPtr, count); - var src = new Span(srcPtr, count); - foreach (var handle in dst) handle.Dispose(); - src.CopyTo(dst); - src.Clear(); - } - - [UnmanagedCallersOnly] - internal static void Copy(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _) - { - var dst = new Span(dstPtr, count); - var src = new Span(srcPtr, count); - for (var i = 0; i < count; i++) { - dst[i].Dispose(); - dst[i] = src[i].Clone(); - } + return CreateLookup(); } } diff --git a/src/gaemstone.ECS/Entity+Bare.cs b/src/gaemstone.ECS/Entity+Bare.cs new file mode 100644 index 0000000..d42461c --- /dev/null +++ b/src/gaemstone.ECS/Entity+Bare.cs @@ -0,0 +1,35 @@ +using System; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public readonly partial struct Entity + : IEquatable +{ + public static readonly Entity None = default; + + public readonly ecs_entity_t Value; + public uint NumericId => (uint)Value.Data; + + public bool IsSome => Value.Data != 0; + public bool IsNone => Value.Data == 0; + + public Entity(ecs_entity_t value) => Value = value; + + public bool Equals(Entity other) => Value.Data == other.Value.Data; + public override bool Equals(object? obj) => (obj is Entity other) && Equals(other); + public override int GetHashCode() => Value.Data.GetHashCode(); + public override string? ToString() + => IsSome ? $"Entity({Value.Data.Data})" + : "Entity.None"; + + public static bool operator ==(Entity left, Entity right) => left.Equals(right); + public static bool operator !=(Entity left, Entity right) => !left.Equals(right); + + public static implicit operator Id (Entity e) => new(e.Value.Data); + public static implicit operator ecs_entity_t(Entity e) => e.Value; + public static implicit operator ecs_id_t (Entity e) => e.Value.Data; + + public static implicit operator Term (Entity entity) => new(entity); + public static implicit operator TermId(Entity entity) => new(entity); +} diff --git a/src/gaemstone.ECS/Entity.cs b/src/gaemstone.ECS/Entity.cs index 5493c20..be1abc9 100644 --- a/src/gaemstone.ECS/Entity.cs +++ b/src/gaemstone.ECS/Entity.cs @@ -1,32 +1,162 @@ using System; +using System.Collections.Generic; +using System.Linq; +using gaemstone.ECS.Internal; using static flecs_hub.flecs; namespace gaemstone.ECS; -public readonly struct Entity - : IEquatable +public unsafe readonly partial struct Entity + : IEquatable> { - public static readonly Entity None = default; + public static readonly Entity None = default; - public readonly ecs_entity_t Value; - public uint Id => (uint)Value.Data; + public readonly World World; + public readonly Entity Value; - public bool IsSome => Value.Data != 0; - public bool IsNone => Value.Data == 0; + public uint NumericId => Value.NumericId; + public bool IsNone => Value.IsNone; + public bool IsSome => Value.IsSome; - public Entity(ecs_entity_t value) => Value = value; + public bool IsValid => EntityAccess.IsValid(World, this); + public bool IsAlive => EntityAccess.IsAlive(World, this); - public bool Equals(Entity other) => Value.Data == other.Value.Data; - public override bool Equals(object? obj) => (obj is Entity other) && Equals(other); - public override int GetHashCode() => Value.Data.GetHashCode(); + public string? Name { get => EntityAccess.GetName(World, this); set => EntityAccess.SetName(World, this, value); } + public string? Symbol { get => EntityAccess.GetSymbol(World, this); set => EntityAccess.SetSymbol(World, this, value); } + + public EntityPath Path => EntityPath.From(World, this); + public EntityType Type => new(World, ecs_get_type(World, this)); + + public Entity? Parent + => GetOrNull(World, GetTargets(FlecsBuiltIn.ChildOf).FirstOrDefault()); + public IEnumerable> Children + => Iterator.FromTerm(World, new(FlecsBuiltIn.ChildOf, this)).GetAllEntities(); + + + private Entity(World world, Entity value) + { World = world; Value = value; } + + public static Entity GetOrInvalid(World world, Entity value) + => new(world, value); + public static Entity? GetOrNull(World world, Entity value) + => ecs_is_valid(world, value) ? new(world, value) : null; + public static Entity GetOrThrow(World world, Entity value) + => ecs_is_valid(world, value) ? new(world, value) : throw new InvalidOperationException($"The entity {value} is not valid"); + + public void Delete() + => ecs_delete(World, this); + + + public Entity CreateLookup() + { + ref var lookup = ref Lookup.Entity.Value; + if (lookup.IsSome) throw new InvalidOperationException( + $"The lookup for type {typeof(T)} in context {typeof(TContext)} is already in use by {lookup}"); + lookup = this; + return this; + } + + public EntityBuilder NewChild(EntityPath? path = null) + => World.New(path?.ThrowIfAbsolute(), this); + public Entity? LookupChildOrNull(EntityPath path) + => World.LookupPathOrNull(path.ThrowIfAbsolute(), this); + public Entity LookupChildOrThrow(EntityPath path) + => World.LookupPathOrThrow(path.ThrowIfAbsolute()!, this); + + + public Entity ChildOf(Entity parent) + => Add(FlecsBuiltIn.ChildOf, parent); + + public bool IsDisabled => Has(FlecsBuiltIn.Disabled); + public Entity Disable() => Add(FlecsBuiltIn.Disabled); + public Entity Enable() => Remove(FlecsBuiltIn.Disabled); + + + public Entity Add(Id id) { EntityAccess.Add(World, this, id); return this; } + public Entity Add(string symbol) => Add(World.LookupSymbolOrThrow(symbol)); + public Entity Add(Entity relation, Entity target) => Add(Id.Pair(relation, target)); + public Entity Add() => Add(World.Entity()); + public Entity Add(Entity target) => Add(World.Entity(), target); + public Entity Add() => Add(World.Entity(), World.Entity()); + + public Entity Remove(Id id) { EntityAccess.Remove(World, this, id); return this; } + public Entity Remove(string symbol) => Remove(World.LookupSymbolOrThrow(symbol)); + public Entity Remove(Entity relation, Entity target) => Remove(Id.Pair(relation, target)); + public Entity Remove() => Remove(World.Entity()); + public Entity Remove(Entity target) => Remove(World.Entity(), target); + public Entity Remove() => Remove(World.Entity(), World.Entity()); + + public bool Has(Id id) => EntityAccess.Has(World, this, id); + public bool Has(string symbol) => Has(World.LookupSymbolOrThrow(symbol)); + public bool Has(Entity relation, Entity target) => Has(Id.Pair(relation, target)); + public bool Has() => Has(World.Entity()); + public bool Has(Entity target) => Has(World.Entity(), target); + public bool Has() => Has(World.Entity(), World.Entity()); + + + public T? GetOrNull(Id id) where T : unmanaged => EntityAccess.GetOrNull(World, this, id); + public T? GetOrNull(Id id, T _ = null!) where T : class => EntityAccess.GetOrNull(World, this, id); + public T GetOrThrow(Id id) => EntityAccess.GetOrThrow(World, this, id); + public ref T GetMut(Id id) where T : unmanaged => ref EntityAccess.GetMut(World, this, id); + public ref T GetRefOrNull(Id id) where T : unmanaged => ref EntityAccess.GetRefOrNull(World, this, id); + public ref T GetRefOrThrow(Id id) where T : unmanaged => ref EntityAccess.GetRefOrThrow(World, this, id); + + public Entity Modified(Id id) { EntityAccess.Modified(World, this, id); return this; } + public Entity Set(Id id, in T value) where T : unmanaged { EntityAccess.Set(World, this, id, value); return this; } + public Entity Set(Id id, T value) where T : class { EntityAccess.Set(World, this, id, value); return this; } + + + public T? GetOrNull() where T : unmanaged => GetOrNull(World.Entity()); + public T? GetOrNull(T _ = null!) where T : class => GetOrNull(World.Entity()); + public T GetOrThrow() => GetOrThrow(World.Entity()); + public ref T GetMut() where T : unmanaged => ref GetMut(World.Entity()); + public ref T GetRefOrNull() where T : unmanaged => ref GetRefOrNull(World.Entity()); + public ref T GetRefOrThrow() where T : unmanaged => ref GetRefOrThrow(World.Entity()); + + public Entity Modified() => Modified(World.Entity()); + public Entity Set(in T value) where T : unmanaged => Set(World.Entity(), value); + public Entity Set(T value) where T : class => Set(World.Entity(), value); + + + public IEnumerable> GetTargets(Entity relation) + { + foreach (var entity in EntityAccess.GetTargets(World, this, relation)) + yield return new(World, entity); + } + public IEnumerable> GetTargets(string symbol) + => GetTargets(World.LookupSymbolOrThrow(symbol)); + public IEnumerable> GetTargets() + => GetTargets(World.Entity()); + + + public bool Equals(Entity other) + { +#if DEBUG + // In DEBUG mode, we additionally check if the worlds the two compared + // values are from the same world. This accounts for the world being a + // stage, hence why it might not be the cheapest operation. + if (World != other.World) throw new ArgumentException( + "The specified values are not from the same world"); +#endif + return Value == other.Value; + } + public override bool Equals(object? obj) + => (obj is Entity other) && Equals(other); + public override int GetHashCode() + => Value.GetHashCode(); public override string? ToString() - => IsSome ? $"Entity({Value.Data.Data})" - : "Entity.None"; + => Value.ToString(); + + public static bool operator ==(Entity left, Entity right) => left.Equals(right); + public static bool operator !=(Entity left, Entity right) => !left.Equals(right); + + public static implicit operator Entity (Entity entity) => entity.Value; + public static implicit operator ecs_entity_t(Entity entity) => entity.Value.Value; - public static bool operator ==(Entity left, Entity right) => left.Equals(right); - public static bool operator !=(Entity left, Entity right) => !left.Equals(right); + public static implicit operator Id(Entity entity) => Id.GetUnsafe(entity.World, entity); + public static implicit operator Id (Entity entity) => new(entity.Value.Value.Data); + public static implicit operator ecs_id_t (Entity entity) => entity.Value.Value.Data; - public static implicit operator ecs_entity_t(Entity e) => e.Value; - public static implicit operator Id(Entity e) => new(e.Value.Data); - public static implicit operator ecs_id_t(Entity e) => e.Value.Data; + public static implicit operator Term (Entity entity) => new(entity.Value); + public static implicit operator TermId(Entity entity) => new(entity.Value); } diff --git a/src/gaemstone.ECS/EntityBuilder.cs b/src/gaemstone.ECS/EntityBuilder.cs index 3361091..750c52a 100644 --- a/src/gaemstone.ECS/EntityBuilder.cs +++ b/src/gaemstone.ECS/EntityBuilder.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; -using gaemstone.Utility; +using gaemstone.ECS.Internal; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; +using static gaemstone.ECS.Internal.FlecsBuiltIn; namespace gaemstone.ECS; -public class EntityBuilder +public class EntityBuilder { - public World World { get; } + public World World { get; } /// Set to modify existing entity (optional). - public Entity Id { get; set; } + public Entity Id { get; set; } /// /// Path of the entity. If no entity is provided, an entity with this path @@ -26,7 +28,7 @@ public class EntityBuilder /// function name, where these identifiers differ from the name they are /// registered with in flecs. /// - public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; } + public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; } private string? _symbol = null; /// @@ -44,43 +46,56 @@ public class EntityBuilder public string? Expression { get; } /// Actions to run once the entity has been created. - private readonly List> _toSet = new(); + private readonly List> _toSet = new(); - public EntityBuilder(World world, EntityPath? path = null) + public EntityBuilder(World world, EntityPath? path = null) { World = world; Path = path; } - public EntityBuilder(World world, Entity parent, EntityPath? path = null) + public EntityBuilder(World world, Entity parent, EntityPath? path = null) : this(world, path) { // If given path is absolute, the new entity won't be created as a // child of the specified parent. Alternatively, EntityRef.NewChild // can be used, which will throw when an absolute path is given. - if ((path?.IsRelative != false) && parent.IsSome) Add(World.ChildOf, parent); + if ((path?.IsRelative != false) && parent.IsSome) Add(ChildOf, parent); } - public EntityBuilder Add(Id id) + public EntityBuilder Add(Id id) { // If adding a ChildOf relation, store the parent separately. - if (id.RelationUnsafe == World.ChildOf) + if (id.RelationUnsafe == ChildOf) { _parent = id.TargetUnsafe; return this; } if (_toAdd.Count == 31) throw new NotSupportedException( "Must not add more than 31 Ids at once with EntityBuilder"); _toAdd.Add(id); return this; } - public EntityBuilder Add(string symbol) - => Add(World.LookupBySymbolOrThrow(symbol)); - public EntityBuilder Add(Entity relation, Entity target) - => Add(ECS.Id.Pair(relation, target)); + public EntityBuilder Add(string symbol) + => Add(World.LookupSymbolOrThrow(symbol)); - public EntityBuilder Set(Id id, in T value) where T : unmanaged + public EntityBuilder Add() + => Add(World.Entity()); + + public EntityBuilder Add(Entity relation, Entity target) + => Add(World.Pair(relation, target)); + public EntityBuilder Add(Entity target) + => Add(World.Pair(target)); + public EntityBuilder Add() + => Add(World.Pair()); + + public EntityBuilder Set(Id id, in T value) where T : unmanaged // "in" can't be used with lambdas, so we make a local copy. - { var copy = value; _toSet.Add(e => e.Set(id, copy)); return this; } + { var copy = value; _toSet.Add(e => EntityAccess.Set(World, e, id, copy)); return this; } + public EntityBuilder Set(Id id, T value) where T : class + { _toSet.Add(e => EntityAccess.Set(World, e, id, value)); return this; } + - public EntityBuilder Set(Id id, T obj) where T : class - { _toSet.Add(e => e.Set(id, obj)); return this; } + public EntityBuilder Set(in T value) where T : unmanaged + => Set(World.Entity(), value); + public EntityBuilder Set(T value) where T : class + => Set(World.Entity(), value); - public unsafe EntityRef Build() + public unsafe Entity Build() { var parent = _parent; @@ -88,7 +103,7 @@ public class EntityBuilder if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException( "Entity already has parent set (via ChildOf), so path must not be absolute"); // If path specifies more than just a name, ensure the parent entity exists. - if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(World, parent, Path.Parent!); + if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(World, Path.Parent!, parent); } using var alloc = TempAllocator.Use(); @@ -102,11 +117,11 @@ public class EntityBuilder }; var add = desc.add; var index = 0; - if (parent.IsSome) add[index++] = ECS.Id.Pair(World.ChildOf, parent); + if (parent.IsSome) add[index++] = World.Pair(ChildOf, parent); foreach (var id in _toAdd) add[index++] = id; var entityId = ecs_entity_init(World, &desc); - var entity = new EntityRef(World, new(entityId)); + var entity = Entity.GetOrInvalid(World, new(entityId)); foreach (var action in _toSet) action(entity); return entity; diff --git a/src/gaemstone.ECS/EntityPath.cs b/src/gaemstone.ECS/EntityPath.cs index 1de24a3..9f68ecb 100644 --- a/src/gaemstone.ECS/EntityPath.cs +++ b/src/gaemstone.ECS/EntityPath.cs @@ -4,11 +4,13 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; -using gaemstone.Utility; +using gaemstone.ECS.Internal; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; +// TODO: Redo this with a single UTF8 byte array. public class EntityPath { private readonly byte[][] _parts; @@ -48,6 +50,26 @@ public class EntityPath return bytes; }).ToArray()) { } + public static unsafe EntityPath From(World world, Entity entity) + { + var parts = new List(32); + + do { + var name = ecs_get_name(world, entity).FlecsToBytes(); + if (name != null) parts.Add(name); + else { + // If name is not set, use the numeric Id, instead. + var id = entity.NumericId.ToString(); + var bytes = new byte[Encoding.UTF8.GetByteCount(id) + 1]; + Encoding.UTF8.GetBytes(id, bytes); + parts.Add(bytes); + } + } while ((entity = new(ecs_get_target(world, entity, EcsChildOf, 0))).IsSome); + + parts.Reverse(); + return new(true, parts.ToArray()); + } + public static bool TryParse(string str, [NotNullWhen(true)] out EntityPath? result) { result = null; @@ -106,6 +128,10 @@ public class EntityPath // throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character"); // } + public EntityPath ThrowIfAbsolute() + => IsRelative ? this : throw new InvalidOperationException( + $"Path '{this}' must not be absolute"); + public string[] GetParts() { var result = new string[Count]; @@ -135,7 +161,7 @@ public class EntityPath internal static unsafe Entity Lookup( - World world, Entity parent, EntityPath path, bool throwOnNotFound) + World world, EntityPath path, Entity parent, bool throwOnNotFound) { var start = path.IsAbsolute ? Entity.None // If path is absolute, ignore parent and use root. : parent.IsNone ? new(ecs_get_scope(world)) // If no parent is specified, use the current scope. @@ -145,14 +171,13 @@ public class EntityPath foreach (var part in path) fixed (byte* ptr = part.AsSpan()) { current = new(ecs_lookup_child(world, current, ptr)); - if (current.IsSome && ecs_is_alive(world, current)) continue; - if (!throwOnNotFound) return Entity.None; - - var startStr = EntityRef.CreateOrNull(world, start)?.GetFullPath().ToString() ?? start.ToString(); - throw new EntityNotFoundException( - start.IsNone ? $"Entity at '{path}' not found" - : (start == parent) ? $"Child entity of '{startStr}' at '{path}' not found" - : $"Entity at scope '{startStr}' at '{path}' not found"); + if (current.IsNone || !ecs_is_alive(world, current)) { + if (!throwOnNotFound) return Entity.None; + else throw new EntityNotFoundException( + start.IsNone ? $"Entity at '{path}' not found" + : (start == parent) ? $"Child entity of '{From(world, start)}' at '{path}' not found" + : $"Entity at scope '{From(world, start)}' at '{path}' not found"); + } } return current; @@ -160,7 +185,7 @@ public class EntityPath /// Used by . internal static unsafe Entity EnsureEntityExists( - World world, Entity parent, EntityPath path) + World world, EntityPath path, Entity parent) { // If no parent is specified and path is relative, use the current scope. if (parent.IsNone && path.IsRelative) parent = new(ecs_get_scope(world)); @@ -170,7 +195,7 @@ public class EntityPath fixed (byte* ptr = part.AsSpan()) if (skipLookup || (parent = new(ecs_lookup_child(world, parent, ptr))).IsNone) { var desc = new ecs_entity_desc_t { name = ptr, sep = CStringExtensions.ETX }; - if (parent.IsSome) desc.add[0] = Id.Pair(world.ChildOf, parent); + if (parent.IsSome) desc.add[0] = Id.Pair(FlecsBuiltIn.ChildOf, parent); parent = new(ecs_entity_init(world, &desc)); skipLookup = true; } @@ -179,30 +204,6 @@ public class EntityPath } } -public static class EntityPathExtensions -{ - public static unsafe EntityPath GetFullPath(this EntityRef entity) - { - var current = (Entity)entity; - var parts = new List(32); - - do { - var name = ecs_get_name(entity.World, current).FlecsToBytes(); - if (name != null) parts.Add(name); - else { - // If name is not set, use the numeric Id, instead. - var id = current.Id.ToString(); - var bytes = new byte[Encoding.UTF8.GetByteCount(id) + 1]; - Encoding.UTF8.GetBytes(id, bytes); - parts.Add(bytes); - } - } while ((current = new(ecs_get_target(entity.World, current, EcsChildOf, 0))).IsSome); - - parts.Reverse(); - return new(true, parts.ToArray()); - } -} - public readonly ref struct UTF8View { private readonly ReadOnlySpan _bytes; diff --git a/src/gaemstone.ECS/EntityRef.cs b/src/gaemstone.ECS/EntityRef.cs deleted file mode 100644 index 27e9421..0000000 --- a/src/gaemstone.ECS/EntityRef.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe class EntityRef - : IEquatable -{ - public World World { get; } - public Entity Entity { get; } - public uint Id => Entity.Id; - - public bool IsAlive => ecs_is_alive(World, this); - public EntityType Type => new(World, ecs_get_type(World, this)); - - public string? Name { - get => ecs_get_name(World, this).FlecsToString()!; - set { using var alloc = TempAllocator.Use(); - ecs_set_name(World, this, alloc.AllocateCString(value)); } - } - public string? Symbol { - get => ecs_get_symbol(World, this).FlecsToString()!; - set { using var alloc = TempAllocator.Use(); - ecs_set_symbol(World, this, alloc.AllocateCString(value)); } - } - - - private EntityRef(World world, Entity entity, bool throwOnInvalid) - { - if (throwOnInvalid && !ecs_is_valid(world, entity)) - throw new InvalidOperationException($"The entity {entity} is not valid"); - World = world; - Entity = entity; - } - - public EntityRef(World world, Entity entity) - : this(world, entity, true) { } - - public static EntityRef? CreateOrNull(World world, Entity entity) - => ecs_is_valid(world, entity) ? new(world, entity) : null; - - - public void Delete() - => ecs_delete(World, this); - - public EntityBuilder NewChild(EntityPath? path = null) - => World.New(this, EnsureRelativePath(path)); - public EntityRef? LookupChild(EntityPath path) - => World.LookupByPath(this, EnsureRelativePath(path)!); - public EntityRef LookupChildOrThrow(EntityPath path) - => World.LookupByPathOrThrow(this, EnsureRelativePath(path)!); - - private static EntityPath? EnsureRelativePath(EntityPath? path) - { - if (path?.IsAbsolute == true) throw new ArgumentException( - $"Path '{path}' must not be absolute", nameof(path)); - return path; - } - - - public EntityRef? Parent - => GetTargets(World.ChildOf).FirstOrDefault(); - - // TODO: Change to property after all? - public IEnumerable GetChildren() - { - foreach (var iter in Iterator.FromTerm(World, new(World.ChildOf, this))) - for (var i = 0; i < iter.Count; i++) - yield return iter.Entity(i); - } - - public EntityRef ChildOf(Entity parent) - => Add(World.ChildOf, parent); - - - public bool IsDisabled => Has(World.Disabled); - public EntityRef Disable() => Add(World.Disabled); - public EntityRef Enable() => Remove(World.Disabled); - - - public EntityRef Add(Id id) { ecs_add_id(World, this, id); return this; } - public EntityRef Add(string symbol) => Add(World.LookupBySymbolOrThrow(symbol)); - public EntityRef Add(Entity relation, Entity target) => Add(ECS.Id.Pair(relation, target)); - - public EntityRef Remove(Id id) { ecs_remove_id(World, this, id); return this; } - public EntityRef Remove(string symbol) => Remove(World.LookupBySymbolOrThrow(symbol)); - public EntityRef Remove(Entity relation, Entity target) => Remove(ECS.Id.Pair(relation, target)); - - public bool Has(Id id) => ecs_has_id(World, this, id); - public bool Has(string symbol) => Has(World.LookupBySymbolOrThrow(symbol)); - public bool Has(Entity relation, Entity target) => Has(ECS.Id.Pair(relation, target)); - - - public T? GetOrNull(Id id) - where T : unmanaged - { - var ptr = ecs_get_id(World, this, id); - return (ptr != null) ? Unsafe.Read(ptr) : null; - } - - public T? GetOrNull(Id id, T _ = null!) - where T : class - { - var ptr = ecs_get_id(World, this, id); - return (T?)Unsafe.Read(ptr).Target; - } - - public T GetOrThrow(Id id) - { - var ptr = ecs_get_id(World, this, id); - if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}"); - if (typeof(T).IsValueType) return Unsafe.Read(ptr); - else return (T)Unsafe.Read(ptr).Target!; - } - - public ref T GetMut(Id id) - where T : unmanaged - { - var ptr = ecs_get_mut_id(World, this, id); - // NOTE: Value is added if it doesn't exist on the entity. - return ref Unsafe.AsRef(ptr); - } - - public ref T GetRefOrNull(Id id) - where T : unmanaged - { - var @ref = ecs_ref_init_id(World, this, id); - var ptr = ecs_ref_get_id(World, &@ref, id); - return ref (ptr != null) ? ref Unsafe.AsRef(ptr) - : ref Unsafe.NullRef(); - } - - public ref T GetRefOrThrow(Id id) - where T : unmanaged - { - ref var ptr = ref GetRefOrNull(id); - if (Unsafe.IsNullRef(ref ptr)) throw new Exception( - $"Component {typeof(T)} not found on {this}"); - return ref ptr; - } - - - public void Modified(Id id) - => ecs_modified_id(World, this, id); - - public EntityRef Set(Id id, in T value) - where T : unmanaged - { - var size = (ulong)Unsafe.SizeOf(); - fixed (T* ptr = &value) - if (ecs_set_id(World, this, id, size, ptr).Data == 0) - throw new InvalidOperationException(); - return this; - } - - public EntityRef Set(Id id, T obj) - where T : class - { - if (obj == null) throw new ArgumentNullException(nameof(obj)); - var size = (ulong)sizeof(ReferenceHandle); - // Dispose this handle afterwards, since Flecs clones it. - using var handle = ReferenceHandle.Alloc(obj); - if (ecs_set_id(World, this, id, size, &handle).Data == 0) - throw new InvalidOperationException(); - return this; - } - - - private EntityRef? GetTarget(Entity relation, int index) - => CreateOrNull(World, new(ecs_get_target(World, this, relation, index))); - public IEnumerable GetTargets(Entity relation) - { var index = 0; while (GetTarget(relation, index++) is EntityRef target) yield return target; } - public IEnumerable GetTargets(string symbol) - => GetTargets(World.LookupBySymbolOrThrow(symbol)); - - public bool Equals(EntityRef? other) => (other is not null) && (World == other.World) && (Entity == other.Entity); - public override bool Equals(object? obj) => Equals(obj as EntityRef); - public override int GetHashCode() => HashCode.Combine(World, Entity); - public override string? ToString() => ecs_entity_str(World, this).FlecsToStringAndFree()!; - - public static bool operator ==(EntityRef? left, EntityRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false); - public static bool operator !=(EntityRef? left, EntityRef? right) => !(left == right); - - public static implicit operator Entity(EntityRef? e) => e?.Entity ?? default; - public static implicit operator ecs_entity_t(EntityRef? e) => e?.Entity.Value ?? default; - - public static implicit operator Id(EntityRef? e) => new(e?.Entity.Value.Data ?? default); - public static implicit operator ecs_id_t(EntityRef? e) => e?.Entity.Value.Data ?? default; -} diff --git a/src/gaemstone.ECS/EntityType.cs b/src/gaemstone.ECS/EntityType.cs index fb34df3..82d504d 100644 --- a/src/gaemstone.ECS/EntityType.cs +++ b/src/gaemstone.ECS/EntityType.cs @@ -1,17 +1,17 @@ using System.Collections; using System.Collections.Generic; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe readonly struct EntityType - : IReadOnlyList +public unsafe readonly struct EntityType + : IReadOnlyList> { - public World World { get; } - public ecs_type_t* Handle { get; } + public readonly World World; + public readonly ecs_type_t* Handle; - public EntityType(World world, ecs_type_t* handle) + public EntityType(World world, ecs_type_t* handle) { World = world; Handle = handle; } public override string ToString() @@ -19,7 +19,9 @@ public unsafe readonly struct EntityType // IReadOnlyList implementation public int Count => Handle->count; - public IdRef this[int index] => new(World, new(Handle->array[index])); - public IEnumerator GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; } + public Id this[int index] + => Id.GetUnsafe(World, new(Handle->array[index])); + public IEnumerator> GetEnumerator() + { for (var i = 0; i < Count; i++) yield return this[i]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/gaemstone.ECS/Exceptions.cs b/src/gaemstone.ECS/Exceptions.cs new file mode 100644 index 0000000..8782784 --- /dev/null +++ b/src/gaemstone.ECS/Exceptions.cs @@ -0,0 +1,31 @@ +using System; + +namespace gaemstone.ECS; + +public class EntityNotFoundException + : Exception +{ + public EntityNotFoundException(string message) + : base(message) { } +} + +public class ComponentNotFoundException + : Exception +{ + public ComponentNotFoundException(string message) + : base(message) { } +} + +public class FlecsException + : Exception +{ + public FlecsException() : base() { } + public FlecsException(string message) : base(message) { } +} + +public class FlecsAbortException + : FlecsException +{ + internal FlecsAbortException() + : base("Abort was called by flecs") { } +} diff --git a/src/gaemstone.ECS/Filter.cs b/src/gaemstone.ECS/Filter.cs index 9437446..7e5f08a 100644 --- a/src/gaemstone.ECS/Filter.cs +++ b/src/gaemstone.ECS/Filter.cs @@ -1,26 +1,26 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Filter +public unsafe class Filter : IDisposable { - public World World { get; } + public World World { get; } public ecs_filter_t* Handle { get; } - public Iterator.Variable? ThisVar { get { + public Variable? ThisVar { get { var index = ecs_filter_find_this_var(this); return (index >= 0) ? new(index, "This") : null; } } - internal Filter(World world, ecs_filter_t* handle) + internal Filter(World world, ecs_filter_t* handle) { World = world; Handle = handle; } - public Filter(World world, FilterDesc desc) + public Filter(World world, FilterDesc desc) { using var alloc = TempAllocator.Use(); var flecsDesc = desc.ToFlecs(alloc); @@ -31,13 +31,22 @@ public unsafe class Filter public void Dispose() => ecs_filter_fini(this); - public Iterator Iter() - => new(World, IteratorType.Filter, ecs_filter_iter(World, this)); + public FilterIterator Iter() + => new(ecs_filter_iter(World, this)); public override string ToString() => ecs_filter_str(World, this).FlecsToStringAndFree()!; - public static implicit operator ecs_filter_t*(Filter q) => q.Handle; + public static implicit operator ecs_filter_t*(Filter filter) => filter.Handle; +} + +public unsafe class FilterIterator + : Iterator +{ + internal FilterIterator(ecs_iter_t value) + : base(value) { } + public override bool Next() + => ecs_filter_next(Handle); } public class FilterDesc diff --git a/src/gaemstone.ECS/Id+Bare.cs b/src/gaemstone.ECS/Id+Bare.cs new file mode 100644 index 0000000..938f591 --- /dev/null +++ b/src/gaemstone.ECS/Id+Bare.cs @@ -0,0 +1,36 @@ +using System; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public readonly struct Id + : IEquatable +{ + public readonly ecs_id_t Value; + public IdFlags Flags => (IdFlags)(Value & ECS_ID_FLAGS_MASK); + + public bool IsPair => ecs_id_is_pair(this); + public bool IsWildcard => ecs_id_is_wildcard(this); + + public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 }); + public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK }); + + public Id(ecs_id_t value) => Value = value; + + public static Id Pair(Entity relation, Entity target) + => new(ecs_make_pair(relation, target)); + + public bool Equals(Id other) => Value.Data == other.Value.Data; + public override bool Equals(object? obj) => (obj is Id other) && Equals(other); + public override int GetHashCode() => Value.Data.GetHashCode(); + public override string? ToString() + => (Flags != default) ? $"Id({Value.Data}, Flags={Flags})" + : $"Id({Value.Data})"; + + public static bool operator ==(Id left, Id right) => left.Equals(right); + public static bool operator !=(Id left, Id right) => !left.Equals(right); + + public static implicit operator ecs_id_t(Id id) => id.Value; + + public static implicit operator Term(Id id) => new(id); +} diff --git a/src/gaemstone.ECS/Id.cs b/src/gaemstone.ECS/Id.cs index a5bce10..34c9261 100644 --- a/src/gaemstone.ECS/Id.cs +++ b/src/gaemstone.ECS/Id.cs @@ -1,41 +1,66 @@ using System; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public readonly struct Id - : IEquatable +public unsafe struct Id + : IEquatable> { - public readonly ecs_id_t Value; - public IdFlags Flags => (IdFlags)(Value & ECS_ID_FLAGS_MASK); + public readonly World World; + public readonly Id Value; - public bool IsPair => ecs_id_is_pair(this); - public bool IsWildcard => ecs_id_is_wildcard(this); + public IdFlags Flags => Value.Flags; + public bool IsPair => Value.IsPair; + public bool IsWildcard => Value.IsWildcard; - public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 }); - public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK }); + public bool IsValid => ecs_id_is_valid(World, this); + public bool IsTag => ecs_id_is_tag(World, this).Data != default; + public bool IsInUse => ecs_id_in_use(World, this); + public int Count => ecs_count_id(World, this); - public Id(ecs_id_t value) => Value = value; + private Id(World world, Id id) + { World = world; Value = id; } - public static Id Combine(IdFlags flags, Id id) - => new((ulong)flags | id.Value); + public static Id GetUnsafe(World world, Id value) + => new(world, value); + public static Id? GetOrNull(World world, Id value) + => ecs_id_is_valid(world, value) ? new(world, value) : null; + public static Id GetOrThrow(World world, Id value) + => ecs_id_is_valid(world, value) ? new(world, value) : throw new InvalidOperationException($"The id {value} is not valid"); - public static Id Pair(Entity relation, Entity target) - => Combine(IdFlags.Pair, new( - ((relation.Value.Data << 32) & ECS_COMPONENT_MASK) | - ( target.Value.Data & ECS_ENTITY_MASK ))); + public Entity? AsEntity() + => !IsPair ? World.LookupAliveOrNull(new Entity(new() { Data = Value })) : null; + public (Entity Relation, Entity Target)? AsPair() + => IsPair && (World.LookupAliveOrNull(Value.RelationUnsafe) is Entity relation) && + (World.LookupAliveOrNull(Value.TargetUnsafe ) is Entity target ) + ? (relation, target) : null; - public bool Equals(Id other) => Value.Data == other.Value.Data; - public override bool Equals(object? obj) => (obj is Id other) && Equals(other); - public override int GetHashCode() => Value.Data.GetHashCode(); + public bool Equals(Id other) + { +#if DEBUG + // In DEBUG mode, we additionally check if the worlds the two compared + // values are from the same world. This accounts for the world being a + // stage, hence why it might not be the cheapest operation. + if (World != other.World) throw new ArgumentException( + "The specified values are not from the same world"); +#endif + return Value == other.Value; + } + public override bool Equals(object? obj) + => (obj is Id other) && Equals(other); + public override int GetHashCode() + => Value.GetHashCode(); public override string? ToString() - => (Flags != default) ? $"Id({Value.Data}, Flags={Flags})" - : $"Id({Value.Data})"; + => ecs_id_str(World, this).FlecsToStringAndFree()!; - public static bool operator ==(Id left, Id right) => left.Equals(right); - public static bool operator !=(Id left, Id right) => !left.Equals(right); + public static bool operator ==(Id left, Id right) => left.Equals(right); + public static bool operator !=(Id left, Id right) => !left.Equals(right); - public static implicit operator ecs_id_t(Id i) => i.Value; + public static implicit operator Id (Id id) => id.Value; + public static implicit operator ecs_id_t(Id id) => id.Value.Value; + + public static implicit operator Term(Id id) => new(id.Value); } [Flags] diff --git a/src/gaemstone.ECS/IdRef.cs b/src/gaemstone.ECS/IdRef.cs deleted file mode 100644 index 1587734..0000000 --- a/src/gaemstone.ECS/IdRef.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe class IdRef - : IEquatable -{ - public World World { get; } - public Id Id { get; } - public IdFlags Flags => Id.Flags; - - public bool IsPair => Id.IsPair; - public bool IsWildcard => Id.IsWildcard; - - public bool IsValid => ecs_id_is_valid(World, this); - public bool IsInUse => ecs_id_in_use(World, this); - public int Count => ecs_count_id(World, this); - - public IdRef(World world, Id id) - { World = world; Id = id; } - - public static IdRef Combine(IdFlags flags, IdRef id) - => new(id.World, Id.Combine(flags, id)); - - public static IdRef Pair(World world, Entity relation, Entity target) - => new(world, Id.Pair(relation, target)); - public static IdRef Pair(EntityRef relation, Entity target) - => Pair(relation.World, relation, target); - public static IdRef Pair(Entity relation, EntityRef target) - => Pair(target.World, relation, target); - public static IdRef Pair(EntityRef relation, EntityRef target) - => Pair(relation.World, relation, target); - - public EntityRef? AsEntity() - => (Flags == default) ? World.LookupAlive(new Entity(new() { Data = Id })) : null; - public (EntityRef Relation, EntityRef Target)? AsPair() - => IsPair && (World.LookupAlive(Id.RelationUnsafe) is EntityRef relation) && - (World.LookupAlive(Id.TargetUnsafe ) is EntityRef target ) - ? (relation, target) : null; - - public bool Equals(IdRef? other) => (other is not null) && (World == other.World) && (Id == other.Id); - public override bool Equals(object? obj) => Equals(obj as IdRef); - public override int GetHashCode() => HashCode.Combine(World, Id); - public override string? ToString() => ecs_id_str(World, this).FlecsToStringAndFree()!; - - public static bool operator ==(IdRef? left, IdRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false); - public static bool operator !=(IdRef? left, IdRef? right) => !(left == right); - - public static implicit operator Id(IdRef i) => i.Id; - public static implicit operator ecs_id_t(IdRef i) => i.Id.Value; -} diff --git a/src/gaemstone.ECS/Internal/EntityAccess.cs b/src/gaemstone.ECS/Internal/EntityAccess.cs new file mode 100644 index 0000000..963e86f --- /dev/null +++ b/src/gaemstone.ECS/Internal/EntityAccess.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using gaemstone.ECS.Utility; +using static flecs_hub.flecs; + +namespace gaemstone.ECS.Internal; + +public unsafe static class EntityAccess +{ + public static bool IsValid(World world, Entity entity) + => ecs_is_valid(world, entity); + public static bool IsAlive(World world, Entity entity) + => ecs_is_alive(world, entity); + + public static string? GetName(World world, Entity entity) + => ecs_get_name(world, entity).FlecsToString()!; + public static void SetName(World world, Entity entity, string? value) + { using var alloc = TempAllocator.Use(); ecs_set_name(world, entity, alloc.AllocateCString(value)); } + + public static string? GetSymbol(World world, Entity entity) + => ecs_get_symbol(world, entity).FlecsToString()!; + public static void SetSymbol(World world, Entity entity, string? value) + { using var alloc = TempAllocator.Use(); ecs_set_symbol(world, entity, alloc.AllocateCString(value)); } + + + public static void Add(World world, Entity entity, Id id) + => ecs_add_id(world, entity, id); + public static void Remove(World world, Entity entity, Id id) + => ecs_remove_id(world, entity, id); + public static bool Has(World world, Entity entity, Id id) + => ecs_has_id(world, entity, id); + + + public static IEnumerable GetTargets(World world, Entity entity, Entity relation) + { + Entity GetTarget(int index) + => new(ecs_get_target(world, entity, relation, index)); + for (var i = 0; GetTarget(i) is { IsSome: true } target; i++) + yield return target; + } + + + public static T? GetOrNull(World world, Entity entity, Id id) + where T : unmanaged + { + var ptr = ecs_get_id(world, entity, id); + return (ptr != null) ? Unsafe.Read(ptr) : null; + } + + public static T? GetOrNull(World world, Entity entity, Id id, T _ = null!) + where T : class + { + var ptr = ecs_get_id(world, entity, id); + return (T?)Unsafe.Read(ptr).Target; + } + + public static T GetOrThrow(World world, Entity entity, Id id) + { + var ptr = ecs_get_id(world, entity, id); + if (ptr == null) throw new ComponentNotFoundException($"Component {id} not found on {entity}"); + if (typeof(T).IsValueType) return Unsafe.Read(ptr); + else return (T)Unsafe.Read(ptr).Target!; + } + + public static ref T GetMut(World world, Entity entity, Id id) + where T : unmanaged + { + var ptr = ecs_get_mut_id(world, entity, id); + // NOTE: Value is added if it doesn't exist on the entity. + return ref Unsafe.AsRef(ptr); + } + + public static ref T GetRefOrNull(World world, Entity entity, Id id) + where T : unmanaged + { + var @ref = ecs_ref_init_id(world, entity, id); + var ptr = ecs_ref_get_id(world, &@ref, id); + return ref (ptr != null) ? ref Unsafe.AsRef(ptr) + : ref Unsafe.NullRef(); + } + + public static ref T GetRefOrThrow(World world, Entity entity, Id id) + where T : unmanaged + { + ref var ptr = ref GetRefOrNull(world, entity, id); + if (Unsafe.IsNullRef(ref ptr)) throw new Exception( + $"Component {typeof(T)} not found on {entity}"); + return ref ptr; + } + + + public static void Modified(World world, Entity entity, Id id) + => ecs_modified_id(world, entity, id); + + public static Entity Set(World world, Entity entity, Id id, in T value) + where T : unmanaged + { + var size = (ulong)Unsafe.SizeOf(); + fixed (T* ptr = &value) + if (ecs_set_id(world, entity, id, size, ptr).Data == 0) + throw new InvalidOperationException(); + return entity; + } + + public static Entity Set(World world, Entity entity, Id id, T value) + where T : class + { + if (value == null) throw new ArgumentNullException(nameof(value)); + var size = (ulong)sizeof(ReferenceHandle); + // Dispose entity handle afterwards, since Flecs clones it. + using var handle = ReferenceHandle.Alloc(value); + if (ecs_set_id(world, entity, id, size, &handle).Data == 0) + throw new InvalidOperationException(); + return entity; + } +} diff --git a/src/gaemstone.ECS/Internal/FlecsBuiltIn.cs b/src/gaemstone.ECS/Internal/FlecsBuiltIn.cs new file mode 100644 index 0000000..59c623e --- /dev/null +++ b/src/gaemstone.ECS/Internal/FlecsBuiltIn.cs @@ -0,0 +1,11 @@ +using static flecs_hub.flecs; + +namespace gaemstone.ECS.Internal; + +public static class FlecsBuiltIn +{ + public static Entity ChildOf { get; } = new(pinvoke_EcsChildOf()); + + // FIXME: Hopefully flecs-cs will expose this one day. + public static Entity Disabled { get; } = new(new() { Data = ECS_HI_COMPONENT_ID + 7 }); +} diff --git a/src/gaemstone.ECS/Internal/Iterator.cs b/src/gaemstone.ECS/Internal/Iterator.cs new file mode 100644 index 0000000..d68e29c --- /dev/null +++ b/src/gaemstone.ECS/Internal/Iterator.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using gaemstone.ECS.Utility; +using static flecs_hub.flecs; + +namespace gaemstone.ECS.Internal; + +public unsafe class Iterator + : IDisposable +{ + public ecs_iter_t* Handle; + private readonly bool _handleIsOwned; + + public World World => new(Handle->world); + public int Count => Handle->count; + + public IteratorFlags Flags => (IteratorFlags)(uint)Handle->flags; + public bool IsValid => (Flags & IteratorFlags.IsValid) != 0; + + public TimeSpan DeltaTime => TimeSpan.FromSeconds(Handle->delta_time); + public TimeSpan DeltaSystemTime => TimeSpan.FromSeconds(Handle->delta_system_time); + + public Iterator(ecs_iter_t* handle) + { + Handle = handle; + _handleIsOwned = false; + } + public Iterator(ecs_iter_t value) + { + Handle = GlobalHeapAllocator.Instance.AllocateCopy(value); + _handleIsOwned = true; + } + + public void Dispose() + { + if (Handle == null) return; + + // When an iterator is iterated until completion, + // ecs_iter_fini will be called automatically. + if (IsValid) ecs_iter_fini(Handle); + + if (_handleIsOwned) + GlobalHeapAllocator.Instance.Free(Handle); + Handle = null; + } + + + public Entity GetVar(Variable var) + => new(ecs_iter_get_var(Handle, var.Index)); + public Iterator SetVar(Variable var, Entity entity) + { ecs_iter_set_var(Handle, var.Index, entity); return this; } + + + public virtual bool Next() + => ecs_iter_next(Handle); + + public Entity First() + => new(ecs_iter_first(Handle)); + + public IEnumerable GetAllEntities() + { + while (Next()) + for (var i = 0; i < Count; i++) + yield return Entity(i); + } + + + public bool FieldIsSet(int index) + => ecs_field_is_set(Handle, index); + public Id FieldId(int index) + => new(ecs_field_id(Handle, index)); + public bool FieldIs(int index, Id id) + => ecs_field_id(Handle, index) == id.Value; + + + public Entity Entity(int index) + => new(Handle->entities[index]); + + public Span Field(int index) + where T : unmanaged + { + var size = (ulong)Unsafe.SizeOf(); + var isSelf = ecs_field_is_self(Handle, index); + var pointer = ecs_field_w_size(Handle, size, index); + return new(pointer, isSelf ? Count : 1); + } + public Span FieldOrEmpty(int index) where T : unmanaged + => FieldIsSet(index) ? Field(index) : Span.Empty; + + public SpanToRef Field(int index, T _ = null!) where T : class + => new(Field(index)); + public SpanToRef FieldOrEmpty(int index, T _ = null!) where T : class + => FieldIsSet(index) ? Field(index) : SpanToRef.Empty; + + + public override string ToString() + => ecs_iter_str(Handle).FlecsToStringAndFree()!; +} + +public readonly ref struct SpanToRef + where T : class +{ + public static SpanToRef Empty => default; + private readonly Span _span; + public int Length => _span.Length; + public T this[int index] => (T)_span[index].Target!; + public T? GetOrNull(int index) => ((index >= 0) && (index < Length)) ? this[index] : null; + internal SpanToRef(Span span) => _span = span; +} diff --git a/src/gaemstone.ECS/Internal/Lookup.cs b/src/gaemstone.ECS/Internal/Lookup.cs new file mode 100644 index 0000000..52b2e56 --- /dev/null +++ b/src/gaemstone.ECS/Internal/Lookup.cs @@ -0,0 +1,9 @@ +namespace gaemstone.ECS.Internal; + +internal static class Lookup +{ + internal static class Entity + { + public static Entity Value; + } +} diff --git a/src/gaemstone.ECS/Iterator.cs b/src/gaemstone.ECS/Iterator.cs index fdf69fb..b6e95ed 100644 --- a/src/gaemstone.ECS/Iterator.cs +++ b/src/gaemstone.ECS/Iterator.cs @@ -1,154 +1,86 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Runtime.CompilerServices; -using gaemstone.Utility; +using System.Linq; +using gaemstone.ECS.Internal; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Iterator - : IEnumerable - , IDisposable +public unsafe class Iterator : Iterator + , IEnumerable> { - public World World { get; } - public IteratorType? Type { get; } - public ecs_iter_t Value; + public new World World => new(base.World); - public bool Completed { get; private set; } - public int Count => Value.count; - public TimeSpan DeltaTime => TimeSpan.FromSeconds(Value.delta_time); - public TimeSpan DeltaSystemTime => TimeSpan.FromSeconds(Value.delta_system_time); + public Iterator(ecs_iter_t* handle) : base(handle) { } + public Iterator(ecs_iter_t value) : base(value) { } - public Iterator(World world, IteratorType? type, ecs_iter_t value) - { World = world; Type = type; Value = value; } - - public static Iterator FromTerm(World world, Term term) + public static TermIterator FromTerm(World world, Term term) { using var alloc = TempAllocator.Use(); var flecsTerm = term.ToFlecs(alloc); var flecsIter = ecs_term_iter(world, &flecsTerm); - return new(world, IteratorType.Term, flecsIter); - } - - public void Dispose() - { - // When an iterator is iterated until completion, - // ecs_iter_fini will be called automatically. - if (!Completed) - fixed (ecs_iter_t* ptr = &Value) - ecs_iter_fini(ptr); - } - - - public EntityRef GetVar(Variable var) - { - fixed (ecs_iter_t* ptr = &Value) - return new(World, new(ecs_iter_get_var(ptr, var.Index))); - } - - public Iterator SetVar(Variable var, Entity entity) - { - fixed (ecs_iter_t* ptr = &Value) - ecs_iter_set_var(ptr, var.Index, entity); - return this; - } - - - public bool Next() - { - fixed (ecs_iter_t* ptr = &Value) { - var result = Type switch { - IteratorType.Term => ecs_term_next(ptr), - IteratorType.Filter => ecs_filter_next(ptr), - IteratorType.Query => ecs_query_next(ptr), - IteratorType.Rule => ecs_rule_next(ptr), - _ => ecs_iter_next(ptr), - }; - Completed = !result; - return result; - } - } - - - public bool FieldIsSet(int index) - { - fixed (ecs_iter_t* ptr = &Value) - return ecs_field_is_set(ptr, index); - } - - public bool FieldIs(int index, Id id) - { - fixed (ecs_iter_t* ptr = &Value) - return ecs_field_id(ptr, index) == id.Value; - } - - public IdRef FieldId(int index) - { - fixed (ecs_iter_t* ptr = &Value) - return new(World, new(ecs_field_id(ptr, index))); + return new(flecsIter); } + public new Entity? GetVar(Variable var) + => Entity.GetOrNull(World, base.GetVar(var)); + public new Iterator SetVar(Variable var, Entity entity) + => (Iterator)base.SetVar(var, entity); - public EntityRef Entity(int index) - => new(World, new(Value.entities[index])); + public new IEnumerable> GetAllEntities() + => base.GetAllEntities().Select(e => Entity.GetOrInvalid(World, e)); - public Span Field(int index) - where T : unmanaged - { - fixed (ecs_iter_t* ptr = &Value) { - var size = (ulong)Unsafe.SizeOf(); - var isSelf = ecs_field_is_self(ptr, index); - var pointer = ecs_field_w_size(ptr, size, index); - return new(pointer, isSelf ? Count : 1); - } - } + public new Id FieldId(int index) + => Id.GetUnsafe(World, base.FieldId(index)); - public SpanToRef Field(int index, T _ = null!) where T : class - => new(Field(index)); - - public Span FieldOrEmpty(int index) where T : unmanaged - => FieldIsSet(index) ? Field(index) : Span.Empty; - - public SpanToRef FieldOrEmpty(int index, T _ = null!) where T : class - => FieldIsSet(index) ? Field(index, _) : SpanToRef.Empty; - - - public override string ToString() - { - fixed (ecs_iter_t* ptr = &Value) - return ecs_iter_str(ptr).FlecsToStringAndFree()!; - } + public new Entity Entity(int index) + => Entity.GetOrInvalid(World, base.Entity(index)); // IEnumerable implementation - public IEnumerator GetEnumerator() { while (Next()) yield return this; } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - + public virtual IEnumerator> GetEnumerator() + { while (Next()) yield return this; } + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} - public class Variable - { - public int Index { get; } - public string Name { get; } - public Variable(int index, string name) - { Index = index; Name = name; } - } +[Flags] +public enum IteratorFlags : uint +{ + /// Does iterator contain valid result. + IsValid = EcsIterIsValid, + /// Is iterator filter (metadata only). + IsFilter = EcsIterIsFilter, + /// Is iterator instanced. + IsInstanced = EcsIterIsInstanced, + /// Does result have shared terms. + HasShared = EcsIterHasShared, + /// Result only populates table. + TableOnly = EcsIterTableOnly, + /// Treat terms with entity subject as optional. + EntityOptional = EcsIterEntityOptional, + /// Iterator has no results. + NoResults = EcsIterNoResults, + /// Only evaluate non-this terms. + IgnoreThis = EcsIterIgnoreThis, + + MatchVar = EcsIterMatchVar, +} - public readonly ref struct SpanToRef - where T : class - { - public static SpanToRef Empty => default; - private readonly Span _span; - public int Length => _span.Length; - public T this[int index] => (T)_span[index].Target!; - public T? GetOrNull(int index) => ((index >= 0) && (index < Length)) ? this[index] : null; - internal SpanToRef(Span span) => _span = span; - } +public class Variable +{ + public int Index { get; } + public string Name { get; } + public Variable(int index, string name) + { Index = index; Name = name; } } -public enum IteratorType +public unsafe class TermIterator + : Iterator { - Term, - Filter, - Query, - Rule, + internal TermIterator(ecs_iter_t value) + : base(value) { } + public override bool Next() + => ecs_term_next(Handle); } diff --git a/src/gaemstone.ECS/Observer.cs b/src/gaemstone.ECS/Observer.cs index b3945a0..833386e 100644 --- a/src/gaemstone.ECS/Observer.cs +++ b/src/gaemstone.ECS/Observer.cs @@ -1,14 +1,15 @@ using System; using System.Runtime.InteropServices; -using gaemstone.Utility; +using gaemstone.ECS.Internal; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; public static class ObserverExtensions { - public static unsafe EntityRef InitObserver(this EntityRef entity, - FilterDesc filter, Action callback, params Entity[] events) + public static unsafe Entity InitObserver(this Entity entity, + FilterDesc filter, Action> callback, params Entity[] events) { if (events.Length == 0) throw new ArgumentException("Must specify at least 1 event", nameof(events)); if (events.Length > 8) throw new ArgumentException("Must specify at most 8 events", nameof(events)); @@ -25,7 +26,9 @@ public static class ObserverExtensions var span = desc.events; for (var i = 0; i < events.Length; i++) span[i] = events[i]; - return new(world, new(ecs_observer_init(world, &desc))); + if (ecs_observer_init(world, &desc).Data == default) + throw new InvalidOperationException(); + return entity; } [UnmanagedCallersOnly] @@ -33,7 +36,7 @@ public static class ObserverExtensions { var (world, callback) = CallbackContextHelper .Get<(World, Action)>((nint)iter->binding_ctx); - callback(new Iterator(world, null, *iter)); + callback(new Iterator(iter)); } [UnmanagedCallersOnly] diff --git a/src/gaemstone.ECS/Query.cs b/src/gaemstone.ECS/Query.cs index 0f9ff93..a4e1108 100644 --- a/src/gaemstone.ECS/Query.cs +++ b/src/gaemstone.ECS/Query.cs @@ -1,19 +1,19 @@ using System; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Query +public unsafe class Query : IDisposable { - public World World { get; } + public World World { get; } public ecs_query_t* Handle { get; } - public Filter Filter { get; } + public Filter Filter { get; } - public Iterator.Variable? ThisVar => Filter.ThisVar; + public Variable? ThisVar => Filter.ThisVar; - public Query(World world, QueryDesc desc) + public Query(World world, QueryDesc desc) { using var alloc = TempAllocator.Use(); var flecsDesc = desc.ToFlecs(alloc); @@ -25,13 +25,22 @@ public unsafe class Query public void Dispose() => ecs_query_fini(this); - public Iterator Iter() - => new(World, IteratorType.Query, ecs_query_iter(World, this)); + public QueryIterator Iter() + => new(ecs_query_iter(World, this)); public override string ToString() => ecs_query_str(Handle).FlecsToStringAndFree()!; - public static implicit operator ecs_query_t*(Query q) => q.Handle; + public static implicit operator ecs_query_t*(Query query) => query.Handle; +} + +public unsafe class QueryIterator + : Iterator +{ + internal QueryIterator(ecs_iter_t value) + : base(value) { } + public override bool Next() + => ecs_query_next(Handle); } public class QueryDesc : FilterDesc @@ -39,10 +48,10 @@ public class QueryDesc : FilterDesc public QueryDesc(params Term[] terms) : base(terms) { } public QueryDesc(string expression) : base(expression) { } - public new unsafe ecs_query_desc_t ToFlecs(IAllocator allocator) + public new unsafe ecs_query_desc_t ToFlecs(IAllocator alloc) { var desc = new ecs_query_desc_t { - filter = base.ToFlecs(allocator), + filter = base.ToFlecs(alloc), // TODO: Implement more Query features. }; return desc; diff --git a/src/gaemstone.ECS/Rule.cs b/src/gaemstone.ECS/Rule.cs index 142905d..1234915 100644 --- a/src/gaemstone.ECS/Rule.cs +++ b/src/gaemstone.ECS/Rule.cs @@ -1,23 +1,23 @@ using System; using System.Collections; using System.Collections.Generic; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Rule +public unsafe class Rule : IDisposable { - public World World { get; } + public World World { get; } public ecs_rule_t* Handle { get; } - public Filter Filter { get; } + public Filter Filter { get; } private VariableCollection? _variables; public VariableCollection Variables => _variables ??= new(this); - public Iterator.Variable? ThisVar => Filter.ThisVar; + public Variable? ThisVar => Filter.ThisVar; - public Rule(World world, FilterDesc desc) + public Rule(World world, FilterDesc desc) { using var alloc = TempAllocator.Use(); var flecsDesc = desc.ToFlecs(alloc); @@ -29,48 +29,53 @@ public unsafe class Rule public void Dispose() => ecs_rule_fini(this); - public Iterator Iter() - => new(World, IteratorType.Rule, ecs_rule_iter(World, this)); + public RuleIterator Iter() + => new(ecs_rule_iter(World, this)); public override string ToString() => ecs_rule_str(Handle).FlecsToStringAndFree()!; - public static implicit operator ecs_rule_t*(Rule q) => q.Handle; + public static implicit operator ecs_rule_t*(Rule rule) => rule.Handle; public unsafe class VariableCollection - : IReadOnlyCollection + : IReadOnlyCollection { - private readonly List _variables = new(); - - public Rule Rule { get; } + private readonly List _variables = new(); public int Count => _variables.Count; - public Iterator.Variable? this[int index] + public Variable? this[int index] => _variables.Find(v => v.Index == index); - public Iterator.Variable? this[string name] + public Variable? this[string name] => _variables.Find(v => string.Equals( v.Name, name, StringComparison.OrdinalIgnoreCase)); - internal VariableCollection(Rule rule) + internal VariableCollection(ecs_rule_t* handle) { - Rule = rule; - // Find the $This variable, if the rule has one. - var thisIndex = ecs_filter_find_this_var(ecs_rule_get_filter(Rule)); + var thisIndex = ecs_filter_find_this_var(ecs_rule_get_filter(handle)); if (thisIndex >= 0) _variables.Add(new(thisIndex, "This")); // Find all the other "accessible" variables. - var count = ecs_rule_var_count(Rule); + var count = ecs_rule_var_count(handle); for (var i = 0; i < count; i++) { - if ((i == thisIndex) || !ecs_rule_var_is_entity(Rule, i)) continue; - var name = ecs_rule_var_name(Rule, i).FlecsToString()!; + if ((i == thisIndex) || !ecs_rule_var_is_entity(handle, i)) continue; + var name = ecs_rule_var_name(handle, i).FlecsToString()!; _variables.Add(new(i, name)); } } // IEnumerable implementation - public IEnumerator GetEnumerator() => _variables.GetEnumerator(); + public IEnumerator GetEnumerator() => _variables.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _variables.GetEnumerator(); } } + +public unsafe class RuleIterator + : Iterator +{ + internal RuleIterator(ecs_iter_t value) + : base(value) { } + public override bool Next() + => ecs_rule_next(Handle); +} diff --git a/src/gaemstone.ECS/System.cs b/src/gaemstone.ECS/System.cs index 49999f5..29ea57b 100644 --- a/src/gaemstone.ECS/System.cs +++ b/src/gaemstone.ECS/System.cs @@ -1,34 +1,37 @@ using System; using System.Runtime.InteropServices; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; public static class SystemExtensions { - public static unsafe EntityRef InitSystem(this EntityRef entity, - QueryDesc query, Action callback) + public static unsafe Entity InitSystem(this Entity entity, + QueryDesc query, Action> callback) { var world = entity.World; + var internalCallback = (nint iter) => + callback(new Iterator((ecs_iter_t*)iter)); using var alloc = TempAllocator.Use(); var desc = new ecs_system_desc_t { entity = entity, query = query.ToFlecs(alloc), - binding_ctx = (void*)CallbackContextHelper.Create((world, callback)), + binding_ctx = (void*)CallbackContextHelper.Create(internalCallback), binding_ctx_free = new() { Data = new() { Pointer = &FreeContext } }, callback = new() { Data = new() { Pointer = &Callback } }, }; - return new(world, new(ecs_system_init(world, &desc))); + if (ecs_system_init(world, &desc).Data == default) + throw new InvalidOperationException(); + return entity; } + [UnmanagedCallersOnly] private static unsafe void Callback(ecs_iter_t* iter) - { - var (world, callback) = CallbackContextHelper - .Get<(World, Action)>((nint)iter->binding_ctx); - callback(new Iterator(world, null, *iter)); - } + => CallbackContextHelper + .Get>((nint)iter->binding_ctx) + .Invoke((nint)iter); [UnmanagedCallersOnly] private static unsafe void FreeContext(void* context) diff --git a/src/gaemstone.ECS/Term.cs b/src/gaemstone.ECS/Term.cs index fa5b36d..ef794ad 100644 --- a/src/gaemstone.ECS/Term.cs +++ b/src/gaemstone.ECS/Term.cs @@ -1,5 +1,5 @@ using System; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -19,11 +19,6 @@ public class Term public Term(TermId relation, TermId target) { Relation = relation; Target = target; } - public static implicit operator Term(EntityRef entity) => new(entity); - public static implicit operator Term(Entity entity) => new(entity); - public static implicit operator Term(IdRef id) => new(id); - public static implicit operator Term(Id id) => new(id); - public Term None { get { InOutKind = TermInOutKind.None; return this; } } public Term In { get { InOutKind = TermInOutKind.In; return this; } } public Term Out { get { InOutKind = TermInOutKind.Out; return this; } } @@ -35,9 +30,9 @@ public class Term public ecs_term_t ToFlecs(IAllocator allocator) => new() { id = Id, - src = Source?.ToFlecs(allocator) ?? default, + src = Source ?.ToFlecs(allocator) ?? default, first = Relation?.ToFlecs(allocator) ?? default, - second = Target?.ToFlecs(allocator) ?? default, + second = Target ?.ToFlecs(allocator) ?? default, inout = (ecs_inout_kind_t)InOutKind, oper = (ecs_oper_kind_t)OperKind, id_flags = (ecs_id_t)(ulong)Flags, @@ -83,10 +78,6 @@ public class TermId } else Name = name; } - public static implicit operator TermId(EntityRef entity) => new(entity); - public static implicit operator TermId(Entity entity) => new(entity); - public static implicit operator TermId(string name) => new(name); - public ecs_term_id_t ToFlecs(IAllocator allocator) => new() { id = Id, name = allocator.AllocateCString(Name), diff --git a/src/gaemstone.Utility/Allocators.cs b/src/gaemstone.ECS/Utility/Allocators.cs similarity index 91% rename from src/gaemstone.Utility/Allocators.cs rename to src/gaemstone.ECS/Utility/Allocators.cs index f392222..5208622 100644 --- a/src/gaemstone.Utility/Allocators.cs +++ b/src/gaemstone.ECS/Utility/Allocators.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading; using static flecs_hub.flecs.Runtime; -namespace gaemstone.Utility; +namespace gaemstone.ECS.Utility; public interface IAllocator { @@ -15,18 +15,19 @@ public interface IAllocator public unsafe static class AllocatorExtensions { + public static Span AllocateCopy(this IAllocator allocator, ReadOnlySpan orig) where T : unmanaged + { var copy = allocator.Allocate(orig.Length); orig.CopyTo(copy); return copy; } public static Span Allocate(this IAllocator allocator, int count) where T : unmanaged => new((void*)allocator.Allocate(sizeof(T) * count), count); public static void Free(this IAllocator allocator, Span span) where T : unmanaged => allocator.Free((nint)Unsafe.AsPointer(ref span[0])); - public static Span AllocateCopy(this IAllocator allocator, ReadOnlySpan orig) where T : unmanaged - { var copy = allocator.Allocate(orig.Length); orig.CopyTo(copy); return copy; } - - public static ref T Allocate(this IAllocator allocator) where T : unmanaged - => ref Unsafe.AsRef((void*)allocator.Allocate(sizeof(T))); - public static void Free(this IAllocator allocator, ref T value) where T : unmanaged - => allocator.Free((nint)Unsafe.AsPointer(ref value)); + public static T* AllocateCopy(this IAllocator allocator, T orig) where T : unmanaged + { var ptr = allocator.Allocate(); *ptr = orig; return ptr; } + public static T* Allocate(this IAllocator allocator) where T : unmanaged + => (T*)allocator.Allocate(sizeof(T)); + public static void Free(this IAllocator allocator, T* ptr) where T : unmanaged + => allocator.Free((nint)ptr); public static CString AllocateCString(this IAllocator allocator, string? value) { diff --git a/src/gaemstone.Utility/CStringExtensions.cs b/src/gaemstone.ECS/Utility/CStringExtensions.cs similarity index 97% rename from src/gaemstone.Utility/CStringExtensions.cs rename to src/gaemstone.ECS/Utility/CStringExtensions.cs index 0cc756b..406d552 100644 --- a/src/gaemstone.Utility/CStringExtensions.cs +++ b/src/gaemstone.ECS/Utility/CStringExtensions.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using static flecs_hub.flecs; using static flecs_hub.flecs.Runtime; -namespace gaemstone.Utility; +namespace gaemstone.ECS.Utility; public unsafe static class CStringExtensions { diff --git a/src/gaemstone.Utility/CallbackContextHelper.cs b/src/gaemstone.ECS/Utility/CallbackContextHelper.cs similarity index 94% rename from src/gaemstone.Utility/CallbackContextHelper.cs rename to src/gaemstone.ECS/Utility/CallbackContextHelper.cs index 3d22015..9c838bb 100644 --- a/src/gaemstone.Utility/CallbackContextHelper.cs +++ b/src/gaemstone.ECS/Utility/CallbackContextHelper.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace gaemstone.Utility; +namespace gaemstone.ECS.Utility; public static class CallbackContextHelper { diff --git a/src/gaemstone.ECS/Utility/ReferenceHandle.cs b/src/gaemstone.ECS/Utility/ReferenceHandle.cs new file mode 100644 index 0000000..5dec50f --- /dev/null +++ b/src/gaemstone.ECS/Utility/ReferenceHandle.cs @@ -0,0 +1,71 @@ +using System; +using System.Runtime.InteropServices; +using static flecs_hub.flecs; + +namespace gaemstone.ECS.Utility; + +public unsafe readonly struct ReferenceHandle + : IDisposable +{ + public static int NumActiveHandles { get; private set; } + + + private readonly nint _value; + + public object? Target => (_value != default) + ? ((GCHandle)_value).Target : null; + + private ReferenceHandle(nint value) + => _value = value; + + public static ReferenceHandle Alloc(object? target) + { + if (target == null) return default; + NumActiveHandles++; + return new((nint)GCHandle.Alloc(target)); + } + + public ReferenceHandle Clone() + => Alloc(Target); + + public void Dispose() + { + if (_value == default) return; + NumActiveHandles--; + ((GCHandle)_value).Free(); + } + + + [UnmanagedCallersOnly] + internal static void Construct(void* ptr, int count, ecs_type_info_t* _) + => new Span(ptr, count).Clear(); + + [UnmanagedCallersOnly] + internal static void Destruct(void* ptr, int count, ecs_type_info_t* _) + { + var span = new Span(ptr, count); + foreach (var handle in span) handle.Dispose(); + span.Clear(); + } + + [UnmanagedCallersOnly] + internal static void Move(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _) + { + var dst = new Span(dstPtr, count); + var src = new Span(srcPtr, count); + foreach (var handle in dst) handle.Dispose(); + src.CopyTo(dst); + src.Clear(); + } + + [UnmanagedCallersOnly] + internal static void Copy(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _) + { + var dst = new Span(dstPtr, count); + var src = new Span(srcPtr, count); + for (var i = 0; i < count; i++) { + dst[i].Dispose(); + dst[i] = src[i].Clone(); + } + } +} diff --git a/src/gaemstone.Utility/SpanExtensions.cs b/src/gaemstone.ECS/Utility/SpanExtensions.cs similarity index 97% rename from src/gaemstone.Utility/SpanExtensions.cs rename to src/gaemstone.ECS/Utility/SpanExtensions.cs index 3643a87..c072e0f 100644 --- a/src/gaemstone.Utility/SpanExtensions.cs +++ b/src/gaemstone.ECS/Utility/SpanExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace gaemstone.Utility; +namespace gaemstone.ECS.Utility; public static class SpanExtensions { diff --git a/src/gaemstone.ECS/World+Bare.cs b/src/gaemstone.ECS/World+Bare.cs new file mode 100644 index 0000000..bbda414 --- /dev/null +++ b/src/gaemstone.ECS/World+Bare.cs @@ -0,0 +1,73 @@ +using System; +using System.Runtime.InteropServices; +using gaemstone.ECS.Utility; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public unsafe struct World + : IEquatable + , IDisposable +{ + public ecs_world_t* Handle { get; } + + public bool IsDeferred => ecs_is_deferred(this); + public bool IsQuitRequested => ecs_should_quit(this); + + public World(ecs_world_t* handle) + => Handle = handle; + + public World(params string[] args) + { + [UnmanagedCallersOnly] + static void Abort() => throw new FlecsAbortException(); + + ecs_os_set_api_defaults(); + var api = ecs_os_get_api(); + api.abort_ = new FnPtr_Void { Pointer = &Abort }; + ecs_os_set_api(&api); + + Handle = ecs_init_w_args(args.Length, null); + } + + public bool Progress(TimeSpan delta) + => ecs_progress(this, (float)delta.TotalSeconds); + + public void Quit() + => ecs_quit(this); + + public void Dispose() + => ecs_fini(this); + + + public Entity LookupAlive(Entity value) + => new(ecs_get_alive(this, value)); + + public Entity LookupPath(EntityPath path, Entity parent = default, + bool throwOnNotFound = false) + => new(EntityPath.Lookup(this, path, parent, throwOnNotFound)); + + // TODO: Provide overload that uses a UTF-8 byte span? + public Entity LookupSymbol(string symbol) + { + using var alloc = TempAllocator.Use(); + return new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false)); + } + + + public bool Equals(World other) + { + if (Handle == other.Handle) return true; + return ecs_get_world((ecs_poly_t*)Handle) + == ecs_get_world((ecs_poly_t*)other.Handle); + } + public override bool Equals(object? obj) + => (obj is World other) && Equals(other); + public override int GetHashCode() + => ((nint)ecs_get_world((ecs_poly_t*)Handle)).GetHashCode(); + + public static bool operator ==(World left, World right) => left.Equals(right); + public static bool operator !=(World left, World right) => !left.Equals(right); + + public static implicit operator ecs_world_t*(World w) => w.Handle; +} diff --git a/src/gaemstone.ECS/World+Lookup.cs b/src/gaemstone.ECS/World+Lookup.cs deleted file mode 100644 index 1086aea..0000000 --- a/src/gaemstone.ECS/World+Lookup.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe partial class World -{ - public EntityRef? LookupAlive(Entity value) - => EntityRef.CreateOrNull(this, new(ecs_get_alive(this, value))); - public EntityRef LookupAliveOrThrow(Entity entity) - => LookupAlive(entity) ?? throw new EntityNotFoundException( - $"Entity {entity} is not alive"); - - // TODO: Simplify method names? - public EntityRef? LookupByPath(EntityPath path) - => LookupByPath(default, path); - public EntityRef? LookupByPath(Entity parent, EntityPath path) - => EntityRef.CreateOrNull(this, EntityPath.Lookup(this, parent, path, false)); - public EntityRef LookupByPathOrThrow(EntityPath path) - => LookupByPathOrThrow(default, path); - public EntityRef LookupByPathOrThrow(Entity parent, EntityPath path) - => new(this, EntityPath.Lookup(this, parent, path, true)); - - // TODO: Provide overload that uses a UTF-8 byte span? - public EntityRef? LookupBySymbol(string symbol) - { - using var alloc = TempAllocator.Use(); - var entity = ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false); - return EntityRef.CreateOrNull(this, new(entity)); - } - public EntityRef LookupBySymbolOrThrow(string symbol) - => LookupBySymbol(symbol) ?? throw new EntityNotFoundException( - $"Entity with symbol '{symbol}' not found"); -} - -public class EntityNotFoundException : Exception - { public EntityNotFoundException(string message) : base(message) { } } diff --git a/src/gaemstone.ECS/World.cs b/src/gaemstone.ECS/World.cs index 00ad339..9689a2b 100644 --- a/src/gaemstone.ECS/World.cs +++ b/src/gaemstone.ECS/World.cs @@ -1,51 +1,69 @@ using System; -using gaemstone.Utility; +using gaemstone.ECS.Internal; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe partial class World - : IDisposable +public unsafe struct World + : IEquatable> + , IDisposable { - public ecs_world_t* Handle { get; } + public readonly World Value; - // Flecs built-ins that are important to internals. - internal EntityRef ChildOf { get; } - internal EntityRef Disabled { get; } - - public bool IsDeferred => ecs_is_deferred(this); - public bool IsQuitRequested => ecs_should_quit(this); + public bool IsDeferred => Value.IsDeferred; + public bool IsQuitRequested => Value.IsQuitRequested; + public World(World value) + => Value = value; public World(params string[] args) - { - ecs_os_set_api_defaults(); - var api = ecs_os_get_api(); - api.abort_ = new FnPtr_Void { Pointer = &FlecsAbortException.Callback }; - ecs_os_set_api(&api); + : this(new World(args)) { } - Handle = ecs_init_w_args(args.Length, null); + public bool Progress(TimeSpan delta) => Value.Progress(delta); + public void Quit() => Value.Quit(); + public void Dispose() => Value.Dispose(); - ChildOf = LookupByPathOrThrow("/flecs/core/ChildOf"); - Disabled = LookupByPathOrThrow("/flecs/core/Disabled"); - } - public void Dispose() - => ecs_fini(this); + public EntityBuilder New(EntityPath? path = null, Entity parent = default) + => new(this, parent, path); - public EntityBuilder New(EntityPath? path = null) - => new(this, default, path); + public Entity? LookupAliveOrNull(Entity value) + => ECS.Entity.GetOrNull(this, Value.LookupAlive(value)); + public Entity LookupAliveOrThrow(Entity value) + => LookupAliveOrNull(value) ?? throw new EntityNotFoundException( + $"Entity '{value}' could not be found"); - public EntityBuilder New(EntityRef? parent, EntityPath? path = null) - => new(this, parent, path); + public Entity? LookupPathOrNull(EntityPath path, Entity parent = default) + => ECS.Entity.GetOrNull(this, Value.LookupPath(path, parent, false)); + public Entity LookupPathOrThrow(EntityPath path, Entity parent = default) + => ECS.Entity.GetOrInvalid(this, Value.LookupPath(path, parent, true)); + + public Entity? LookupSymbolOrNull(string symbol) + => ECS.Entity.GetOrNull(this, Value.LookupSymbol(symbol)); + public Entity LookupSymbolOrThrow(string symbol) + => LookupSymbolOrNull(symbol) ?? throw new EntityNotFoundException( + $"Entity with symbol '{symbol}' could not be found"); + + + public Entity Entity() + => ECS.Entity.GetOrThrow(this, + Lookup.Entity.Value); + public Id Pair(Entity relation, Entity target) + => Id.GetOrThrow(this, Id.Pair(relation, target)); + public Id Pair(Entity target) + => Pair(Entity(), target); + public Id Pair() + => Pair(Entity(), Entity()); - public bool Progress(TimeSpan delta) - => ecs_progress(this, (float)delta.TotalSeconds); - public void Quit() - => ecs_quit(this); + public bool Equals(World other) => Value == other.Value; + public override bool Equals(object? obj) => (obj is World other) && Equals(other); + public override int GetHashCode() => Value.GetHashCode(); + public static bool operator ==(World left, World right) => left.Equals(right); + public static bool operator !=(World left, World right) => !left.Equals(right); - public static implicit operator ecs_world_t*(World w) => w.Handle; + public static implicit operator World (World world) => world.Value; + public static implicit operator ecs_world_t*(World world) => world.Value.Handle; } diff --git a/src/gaemstone.Utility/FlecsException.cs b/src/gaemstone.Utility/FlecsException.cs deleted file mode 100644 index fa16503..0000000 --- a/src/gaemstone.Utility/FlecsException.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace gaemstone.Utility; - -public class FlecsException - : Exception -{ - public FlecsException() : base() { } - public FlecsException(string message) : base(message) { } -} - -public class FlecsAbortException - : FlecsException -{ - private readonly string _stackTrace = new StackTrace(2, true).ToString(); - public override string? StackTrace => _stackTrace; - - private FlecsAbortException() - : base("Abort was called by flecs") { } - - [UnmanagedCallersOnly] - internal static void Callback() - => throw new FlecsAbortException(); -}