Initial commit

Split gaemstone.ECS into its own project
For older history, see copygirl/gaemstone@0c6d63af21d086f71d1c46acda5ee4c1d0220914
wip/bindgen
copygirl 9 months ago
commit 127e957c97
  1. 27
      .editorconfig
  2. 2
      .gitignore
  3. 3
      .gitmodules
  4. 21
      LICENSE.txt
  5. 103
      README.md
  6. 23
      gaemstone.ECS.csproj
  7. 1
      src/flecs-cs
  8. 30
      src/gaemstone.ECS/Component.cs
  9. 30
      src/gaemstone.ECS/Entity.cs
  10. 61
      src/gaemstone.ECS/EntityBase.cs
  11. 116
      src/gaemstone.ECS/EntityBuilder.cs
  12. 233
      src/gaemstone.ECS/EntityPath.cs
  13. 162
      src/gaemstone.ECS/EntityRef.cs
  14. 25
      src/gaemstone.ECS/EntityType.cs
  15. 77
      src/gaemstone.ECS/Filter.cs
  16. 54
      src/gaemstone.ECS/Identifier.cs
  17. 46
      src/gaemstone.ECS/IdentifierRef.cs
  18. 123
      src/gaemstone.ECS/Iterator.cs
  19. 37
      src/gaemstone.ECS/Observer.cs
  20. 46
      src/gaemstone.ECS/Query.cs
  21. 31
      src/gaemstone.ECS/Rule.cs
  22. 56
      src/gaemstone.ECS/System.cs
  23. 116
      src/gaemstone.ECS/Term.cs
  24. 67
      src/gaemstone.ECS/World+Lookup.cs
  25. 40
      src/gaemstone.ECS/World.cs
  26. 170
      src/gaemstone.Utility/Allocators.cs
  27. 34
      src/gaemstone.Utility/CStringExtensions.cs
  28. 31
      src/gaemstone.Utility/CallbackContextHelper.cs
  29. 41
      src/gaemstone.Utility/SpanExtensions.cs
  30. 19
      src/gaemstone.Utility/SpanToRef.cs

@ -0,0 +1,27 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.cs]
indent_style = tab
indent_size = 4
# IDE0005: Using directive is unnecessary
dotnet_diagnostic.IDE0005.severity = suggestion
# IDE0047: Parentheses can be removed
dotnet_diagnostic.IDE0047.severity = none
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = none
[src/flecs-cs/**]
# Suppress compiler and analyer warnings in dependencies.
dotnet_analyzer_diagnostic.severity = none
dotnet_diagnostic.IDE0005.severity = none
[*.md]
# Allows placing double-space at end of lines to force linebreaks.
trim_trailing_whitespace = false

2
.gitignore vendored

@ -0,0 +1,2 @@
**/obj/
**/bin/

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "src/flecs-cs"]
path = src/flecs-cs
url = https://github.com/flecs-hub/flecs-cs

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 copygirl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,103 @@
# gaemstone.ECS
.. is a medium-level managed wrapper library around the [flecs-cs] bindings
for the amazing [Entity Component System (ECS)][ECS] framework [Flecs]. It is
used as part of the [gæmstone] game engine, but may be used in other projects
as well. To efficiently use this library, a thorough understanding of [Flecs]
is required.
[ECS]: https://en.wikipedia.org/wiki/Entity_component_system
[Flecs]: https://github.com/SanderMertens/flecs
[flecs-cs]: https://github.com/flecs-hub/flecs-cs
[gæmstone]: https://git.mcft.net/copygirl/gaemstone
## Features
These classes have only recently been split from the main **gæmstone** project. It is currently unclear what functionality belongs where, and structural changes are still very likely. In its current state, feel free to use **gæmstone.ECS** as a reference for building similar projects, or be aware that things are in flux, and in general nowhere near stable.
- Simple wrapper structs such as [Entity] and [Identifier].
- Classes with convenience functions like [EntityRef] and [EntityType].
- Define your own [Components] as both value or reference types.
- Query the ECS with [Iterators], [Filters], [Queries] and [Rules].
- Create [Systems] for game logic and [Observers] to act on changes.
- [EntityPath] uses a unix-like path: `/Game/Players/copygirl`
[Entity]: ./src/gaemstone.ECS/Entity.cs
[Identifier]: ./src/gaemstone.ECS/Identifier.cs
[EntityRef]: ./src/gaemstone.ECS/EntityRef.cs
[EntityType]: ./src/gaemstone.ECS/EntityType.cs
[Components]: ./src/gaemstone.ECS/Component.cs
[Iterators]: ./src/gaemstone.ECS/Iterator.cs
[Filters]: ./src/gaemstone.ECS/Filter.cs
[Queries]: ./src/gaemstone.ECS/Query.cs
[Rules]: ./src/gaemstone.ECS/Rule.cs
[Systems]: ./src/gaemstone.ECS/System.cs
[Observers]: ./src/gaemstone.ECS/Observer.cs
[EntityPath]: ./src/gaemstone.ECS/EntityPath.cs
## Example
```cs
var world = new World();
var position = world
.New("Position") // Create a new EntityBuilder, and set its name.
.Symbol("Position") // Set entity's symbol. (Used in query expression.)
.Build() // Actually create the entity in-world.
// Finally, create a component from this entity.
// The "Position" struct is defined at the bottom of this example.
.InitComponent<Position>();
world.New("One").Set(new Position( 0, 0)).Build();
world.New("Two").Set(new Position(10, 20)).Build();
var onUpdate = world.LookupOrThrow("/flecs/pipeline/OnUpdate");
// Create a system that will move all entities with
// the "Position" component downwards by 2 every frame.
world.New("FallSystem").Build()
.InitSystem(onUpdate, new("Position"), iter => {
var positionSpan = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
ref var position = ref positionSpan[i];
position = new(position.X, position.Y + 2);
}
});
// Create a system that will print out entities' positions.
world.New("PrintPositionSystem").Build()
.InitSystem(onUpdate, new("[in] Position"), iter => {
var positionSpan = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
var entity = iter.Entity(i);
var position = positionSpan[i];
Console.WriteLine($"{entity.Name} is at {position}");
}
});
// Infinite loop that runs the "game" at 30 FPS.
while (true) {
var delta = TimeSpan.FromSeconds(1.0f / 30);
// Progress returns false if quit was requested.
if (!world.Progress(delta)) break;
Thread.Sleep(delta);
}
record struct Position(int X, int Y);
```
## Instructions
```sh
# Clone the repository. Recurse into submodules to include flecs-cs and flecs.
git clone --recurse-submodules https://git.mcft.net/copygirl/gaemstone.ECS.git
# If you want to add it to your own repository as a submodule:
git submodule add https://git.mcft.net/copygirl/gaemstone.ECS.git
# To add a reference to this library to your .NET project:
dotnet add reference gaemstone.ECS/gaemstone.ECS.csproj
# To generate flecs-cs' bindings:
./gaemstone.ECS/src/flecs-cs/library.sh
```

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="src/gaemstone.ECS/**/*.cs" />
<Compile Include="src/gaemstone.Utility/**/*.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="src/flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1 @@
Subproject commit a2047983917aa462a8c2f34d5315aea48502f4d8

@ -0,0 +1,30 @@
using System;
using System.Runtime.CompilerServices;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public static class ComponentExtensions
{
public unsafe static EntityRef InitComponent(this EntityRef entity, Type type)
=> (EntityRef)typeof(ComponentExtensions)
.GetMethod(nameof(InitComponent), new[] { typeof(EntityRef) })!
.MakeGenericMethod(type).Invoke(null, new[]{ entity })!;
public unsafe static EntityRef InitComponent<T>(this EntityRef entity)
{
if (typeof(T).IsPrimitive) throw new ArgumentException(
"Must not be primitive");
if (typeof(T).IsValueType && RuntimeHelpers.IsReferenceOrContainsReferences<T>()) throw new ArgumentException(
"Struct component must satisfy the unmanaged constraint. " +
"Consider making it a class if you need to store references.");
var size = Unsafe.SizeOf<T>();
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);
return entity.CreateLookup(typeof(T));
}
}

@ -0,0 +1,30 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public readonly struct Entity
: IEquatable<Entity>
{
public static readonly Entity None = default;
public readonly ecs_entity_t Value;
public uint Id => (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() => $"Entity(0x{Value.Data.Data:X})";
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 ecs_entity_t(Entity e) => e.Value;
public static implicit operator Identifier(Entity e) => new(e.Value.Data);
public static implicit operator ecs_id_t(Entity e) => e.Value.Data;
}

@ -0,0 +1,61 @@
namespace gaemstone.ECS;
public abstract class EntityBase<TReturn>
{
public abstract World World { get; }
public abstract TReturn Add(Identifier id);
public abstract TReturn Remove(Identifier id);
public abstract bool Has(Identifier id);
public TReturn Add(string symbol) => Add(World.LookupSymbolOrThrow(symbol));
public TReturn Add<T>() => Add(World.LookupOrThrow(typeof(T)));
public TReturn Add(Entity relation, Entity target) => Add(Identifier.Pair(relation, target));
public TReturn Add<TRelation>(Entity target) => Add(World.LookupOrThrow<TRelation>(), target);
public TReturn Add<TRelation, TTarget>() => Add(World.LookupOrThrow<TRelation>(), World.LookupOrThrow<TTarget>());
public TReturn Remove(string symbol) => Remove(World.LookupSymbolOrThrow(symbol));
public TReturn Remove<T>() => Remove(World.LookupOrThrow(typeof(T)));
public TReturn Remove(Entity relation, Entity target) => Remove(Identifier.Pair(relation, target));
public TReturn Remove<TRelation>(Entity target) => Remove(World.LookupOrThrow<TRelation>(), target);
public TReturn Remove<TRelation, TTarget>() => Remove(World.LookupOrThrow<TRelation>(), World.LookupOrThrow<TTarget>());
public bool Has(string symbol) => Has(World.LookupSymbolOrThrow(symbol));
public bool Has<T>() => Has(World.LookupOrThrow(typeof(T)));
public bool Has(Entity relation, Entity target) => Has(Identifier.Pair(relation, target));
public bool Has<TRelation>(Entity target) => Has(World.LookupOrThrow<TRelation>(), target);
public bool Has<TRelation, TTarget>() => Has(World.LookupOrThrow<TRelation>(), World.LookupOrThrow<TTarget>());
public abstract T Get<T>(Identifier id);
public abstract T? GetOrNull<T>(Identifier id) where T : unmanaged;
public abstract T? GetOrNull<T>(Identifier id, T _ = null!) where T : class;
public abstract ref T GetMut<T>(Identifier id) where T : unmanaged;
public abstract ref T GetRefOrNull<T>(Identifier id) where T : unmanaged;
public abstract ref T GetRefOrThrow<T>(Identifier id) where T : unmanaged;
public abstract void Modified<T>(Identifier id);
public T Get<T>() => Get<T>(World.LookupOrThrow<T>());
public T? GetOrNull<T>() where T : unmanaged => GetOrNull<T>(World.LookupOrThrow<T>());
public T? GetOrNull<T>(T _ = null!) where T : class => GetOrNull<T>(World.LookupOrThrow<T>());
public ref T GetMut<T>() where T : unmanaged => ref GetMut<T>(World.LookupOrThrow<T>());
public ref T GetRefOrNull<T>() where T : unmanaged => ref GetRefOrNull<T>(World.LookupOrThrow<T>());
public ref T GetRefOrThrow<T>() where T : unmanaged => ref GetRefOrThrow<T>(World.LookupOrThrow<T>());
public void Modified<T>() => Modified<T>(World.LookupOrThrow<T>());
public abstract TReturn Set<T>(Identifier id, in T value) where T : unmanaged;
public abstract TReturn Set<T>(Identifier id, T obj) where T : class;
public TReturn Set<T>(in T value) where T : unmanaged => Set(World.LookupOrThrow<T>(), value);
public TReturn Set<T>(T obj) where T : class => Set(World.LookupOrThrow<T>(), obj);
public TReturn ChildOf(Entity parent) => Add(World.ChildOf, parent);
public TReturn ChildOf<TParent>() => Add(World.ChildOf, World.LookupOrThrow<TParent>());
public TReturn Disable() => Add(World.Disabled);
public TReturn Enable() => Remove(World.Disabled);
public bool IsDisabled => Has(World.Disabled);
}

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public class EntityBuilder
: EntityBase<EntityBuilder>
{
public override World World { get; }
/// <summary> Set to modify existing entity (optional). </summary>
public Entity Id { get; set; }
/// <summary>
/// Path of the entity. If no entity is provided, an entity with this path
/// will be looked up first. When an entity is provided, the path will be
/// verified with the existing entity.
/// </summary>
public EntityPath? Path { get; set; }
/// <summary>
/// Optional entity symbol. A symbol is an unscoped identifier that can
/// be used to lookup an entity. The primary use case for this is to
/// associate the entity with a language identifier, such as a type or
/// function name, where these identifiers differ from the name they are
/// registered with in flecs.
/// </summary>
public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; }
private string? _symbol = null;
/// <summary>
/// When set to true, a low id (typically reserved for components)
/// will be used to create the entity, if no id is specified.
/// </summary>
public bool UseLowId { get; set; }
/// <summary> Ids to add to the new or existing entity. </summary>
private readonly HashSet<Identifier> _toAdd = new();
private Entity _parent = Entity.None;
/// <summary> String expression with components to add. </summary>
public string? Expression { get; }
/// <summary> Actions to run once the entity has been created. </summary>
private readonly List<Action<EntityRef>> _toSet = new();
public EntityBuilder(World world, EntityPath? path = null)
{ World = world; Path = path; }
public override EntityBuilder Add(Identifier id)
{
// If adding a ChildOf relation, store the parent separately.
if (id.AsPair(World) is (EntityRef relation, EntityRef target) &&
(relation == World.ChildOf)) { _parent = target; 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 override EntityBuilder Remove(Identifier id)
=> throw new NotSupportedException();
public override bool Has(Identifier id)
=> !id.IsWildcard ? _toAdd.Contains(id)
: throw new NotSupportedException(); // TODO: Support wildcard.
public override T Get<T>(Identifier id) => throw new NotSupportedException();
public override T? GetOrNull<T>(Identifier id) => throw new NotSupportedException();
public override T? GetOrNull<T>(Identifier id, T _ = null!) where T : class => throw new NotSupportedException();
public override ref T GetMut<T>(Identifier id) => throw new NotSupportedException();
public override ref T GetRefOrNull<T>(Identifier id) => throw new NotSupportedException();
public override ref T GetRefOrThrow<T>(Identifier id) => throw new NotSupportedException();
public override void Modified<T>(Identifier id) => throw new NotImplementedException();
public override EntityBuilder Set<T>(Identifier id, in T value)
// "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; }
public override EntityBuilder Set<T>(Identifier id, T obj)
{ _toSet.Add(e => e.Set(id, obj)); return this; }
public unsafe EntityRef Build()
{
var parent = _parent;
if (Path != null) {
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!);
}
using var alloc = TempAllocator.Use();
var desc = new ecs_entity_desc_t {
id = Id,
name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default,
symbol = alloc.AllocateCString(_symbol),
add_expr = alloc.AllocateCString(Expression),
use_low_id = UseLowId,
sep = CStringExtensions.ETX,
};
var add = desc.add; var index = 0;
if (parent.IsSome) add[index++] = Identifier.Pair(World.ChildOf, parent);
foreach (var id in _toAdd) add[index++] = id;
var entityId = ecs_entity_init(World, &desc);
var entity = new EntityRef(World, new(entityId));
foreach (var action in _toSet) action(entity);
return entity;
}
}

@ -0,0 +1,233 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public class EntityPath
{
private readonly byte[][] _parts;
public bool IsAbsolute { get; }
public bool IsRelative => !IsAbsolute;
public int Count => _parts.Length;
public UTF8View Name => this[^1];
public EntityPath? Parent => (Count > 1) ? new(IsAbsolute, _parts[..^1]) : null;
public UTF8View this[int index]
=> (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1])
: throw new ArgumentOutOfRangeException(nameof(index));
public EntityPath this[Range range]
=> new(IsAbsolute && (range.GetOffsetAndLength(Count).Offset == 0), _parts[range]);
internal EntityPath(bool absolute, params byte[][] parts)
{
if (parts.Length == 0) throw new ArgumentException(
"Must have at least one part", nameof(parts));
IsAbsolute = absolute;
_parts = parts;
}
public EntityPath(params string[] parts)
: this(false, parts) { }
public EntityPath(bool absolute, params string[] parts)
: this(absolute, parts.Select(part => {
if (GetNameValidationError(part) is string error)
throw new ArgumentException(error);
var byteCount = Encoding.UTF8.GetByteCount(part);
// Includes NUL character at the end of bytes.
var bytes = new byte[byteCount + 1];
Encoding.UTF8.GetBytes(part, bytes);
return bytes;
}).ToArray()) { }
public static bool TryParse(string str, [NotNullWhen(true)] out EntityPath? result)
{
result = null;
if (str.Length == 0) return false;
var strSpan = str.AsSpan();
var isAbsolute = (str[0] == '/');
if (isAbsolute) strSpan = strSpan[1..];
var numSeparators = 0;
foreach (var chr in strSpan) if (chr == '/') numSeparators++;
var index = 0;
var parts = new byte[numSeparators + 1][];
foreach (var part in strSpan.Split('/')) {
if (GetNameValidationError(part) != null) return false;
var byteCount = Encoding.UTF8.GetByteCount(part);
// Includes NUL character at the end of bytes.
var bytes = new byte[byteCount + 1];
Encoding.UTF8.GetBytes(part, bytes);
parts[index++] = bytes;
}
result = new(isAbsolute, parts);
return true;
}
public static EntityPath Parse(string str)
{
if (str.Length == 0) throw new ArgumentException(
"String must not be empty", nameof(str));
var parts = str.Split('/');
// If string starts with a slash, first part will be empty, so create an absolute path.
return (parts[0].Length == 0) ? new(true, parts[1..]) : new(parts);
}
public static string? GetNameValidationError(ReadOnlySpan<char> name)
{
if (name.Length == 0) return "Must not be empty";
// NOTE: This is a hopefully straightforward way to also prevent "."
// and ".." to be part of paths which may access the file system.
if (name[0] == '.') return "Must not begin with a dot";
foreach (var chr in name) if (char.IsControl(chr))
return "Must not contain contol characters";
return null;
}
// private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' };
// private static readonly UnicodeCategory[] _validCategories = {
// UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter,
// UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber };
// private static void ValidateRune(Rune rune)
// {
// if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune)))
// throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character");
// }
public string[] GetParts()
{
var result = new string[Count];
for (var i = 0; i < Count; i++) result[i] = this[i];
return result;
}
public override string ToString()
{
var builder = new StringBuilder();
if (IsAbsolute) builder.Append('/');
foreach (var part in this) builder.Append(part).Append('/');
return builder.ToString(0, builder.Length - 1);
}
public static implicit operator EntityPath(string path) => Parse(path);
public Enumerator GetEnumerator() => new(this);
public ref struct Enumerator
{
private readonly EntityPath _path;
private int index = -1;
public UTF8View Current => _path[index];
internal Enumerator(EntityPath path) => _path = path;
public bool MoveNext() => (++index < _path.Count);
}
internal static unsafe Entity Lookup(World world, Entity parent, EntityPath path)
{
// If path is absolute, ignore parent and start at root.
if (path.IsAbsolute) parent = default;
// Otherwise, if no parent is specified, use the current scope.
else if (parent.IsNone) parent = new(ecs_get_scope(world));
foreach (var part in path)
fixed (byte* ptr = part.AsSpan()) {
// FIXME: This breaks when using large entity Ids.
parent = new(ecs_lookup_child(world, parent, ptr));
if (parent.IsNone || !ecs_is_alive(world, parent))
return Entity.None;
}
return parent;
}
/// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary>
internal static unsafe Entity EnsureEntityExists(
World world, Entity parent, EntityPath path)
{
// If no parent is specified and path is relative, use the current scope.
if (parent.IsNone && path.IsRelative) parent = new(ecs_get_scope(world));
var skipLookup = parent.IsNone;
foreach (var part in path)
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] = Identifier.Pair(world.ChildOf, parent);
parent = new(ecs_entity_init(world, &desc));
skipLookup = true;
}
return parent;
}
}
public static class EntityPathExtensions
{
public static unsafe EntityPath GetFullPath(this EntityRef entity)
{
var current = (Entity)entity;
var parts = new List<byte[]>(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<byte> _bytes;
public UTF8View(ReadOnlySpan<byte> bytes)
=> _bytes = bytes;
public int Length => _bytes.Length;
public byte this[int index] => _bytes[index];
public ReadOnlySpan<byte> AsSpan() => _bytes;
public override string ToString() => Encoding.UTF8.GetString(_bytes);
public static implicit operator string(UTF8View view) => view.ToString();
public Enumerator GetEnumerator() => new(_bytes);
public ref struct Enumerator
{
private readonly ReadOnlySpan<byte> _bytes;
private int index = 0;
private Rune _current = default;
public Rune Current => _current;
internal Enumerator(ReadOnlySpan<byte> bytes)
=> _bytes = bytes;
public bool MoveNext()
{
if (index >= _bytes.Length) return false;
if (Rune.DecodeFromUtf8(_bytes[index..], out _current, out var consumed) != OperationStatus.Done)
throw new InvalidOperationException("Contains invalid UTF8");
index += consumed;
return true;
}
}
}

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class EntityRef
: EntityBase<EntityRef>
, IEquatable<EntityRef>
{
public override 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(EnsureRelativePath(path)).ChildOf(this);
public EntityRef? Lookup(EntityPath path)
=> World.Lookup(this, EnsureRelativePath(path)!);
public EntityRef LookupOrThrow(EntityPath path)
=> World.LookupOrThrow(this, EnsureRelativePath(path)!);
private static EntityPath? EnsureRelativePath(EntityPath? path)
{ if (path?.IsAbsolute == true) throw new ArgumentException("path must not be absolute", nameof(path)); return path; }
public EntityRef? Parent
=> GetTarget(World.ChildOf);
public IEnumerable<EntityRef> 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 override EntityRef Add(Identifier id) { ecs_add_id(World, this, id); return this; }
public override EntityRef Remove(Identifier id) { ecs_remove_id(World, this, id); return this; }
public override bool Has(Identifier id) => ecs_has_id(World, this, id);
public override T Get<T>(Identifier id)
{
var ptr = ecs_get_id(World, this, id);
if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}");
return typeof(T).IsValueType ? Unsafe.Read<T>(ptr)
: (T)((GCHandle)Unsafe.Read<nint>(ptr)).Target!;
}
public override T? GetOrNull<T>(Identifier id)
{
var ptr = ecs_get_id(World, this, id);
return (ptr != null) ? Unsafe.Read<T>(ptr) : null;
}
public override T? GetOrNull<T>(Identifier id, T _ = null!)
where T : class
{
var ptr = ecs_get_id(World, this, id);
return (ptr != null) ? (T)((GCHandle)Unsafe.Read<nint>(ptr)).Target! : null;
}
public override ref T GetRefOrNull<T>(Identifier id)
{
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<T>(ptr) : ref Unsafe.NullRef<T>();
}
public override ref T GetRefOrThrow<T>(Identifier id)
{
ref var ptr = ref GetRefOrNull<T>(id);
if (Unsafe.IsNullRef(ref ptr)) throw new Exception(
$"Component {typeof(T)} not found on {this}");
return ref ptr;
}
public override ref T GetMut<T>(Identifier id)
{
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<T>(ptr);
}
public override void Modified<T>(Identifier id)
=> ecs_modified_id(World, this, id);
public override EntityRef Set<T>(Identifier id, in T value)
{
var size = (ulong)Unsafe.SizeOf<T>();
fixed (T* ptr = &value)
if (ecs_set_id(World, this, id, size, ptr).Data == 0)
throw new InvalidOperationException();
return this;
}
public override EntityRef Set<T>(Identifier id, T obj) where T : class
{
var handle = (nint)GCHandle.Alloc(obj);
// FIXME: Previous handle needs to be freed.
if (ecs_set_id(World, this, id, (ulong)sizeof(nint), &handle).Data == 0)
throw new InvalidOperationException();
// FIXME: Handle needs to be freed when component is removed!
return this;
}
public EntityRef? GetTarget(Entity relation, int index = 0)
=> CreateOrNull(World, new(ecs_get_target(World, this, relation, index)));
public EntityRef? GetTarget(string symbol, int index = 0)
=> GetTarget(World.LookupSymbolOrThrow(symbol), index);
public EntityRef? GetTarget<T>(int index = 0)
=> GetTarget(World.LookupOrThrow(typeof(T)), index);
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 Identifier(EntityRef? e) => new(e?.Entity.Value.Data ?? default);
public static implicit operator ecs_id_t(EntityRef? e) => e?.Entity.Value.Data ?? default;
}

@ -0,0 +1,25 @@
using System.Collections;
using System.Collections.Generic;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe readonly struct EntityType
: IReadOnlyList<IdentifierRef>
{
public World World { get; }
public ecs_type_t* Handle { get; }
public EntityType(World world, ecs_type_t* handle)
{ World = world; Handle = handle; }
public override string ToString()
=> ecs_type_str(World, Handle).FlecsToStringAndFree()!;
// IReadOnlyList implementation
public int Count => Handle->count;
public IdentifierRef this[int index] => new(World, new(Handle->array[index]));
public IEnumerator<IdentifierRef> GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe sealed class Filter
: IDisposable
{
public World World { get; }
public ecs_filter_t* Handle { get; }
public Filter(World world, FilterDesc desc)
{
using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc);
World = world;
Handle = ecs_filter_init(world, &flecsDesc);
}
public void Dispose()
=> ecs_filter_fini(Handle);
public Iterator Iter()
=> new(World, IteratorType.Filter, ecs_filter_iter(World, this));
public override string ToString()
=> ecs_filter_str(World, Handle).FlecsToStringAndFree()!;
public static implicit operator ecs_filter_t*(Filter q) => q.Handle;
}
public class FilterDesc
{
public IReadOnlyList<Term> Terms { get; }
public string? Expression { get; }
/// <summary>
/// When true, terms returned by an iterator may either contain 1 or N
/// elements, where terms with N elements are owned, and terms with 1
/// element are shared, for example from a parent or base entity. When
/// false, the iterator will at most return 1 element when the result
/// contains both owned and shared terms.
/// </summary>
public bool Instanced { get; set; }
/// <summary>
/// Entity associated with query (optional).
/// </summary>
public Entity Entity { get; set; }
public FilterDesc(params Term[] terms)
=> Terms = terms;
public FilterDesc(string expression) : this()
=> Expression = expression;
public unsafe ecs_filter_desc_t ToFlecs(IAllocator allocator)
{
var desc = new ecs_filter_desc_t {
expr = allocator.AllocateCString(Expression),
instanced = Instanced,
entity = Entity,
};
var span = desc.terms;
if (Terms.Count > span.Length) {
span = allocator.Allocate<ecs_term_t>(Terms.Count);
desc.terms_buffer = (ecs_term_t*)Unsafe.AsPointer(ref span[0]);
desc.terms_buffer_count = Terms.Count;
}
for (var i = 0; i < Terms.Count; i++)
span[i] = Terms[i].ToFlecs(allocator);
return desc;
}
}

@ -0,0 +1,54 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public readonly struct Identifier
: IEquatable<Identifier>
{
public readonly ecs_id_t Value;
public bool IsPair => ecs_id_is_pair(this);
public bool IsWildcard => ecs_id_is_wildcard(this);
public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK);
public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 });
public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK });
public Identifier(ecs_id_t value) => Value = value;
public static Identifier Combine(IdentifierFlags flags, Identifier id)
=> new((ulong)flags | id.Value);
public static Identifier Pair(Entity relation, Entity target)
=> Combine(IdentifierFlags.Pair, new(
((relation.Value.Data << 32) & ECS_COMPONENT_MASK) |
( target.Value.Data & ECS_ENTITY_MASK )));
public (EntityRef Relation, EntityRef Target)? AsPair(World world)
=> new IdentifierRef(world, this).AsPair();
public bool Equals(Identifier other) => Value.Data == other.Value.Data;
public override bool Equals(object? obj) => (obj is Identifier other) && Equals(other);
public override int GetHashCode() => Value.Data.GetHashCode();
public override string? ToString()
=> (Flags != default) ? $"Identifier(0x{Value.Data:X}, Flags={Flags})"
: $"Identifier(0x{Value.Data:X})";
public static bool operator ==(Identifier left, Identifier right) => left.Equals(right);
public static bool operator !=(Identifier left, Identifier right) => !left.Equals(right);
public static implicit operator ecs_id_t(Identifier i) => i.Value;
}
[Flags]
public enum IdentifierFlags : ulong
{
Pair = 1ul << 63,
Override = 1ul << 62,
Toggle = 1ul << 61,
Or = 1ul << 60,
And = 1ul << 59,
Not = 1ul << 58,
}

@ -0,0 +1,46 @@
using System;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class IdentifierRef
: IEquatable<IdentifierRef>
{
public World World { get; }
public Identifier Id { get; }
public IdentifierFlags 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 IdentifierRef(World world, Identifier id)
{ World = world; Id = id; }
public static IdentifierRef Combine(IdentifierFlags flags, IdentifierRef id)
=> new(id.World, Identifier.Combine(flags, id));
public static IdentifierRef Pair(EntityRef relation, Entity target)
=> new(relation.World, Identifier.Pair(relation, target));
public static IdentifierRef Pair(Entity relation, EntityRef target)
=> new(target.World, Identifier.Pair(relation, target));
public EntityRef? AsEntity()
=> (Flags == default) ? World.Lookup(new Entity(new() { Data = Id })) : null;
public (EntityRef Relation, EntityRef Target)? AsPair()
=> IsPair && (World.Lookup(Id.RelationUnsafe) is EntityRef relation) &&
(World.Lookup(Id.TargetUnsafe ) is EntityRef target )
? (relation, target) : null;
public bool Equals(IdentifierRef? other) => (other is not null) && (World == other.World) && (Id == other.Id);
public override bool Equals(object? obj) => Equals(obj as IdentifierRef);
public override int GetHashCode() => HashCode.Combine(World, Id);
public override string? ToString() => ecs_id_str(World, this).FlecsToStringAndFree()!;
public static bool operator ==(IdentifierRef? left, IdentifierRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false);
public static bool operator !=(IdentifierRef? left, IdentifierRef? right) => !(left == right);
public static implicit operator Identifier(IdentifierRef i) => i.Id;
public static implicit operator ecs_id_t(IdentifierRef i) => i.Id.Value;
}

@ -0,0 +1,123 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class Iterator
: IEnumerable<Iterator>
, IDisposable
{
public World World { get; }
public IteratorType? Type { get; }
public ecs_iter_t Value;
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(World world, IteratorType? type, ecs_iter_t value)
{ World = world; Type = type; Value = value; }
public static Iterator 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 Iterator SetThis(Entity entity)
{
fixed (ecs_iter_t* ptr = &Value)
ecs_iter_set_var(ptr, 0, entity);
return this;
}
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 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 EntityRef Entity(int index)
=> new(World, new(Value.entities[index]));
public IdentifierRef FieldId(int index)
{
fixed (ecs_iter_t* ptr = &Value)
return new(World, new(ecs_field_id(ptr, index)));
}
public Span<T> Field<T>(int index)
where T : unmanaged
{
fixed (ecs_iter_t* ptr = &Value) {
var size = (ulong)Unsafe.SizeOf<T>();
var isSelf = ecs_field_is_self(ptr, index);
var pointer = ecs_field_w_size(ptr, size, index);
return new(pointer, isSelf ? Count : 1);
}
}
public Span<T> FieldOrEmpty<T>(int index)
where T : unmanaged => FieldIsSet(index) ? Field<T>(index) : default;
public SpanToRef<T> FieldRef<T>(int index)
where T : class => new(Field<nint>(index));
public bool FieldIsSet(int index)
{
fixed (ecs_iter_t* ptr = &Value)
return ecs_field_is_set(ptr, index);
}
public bool FieldIs<T>(int index)
{
fixed (ecs_iter_t* ptr = &Value) {
var id = ecs_field_id(ptr, index);
var comp = World.LookupOrThrow<T>();
return id == comp.Entity.Value.Data;
}
}
public override string ToString()
{
fixed (ecs_iter_t* ptr = &Value)
return ecs_iter_str(ptr).FlecsToStringAndFree()!;
}
// IEnumerable implementation
public IEnumerator<Iterator> GetEnumerator() { while (Next()) yield return this; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public enum IteratorType
{
Term,
Filter,
Query,
Rule,
}

@ -0,0 +1,37 @@
using System;
using System.Runtime.InteropServices;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public static class ObserverExtensions
{
public static unsafe EntityRef InitObserver(this EntityRef entity,
Entity @event, FilterDesc filter, Action<Iterator> callback)
{
var world = entity.World;
using var alloc = TempAllocator.Use();
var desc = new ecs_observer_desc_t {
entity = entity,
filter = filter.ToFlecs(alloc),
binding_ctx = (void*)CallbackContextHelper.Create((world, callback)),
binding_ctx_free = new() { Data = new() { Pointer = &FreeContext } },
callback = new() { Data = new() { Pointer = &Callback } },
};
desc.events[0] = @event;
return new(world, new(ecs_observer_init(world, &desc)));
}
[UnmanagedCallersOnly]
private static unsafe void Callback(ecs_iter_t* iter)
{
var (world, callback) = CallbackContextHelper
.Get<(World, Action<Iterator>)>((nint)iter->binding_ctx);
callback(new Iterator(world, null, *iter));
}
[UnmanagedCallersOnly]
private static unsafe void FreeContext(void* context)
=> CallbackContextHelper.Free((nint)context);
}

@ -0,0 +1,46 @@
using System;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe sealed class Query
: IDisposable
{
public World World { get; }
public ecs_query_t* Handle { get; }
public Query(World world, QueryDesc desc)
{
using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc);
World = world;
Handle = ecs_query_init(world, &flecsDesc);
}
public void Dispose()
=> ecs_query_fini(this);
public Iterator Iter()
=> new(World, IteratorType.Query, 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 class QueryDesc : FilterDesc
{
public QueryDesc(string expression) : base(expression) { }
public QueryDesc(params Term[] terms) : base(terms) { }
public new unsafe ecs_query_desc_t ToFlecs(IAllocator allocator)
{
var desc = new ecs_query_desc_t {
filter = base.ToFlecs(allocator),
// TODO: Implement more Query features.
};
return desc;
}
}

@ -0,0 +1,31 @@
using System;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe sealed class Rule
: IDisposable
{
public World World { get; }
public ecs_rule_t* Handle { get; }
public Rule(World world, FilterDesc desc)
{
using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc);
World = world;
Handle = ecs_rule_init(world, &flecsDesc);
}
public void Dispose()
=> ecs_rule_fini(this);
public Iterator Iter()
=> new(World, IteratorType.Rule, 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;
}

@ -0,0 +1,56 @@
using System;
using System.Runtime.InteropServices;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public static class SystemExtensions
{
public static unsafe EntityRef InitSystem(this EntityRef entity,
Entity phase, QueryDesc query, Action<Iterator> callback)
{
var world = entity.World;
entity.Add(world.DependsOn, phase);
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_free = new() { Data = new() { Pointer = &FreeContext } },
callback = new() { Data = new() { Pointer = &Callback } },
};
return new(world, new(ecs_system_init(world, &desc)));
}
[UnmanagedCallersOnly]
private static unsafe void Callback(ecs_iter_t* iter)
{
var (world, callback) = CallbackContextHelper
.Get<(World, Action<Iterator>)>((nint)iter->binding_ctx);
callback(new Iterator(world, null, *iter));
}
// [UnmanagedCallersOnly]
// private static unsafe void Run(ecs_iter_t* flecsIter)
// {
// var (world, callback) = CallbackContextHelper
// .Get<(World, Action<Iterator>)>((nint)flecsIter->binding_ctx);
// // This is what flecs does, so I guess we'll do it too!
// var type = (&flecsIter->next == (delegate*<ecs_iter_t*, Runtime.CBool>)&ecs_query_next)
// ? IteratorType.Query : (IteratorType?)null;
// using var iter = new Iterator(world, type, *flecsIter);
// If the method is marked with [Source], set the $This variable.
// if (Method.Get<SourceAttribute>()?.Type is Type sourceType)
// iter.SetThis(World.LookupOrThrow(sourceType));
// if (flecsIter->field_count == 0) callback(iter);
// else while (iter.Next()) callback(iter);
// }
[UnmanagedCallersOnly]
private static unsafe void FreeContext(void* context)