No more separators in EntityPath

- EntityPath now operates on an array of UTF8 strings
- EntityBuilder.Build, Lookup, ... use separator-less API
- Multiple TempAllocators can be acquired nested
- Add Identifier.RelationUnsafe and .TargetUnsafe
- Identifier(Ref).AsPair returns nullable
- Throw when adding > 31 IDs to EntityBuilder
- Add static Entity.None field, represents default value
- Add Entity.IsSome property, opposite of IsNone
- EntityBase.Add(string) now looks up symbol
wip/source-generators
copygirl 2 years ago
parent 9fdd28fe5a
commit 6e17a525cd
  1. 6
      src/gaemstone.Client/Resources.cs
  2. 5
      src/gaemstone/ECS/Entity.cs
  3. 6
      src/gaemstone/ECS/EntityBase.cs
  4. 49
      src/gaemstone/ECS/EntityBuilder.cs
  5. 216
      src/gaemstone/ECS/EntityPath.cs
  6. 7
      src/gaemstone/ECS/EntityRef.cs
  7. 2
      src/gaemstone/ECS/Filter.cs
  8. 2
      src/gaemstone/ECS/Game.cs
  9. 8
      src/gaemstone/ECS/Identifier.cs
  10. 5
      src/gaemstone/ECS/IdentifierRef.cs
  11. 2
      src/gaemstone/ECS/Iterator.cs
  12. 5
      src/gaemstone/ECS/Observer.cs
  13. 2
      src/gaemstone/ECS/Query.cs
  14. 2
      src/gaemstone/ECS/Rule.cs
  15. 9
      src/gaemstone/ECS/System.cs
  16. 4
      src/gaemstone/ECS/Universe+Lookup.cs
  17. 25
      src/gaemstone/ECS/Universe+Modules.cs
  18. 7
      src/gaemstone/ECS/Universe.cs
  19. 2
      src/gaemstone/Flecs/Systems/Monitor.cs
  20. 2
      src/gaemstone/Flecs/Systems/Rest.cs
  21. 65
      src/gaemstone/Utility/Allocators.cs
  22. 17
      src/gaemstone/Utility/CStringExtensions.cs

@ -40,14 +40,12 @@ public static class Resources
{
if (!path.IsAbsolute) throw new ArgumentException(
$"Path '{path}' must be absolute", nameof(path));
if (path.Depth < 2) throw new ArgumentException(
$"Path '{path}' must have at least a depth of 2", nameof(path));
if (path[1] != "Resources") throw new ArgumentException(
if (path.Count < 3 || path[1] != "Resources") throw new ArgumentException(
$"Path '{path}' must be in the format '/[domain]/Resources/...", nameof(path));
var assembly = Assembly.Load(path[0].ToString());
var builder = new StringBuilder(path[2]);
for (var i = 3; i < path.Depth + 1; i++)
for (var i = 3; i < path.Count; i++)
builder.Append('.').Append(path[i]);
return (assembly, builder.ToString());
}

@ -17,11 +17,12 @@ public class RelationAttribute : Attribute { }
public readonly struct Entity
: IEquatable<Entity>
{
public static readonly Entity None = default;
public readonly ecs_entity_t Value;
public bool IsSome => Value.Data != 0;
public bool IsNone => Value.Data == 0;
public Entity ThrowIfNone() => !IsNone ? this
: throw new InvalidOperationException(this + " is none");
public Entity(ecs_entity_t value) => Value = value;

@ -17,19 +17,19 @@ public abstract class EntityBase<TReturn>
public abstract TReturn Set<T>(in T value) where T : unmanaged;
public abstract TReturn Set<T>(T obj) where T : class;
public TReturn Add(string name) => Add(Universe.LookupOrThrow(name));
public TReturn Add(string symbol) => Add(Universe.LookupSymbolOrThrow(symbol));
public TReturn Add<T>() => Add(Universe.LookupOrThrow(typeof(T)));
public TReturn Add(Entity relation, Entity target) => Add(relation & target);
public TReturn Add<TRelation>(Entity target) => Add(Universe.LookupOrThrow<TRelation>(), target);
public TReturn Add<TRelation, TTarget>() => Add(Universe.LookupOrThrow<TRelation>(), Universe.LookupOrThrow<TTarget>());
public TReturn Remove(string name) => Remove(Universe.LookupOrThrow(name));
public TReturn Remove(string symbol) => Remove(Universe.LookupSymbolOrThrow(symbol));
public TReturn Remove<T>() => Remove(Universe.LookupOrThrow(typeof(T)));
public TReturn Remove(Entity relation, Entity target) => Remove(relation & target);
public TReturn Remove<TRelation>(Entity target) => Remove(Universe.LookupOrThrow<TRelation>(), target);
public TReturn Remove<TRelation, TTarget>() => Remove(Universe.LookupOrThrow<TRelation>(), Universe.LookupOrThrow<TTarget>());
public bool Has(string name) => Has(Universe.LookupOrThrow(name));
public bool Has(string symbol) => Has(Universe.LookupSymbolOrThrow(symbol));
public bool Has<T>() => Has(Universe.LookupOrThrow(typeof(T)));
public bool Has(Entity relation, Entity target) => Has(relation & target);
public bool Has<TRelation>(Entity target) => Has(Universe.LookupOrThrow<TRelation>(), target);

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using gaemstone.Utility;
using static flecs_hub.flecs;
using static flecs_hub.flecs.Runtime;
namespace gaemstone.ECS;
@ -38,6 +39,7 @@ public class EntityBuilder
/// <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; }
@ -48,9 +50,23 @@ public class EntityBuilder
public EntityBuilder(Universe universe, EntityPath? path = null)
{ Universe = universe; Path = path; }
public override EntityBuilder Add(Identifier id) { _toAdd.Add(id); return this; }
public override EntityBuilder Remove(Identifier id) => throw new NotSupportedException();
public override bool Has(Identifier id) => !ecs_id_is_wildcard(id) ? _toAdd.Contains(id) : throw new NotSupportedException();
public override EntityBuilder Add(Identifier id)
{
// If adding a ChildOf relation, store the parent separately.
if (id.AsPair(Universe) is (EntityRef relation, EntityRef target) &&
relation == Universe.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)
=> !ecs_id_is_wildcard(id) ? _toAdd.Contains(id)
: throw new NotSupportedException(); // TODO: Support wildcard.
public override T Get<T>() => throw new NotSupportedException();
public override ref T GetRef<T>() => throw new NotSupportedException();
@ -63,23 +79,36 @@ public class EntityBuilder
public override EntityBuilder Set<T>(T obj)
{ _toSet.Add(e => e.Set(obj)); return this; }
public static CString ETX { get; } = (CString)"\x3";
public unsafe EntityRef Build()
{
using var alloc = TempAllocator.Lock();
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(Universe, parent, Path.Parent!);
}
using var alloc = TempAllocator.Use();
var desc = new ecs_entity_desc_t {
id = ID,
name = (Path != null) ? alloc.AllocateCString(Path.AsSpan(true)) : default,
symbol = alloc.AllocateCString(_symbol),
add_expr = alloc.AllocateCString(Expression),
id = ID,
name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default,
symbol = alloc.AllocateCString(_symbol),
add_expr = alloc.AllocateCString(Expression),
use_low_id = UseLowID,
root_sep = EntityPath.SeparatorAsCString,
sep = EntityPath.SeparatorAsCString,
sep = ETX, // TODO: Replace with CStringExtensions.Empty once supported.
};
var add = desc.add; var index = 0;
if (parent.IsSome) add[index++] = Identifier.Pair(Universe.ChildOf, parent);
foreach (var id in _toAdd) add[index++] = id;
var entityID = ecs_entity_init(Universe, &desc);
var entity = new EntityRef(Universe, new(entityID));
foreach (var action in _toSet) action(entity);
return entity;
}
}

@ -1,136 +1,92 @@
using System;
using System.Buffers;
using System.Globalization;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using gaemstone.Utility;
using static flecs_hub.flecs;
using static flecs_hub.flecs.Runtime;
namespace gaemstone.ECS;
public class EntityPath
{
public const int MaxDepth = 32;
public const char Separator = '/';
private readonly byte[][] _parts;
internal const byte SeparatorAsByte = (byte)Separator;
internal static readonly CString SeparatorAsCString = (CString)Separator.ToString();
private static readonly ThreadLocal<(int Start, int End)[]> PartsCache
= new(() => new (int, int)[MaxDepth]);
private readonly byte[] _bytes;
private readonly (int Start, int End)[] _parts;
public int Depth => _parts.Length - 1;
public bool IsAbsolute => _bytes[0] == SeparatorAsByte;
public bool IsAbsolute { get; }
public bool IsRelative => !IsAbsolute;
public int Count => _parts.Length;
public UTF8View this[int index] { get {
if (index < 0 || index > Depth)
throw new ArgumentOutOfRangeException(nameof(index));
var (start, end) = _parts[index];
return new(_bytes.AsSpan()[start..end]);
} }
public UTF8View Name => this[^1];
public EntityPath? Parent => (Count > 1) ? new(IsAbsolute, _parts[..^1]) : null;
public unsafe EntityPath(byte* pointer)
: this(ArrayFromPointer(pointer)) { }
private static unsafe byte[] ArrayFromPointer(byte* pointer)
public UTF8View this[int index]
=> (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1])
: throw new ArgumentOutOfRangeException(nameof(index));
internal EntityPath(bool absolute, params byte[][] parts)
{
var length = 0;
while (true) if (pointer[length++] == 0) break;
return new Span<byte>(pointer, length).ToArray();
if (parts.Length == 0) throw new ArgumentException(
"Must have at least one part", nameof(parts));
IsAbsolute = absolute;
_parts = parts;
}
// TODO: public EntityPath(EntityPath @base, params string[] parts) { }
public EntityPath(params string[] parts)
: this(ConcatParts(false, parts)) { }
: this(false, parts) { }
public EntityPath(bool absolute, params string[] parts)
: this(ConcatParts(absolute, parts)) { }
private static byte[] ConcatParts(bool absolute, string[] parts)
: this(absolute, parts.Select(part => {
ValidateName(part);
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 EntityPath Parse(string str)
{
// number of slashes + NUL
var totalBytes = (parts.Length - 1) + 1;
// If absolute, and parts doesn't already start with a slash, increase length by 1.
var prependSlash = absolute && parts[0].Length > 0 && parts[0][0] != Separator;
if (prependSlash) totalBytes++;
foreach (var part in parts)
totalBytes += Encoding.UTF8.GetByteCount(part);
var bytes = new byte[totalBytes];
var index = 0;
foreach (var part in parts) {
if (index > 0 || prependSlash) bytes[index++] = SeparatorAsByte;
index += Encoding.UTF8.GetBytes(part, 0, part.Length, bytes, index);
}
// NUL byte at the end of bytes.
// bytes[index++] = 0;
return bytes;
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);
}
private EntityPath(byte[] bytes)
public static void ValidateName(string name)
{
if (bytes.Length <= 1) throw new ArgumentException("Must not be empty");
if (bytes[^1] != 0) throw new ArgumentException("Must end with a NUL character");
_bytes = bytes;
var depth = 0;
var index = 0;
var partStart = 0;
var partsCache = PartsCache.Value!;
// If path starts with separator, it's an absolute path. Skip first byte.
if (_bytes[0] == SeparatorAsByte) index = partStart = 1;
// -1 is used here because we don't want to include the NUL character.
while (index < _bytes.Length - 1) {
if (_bytes[index] == SeparatorAsByte) {
// +1 is used here because one more part will follow after the loop.
if (depth + 1 >= MaxDepth) throw new ArgumentException(
$"Must not exceed maximum depth of {MaxDepth}");
partsCache[depth++] = (Start: partStart, End: index);
ValidatePart(_bytes.AsSpan()[partStart..index]);
partStart = ++index;
} else {
var slice = _bytes.AsSpan()[index..];
if (Rune.DecodeFromUtf8(slice, out var rune, out var consumed) != OperationStatus.Done)
throw new ArgumentException("Contains invalid UTF8");
ValidateRune(rune);
index += consumed;
}
}
if (name.Length == 0) throw new ArgumentException(
"Must not be empty");
partsCache[depth] = (Start: partStart, End: index);
ValidatePart(_bytes.AsSpan()[partStart..^1]);
// Copy parts from the thread local cache - this avoids unnecessary resizing.
_parts = partsCache[..(depth + 1)];
}
private static void ValidatePart(ReadOnlySpan<byte> part)
{
if (part.Length == 0) throw new ArgumentException(
"Must not contain empty parts");
// NOTE: This is a hopefully straightforward way to also prevent "."
// and ".." to be part of paths which may access the file system.
if (part[0] == (byte)'.') throw new ArgumentException(
"Must not contain parts that start with a dot");
if (name[0] == '.') throw new ArgumentException(
"Must not begin with a dot");
foreach (var chr in name) if (char.IsControl(chr))
throw new ArgumentException("Must not contain contol characters");
}
private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' };
private static readonly UnicodeCategory[] _validCategories = {
UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter,
UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber };
// 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");
// }
private static void ValidateRune(Rune rune)
public override string ToString()
{
if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune)))
throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character");
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
{
@ -138,32 +94,58 @@ public class EntityPath
private int index = -1;
public UTF8View Current => _path[index];
internal Enumerator(EntityPath path) => _path = path;
public bool MoveNext() => (++index >= _path.Depth);
public bool MoveNext() => (++index < _path.Count);
}
public ReadOnlySpan<byte> AsSpan(bool includeNul = false)
=> includeNul ? _bytes.AsSpan() : _bytes.AsSpan()[..^1];
public override string ToString() => Encoding.UTF8.GetString(AsSpan());
public static implicit operator string(EntityPath view) => view.ToString();
public static implicit operator EntityPath(string str) => new(str);
internal static unsafe Entity Lookup(Universe universe, 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(universe));
foreach (var part in path)
fixed (byte* ptr = part.AsSpan())
if ((parent = new(ecs_lookup_child(universe, parent, ptr))).IsNone)
return Entity.None;
return parent;
}
/// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary>
internal static unsafe Entity EnsureEntityExists(
Universe universe, 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(universe));
var skipLookup = parent.IsNone;
foreach (var part in path)
fixed (byte* ptr = part.AsSpan())
if (skipLookup || (parent = new(ecs_lookup_child(universe, parent, ptr))).IsNone) {
var desc = new ecs_entity_desc_t { name = ptr };
if (parent.IsSome) desc.add[0] = Identifier.Pair(universe.ChildOf, parent);
parent = new(ecs_entity_init(universe, &desc));
skipLookup = true;
}
return parent;
}
}
public static class EntityPathExtensions
{
public static unsafe EntityPath GetFullPath(this EntityRef entity)
{
var cStr = ecs_get_path_w_sep(entity.Universe, default, entity,
EntityPath.SeparatorAsCString, EntityPath.SeparatorAsCString);
try { return new((byte*)(nint)cStr); }
finally { cStr.FlecsFree(); }
}
var current = (Entity)entity;
var parts = new List<byte[]>(32);
public static unsafe Entity Lookup(Universe universe, Entity parent, EntityPath path)
{
using var alloc = TempAllocator.Lock();
return new(ecs_lookup_path_w_sep(universe, parent, alloc.AllocateCString(path.AsSpan(true)),
EntityPath.SeparatorAsCString, EntityPath.SeparatorAsCString, true));
do { parts.Add(ecs_get_name(entity.Universe, current).FlecsToBytes()!); }
while ((current = new(ecs_get_target(entity.Universe, current, EcsChildOf, 0))).IsSome);
parts.Reverse();
return new(true, parts.ToArray());
}
}
@ -176,8 +158,8 @@ public readonly ref struct UTF8View
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 ReadOnlySpan<byte>(UTF8View view) => view._bytes;
public static implicit operator string(UTF8View view) => view.ToString();
public Enumerator GetEnumerator() => new(_bytes);

@ -19,11 +19,11 @@ public unsafe sealed class EntityRef
public string? Name {
get => ecs_get_name(Universe, this).FlecsToString()!;
set { using var alloc = TempAllocator.Lock(); ecs_set_name(Universe, this, alloc.AllocateCString(value)); }
set { using var alloc = TempAllocator.Use(); ecs_set_name(Universe, this, alloc.AllocateCString(value)); }
}
public string? Symbol {
get => ecs_get_symbol(Universe, this).FlecsToString()!;
set { using var alloc = TempAllocator.Lock(); ecs_set_symbol(Universe, this, alloc.AllocateCString(value)); }
set { using var alloc = TempAllocator.Use(); ecs_set_symbol(Universe, this, alloc.AllocateCString(value)); }
}
// TODO: public IEnumerable<Entity> Children => ...
@ -36,7 +36,8 @@ public unsafe sealed class EntityRef
$"The entity {entity} is not valid");
}
public void Delete() => ecs_delete(Universe, this);
public void Delete()
=> ecs_delete(Universe, this);
public EntityBuilder NewChild(EntityPath? path = null)
=> Universe.New(EnsureRelativePath(path)).ChildOf(this);

@ -18,7 +18,7 @@ public unsafe sealed class Filter
public Filter(Universe universe, FilterDesc desc)
{
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc);
Universe = universe;
Handle = ecs_filter_init(universe, &flecsDesc);

@ -6,7 +6,7 @@ namespace gaemstone.ECS;
/// Entity for storing global game state and configuration.
/// Parameters can use <see cref="GameAttribute"/> to source this entity.
/// </summary>
[Entity]
[Entity, Tag]
public struct Game { }
/// <summary> Short for <c>[Source(typeof(Game))]</c>. </summary>

@ -8,20 +8,24 @@ public readonly struct Identifier
{
public readonly ecs_id_t Value;
public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK);
public bool IsPair => ecs_id_is_pair(Value);
public bool IsWildcard => ecs_id_is_wildcard(Value);
public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 });
public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK });
public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_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) |
(target.Value.Data & ECS_ENTITY_MASK)));
public (EntityRef Relation, EntityRef Target) AsPair(Universe universe)
public (EntityRef Relation, EntityRef Target)? AsPair(Universe universe)
=> new IdentifierRef(universe, this).AsPair();
public bool Equals(Identifier other) => Value.Data == other.Value.Data;

@ -25,9 +25,8 @@ public unsafe class IdentifierRef
public static IdentifierRef Pair(Entity relation, EntityRef target)
=> new(target.Universe, Identifier.Pair(relation, target));
public (EntityRef Relation, EntityRef Target) AsPair()
=> (Universe.LookupOrThrow(new Entity(new() { Data = (ID.Value & ECS_COMPONENT_MASK) >> 32 })),
Universe.LookupOrThrow(new Entity(new() { Data = ID.Value & ECS_ENTITY_MASK })));
public (EntityRef Relation, EntityRef Target)? AsPair()
=> IsPair ? (Universe.LookupOrThrow(ID.RelationUnsafe), Universe.LookupOrThrow(ID.TargetUnsafe)) : null;
public bool Equals(IdentifierRef? other) => (other is not null) && Universe == other.Universe && ID == other.ID;
public override bool Equals(object? obj) => Equals(obj as IdentifierRef);

@ -25,7 +25,7 @@ public unsafe partial class Iterator
public static Iterator FromTerm(Universe universe, Term term)
{
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
var flecsTerm = term.ToFlecs(alloc);
var flecsIter = ecs_term_iter(universe, &flecsTerm);
return new(universe, IteratorType.Term, flecsIter);

@ -21,11 +21,10 @@ public static class ObserverExtensions
public static unsafe EntityRef RegisterObserver(this Universe universe,
FilterDesc filter, Entity @event, Action<Iterator> callback)
{
var entity = universe.New((filter.Name != null) ? new(filter.Name) : null).Build();
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
var desc = new ecs_observer_desc_t {
filter = filter.ToFlecs(alloc),
entity = entity,
entity = universe.New((filter.Name != null) ? new(filter.Name) : null).Build(),
binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)),
callback = new() { Data = new() { Pointer = &SystemExtensions.Callback } },
};

@ -15,7 +15,7 @@ public unsafe sealed class Query
public Query(Universe universe, QueryDesc desc)
{
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc);
Universe = universe;
Handle = ecs_query_init(universe, &flecsDesc);

@ -15,7 +15,7 @@ public unsafe sealed class Rule
public Rule(Universe universe, FilterDesc desc)
{
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc);
Universe = universe;
Handle = ecs_rule_init(universe, &flecsDesc);

@ -25,14 +25,11 @@ public static class SystemExtensions
public static unsafe EntityRef RegisterSystem(this Universe universe,
QueryDesc query, Entity phase, Action<Iterator> callback)
{
var entity = universe.New((query.Name != null) ? new(query.Name) : null)
.Add<DependsOn>(phase)
.Add(phase)
.Build();
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
var desc = new ecs_system_desc_t {
query = query.ToFlecs(alloc),
entity = entity,
entity = universe.New((query.Name != null) ? new(query.Name) : null)
.Add<DependsOn>(phase).Add(phase).Build(),
binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)),
callback = new() { Data = new() { Pointer = &Callback } },
};

@ -30,10 +30,10 @@ public unsafe partial class Universe
public EntityRef? Lookup(EntityPath path)
=> Lookup(default, path);
public EntityRef? Lookup(Entity parent, EntityPath path)
=> GetOrNull(EntityPathExtensions.Lookup(this, parent, path));
=> GetOrNull(EntityPath.Lookup(this, parent, path));
public EntityRef? LookupSymbol(string symbol)
{
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
return GetOrNull(new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false)));
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using gaemstone.Utility;
using static gaemstone.Flecs.Core;
@ -34,8 +35,9 @@ public class ModuleManager
if (attr.Path == null) throw new Exception(
$"Existing module {type} must have ModuleAttribute.Name set");
var entity = Universe.Lookup(new EntityPath(true, attr.Path)) ?? throw new Exception(
$"Existing module {type} with name '{attr.Path}' not found");
var path = new EntityPath(true, attr.Path);
var entity = Universe.Lookup(path) ?? throw new Exception(
$"Existing module {type} with name '{path}' not found");
// This implementation is pretty naive. It simply gets all nested
// types which are tagged with [Entity] attribute or a subtype
@ -88,11 +90,13 @@ public class ModuleManager
// If path is not specified in the attribute, return the type's name.
if (attr.Path == null) {
var assemblyName = type.Assembly.GetName().Name!;
if (!type.FullName!.StartsWith(assemblyName + '.')) throw new InvalidOperationException(
$"Module {type} must be defined in namespace {assemblyName}");
// Strip assembly name from FullName and replace dots in namespace with path separators.
var path = type.FullName![(assemblyName.Length + 1)..].Replace('.', EntityPath.Separator);
return new(true, assemblyName, path);
$"Module {type} must be defined under namespace {assemblyName}");
var fullNameWithoutAssembly = type.FullName![(assemblyName.Length + 1)..];
var parts = fullNameWithoutAssembly.Split('.');
return new(true, parts.Prepend(assemblyName).ToArray());
}
var fullPath = new EntityPath(true, attr.Path);
@ -157,8 +161,13 @@ internal class ModuleInfo
{
foreach (var type in Type.GetNestedTypes()) {
if (type.Get<EntityAttribute>() is not EntityAttribute attr) continue;
if (attr.Name?.Contains(EntityPath.Separator) == true) throw new Exception(
$"{type} must not contain '{EntityPath.Separator}'");
if (attr.Name != null) {
try { EntityPath.ValidateName(attr.Name); }
catch (Exception ex) { throw new Exception(
$"{type} has invalid entity name '{attr.Name}: {ex.Message}'", ex); }
}
var name = attr.Name ?? type.Name;
var entity = Entity.NewChild(name).Symbol(name);
switch (attr) {

@ -8,6 +8,9 @@ public unsafe partial class Universe
public ecs_world_t* Handle { get; }
public ModuleManager Modules { get; }
// flecs built-ins that are important to internals.
public EntityRef ChildOf { get; }
public bool IsDeferred => ecs_is_deferred(this);
public Universe(params string[] args)
@ -19,7 +22,9 @@ public unsafe partial class Universe
Modules.Register(typeof(Flecs.ObserverEvent));
Modules.Register(typeof(Flecs.SystemPhase));
New("Game").Symbol("Game").Build().CreateLookup<Game>();
ChildOf = LookupOrThrow<Flecs.Core.ChildOf>();
New("Game").Symbol("Game").Build().CreateLookup<Game>().Add<Game>();
}
public EntityBuilder New(EntityPath? path = null)

@ -11,7 +11,7 @@ public unsafe class Monitor
{
public void Initialize(EntityRef module)
{
using var alloc = TempAllocator.Lock();
using var alloc = TempAllocator.Use();
ecs_import_c(module.Universe, new() { Data = new() {
Pointer = &MonitorImport } }, alloc.AllocateCString("FlecsMonitor"));
}

@ -11,7 +11,7 @@ public unsafe class Rest
{
public void Initialize(EntityRef module)
{
using (var alloc = TempAllocator.Lock())
using (var alloc = TempAllocator.Use())
ecs_import_c(module.Universe, new() { Data = new() {
Pointer = &RestImport } }, alloc.AllocateCString("FlecsRest"));

@ -21,7 +21,7 @@ public unsafe static class AllocatorExtensions
=> allocator.Free((nint)Unsafe.AsPointer(ref span[0]));
public static Span<T> AllocateCopy<T>(this IAllocator allocator, ReadOnlySpan<T> orig) where T : unmanaged
{ var span = allocator.Allocate<T>(orig.Length); orig.CopyTo(span); return span; }
{ var copy = allocator.Allocate<T>(orig.Length); orig.CopyTo(copy); return copy; }
public static ref T Allocate<T>(this IAllocator allocator) where T : unmanaged
=> ref Unsafe.AsRef<T>((void*)allocator.Allocate(sizeof(T)));
@ -34,35 +34,48 @@ public unsafe static class AllocatorExtensions
var bytes = Encoding.UTF8.GetByteCount(value);
var span = allocator.Allocate<byte>(bytes + 1);
Encoding.UTF8.GetBytes(value, span);
span[^1] = 0;
span[^1] = 0; // In case the allocated span is not cleared.
return new((nint)Unsafe.AsPointer(ref span[0]));
}
public static CString AllocateCString(this IAllocator allocator, ReadOnlySpan<byte> utf8)
=> new((nint)Unsafe.AsPointer(ref allocator.AllocateCopy(utf8)[0]));
{
var copy = allocator.Allocate<byte>(utf8.Length + 1);
utf8.CopyTo(copy);
copy[^1] = 0; // In case the allocated span is not cleared.
return new((nint)Unsafe.AsPointer(ref copy[0]));
}
}
public sealed class TempAllocator
: IAllocator
, IDisposable
public static class TempAllocator
{
public const int Capacity = 1024 * 1024; // 1 MB
private static readonly ThreadLocal<TempAllocator> _tempAllocator = new(() => new());
public static TempAllocator Lock()
private static readonly ThreadLocal<ArenaAllocator> _allocator
= new(() => new(Capacity));
public static ResetOnDispose Use()
{
var allocator = _tempAllocator.Value!;
if (allocator._isInUse) throw new InvalidOperationException(
"This thread's TempAllocator is already in use. Previous caller to Lock() must first call Dispose().");
allocator._isInUse = true;
return allocator;
var allocator = _allocator.Value!;
return new(allocator, allocator.Used);
}
private readonly ArenaAllocator _allocator = new(Capacity);
private bool _isInUse = false;
public sealed class ResetOnDispose
: IAllocator
, IDisposable
{
private readonly ArenaAllocator _allocator = new(Capacity);
private readonly int _start;
public nint Allocate(int byteCount) => _allocator.Allocate(byteCount);
public void Free(nint pointer) { /* Do nothing. */ }
public void Dispose() { _allocator.Reset(); _isInUse = false; }
public ResetOnDispose(ArenaAllocator allocator, int start)
{ _allocator = allocator; _start = start; }
// IAllocator implementation
public nint Allocate(int byteCount) => _allocator.Allocate(byteCount);
public void Free(nint pointer) { /* Do nothing. */ }
// IDisposable implementation
public void Dispose() => _allocator.Reset(_start);
}
}
public class GlobalHeapAllocator
@ -85,14 +98,14 @@ public sealed class ArenaAllocator
public ArenaAllocator(int capacity)
{
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
_buffer = Marshal.AllocHGlobal(capacity);
_buffer = Marshal.AllocHGlobal(capacity);
Capacity = capacity;
}
public void Dispose()
{
Marshal.FreeHGlobal(_buffer);
_buffer = default;
_buffer = default;
Capacity = 0;
}
@ -109,8 +122,12 @@ public sealed class ArenaAllocator
public void Free(nint pointer)
{ /* Do nothing. */ }
public void Reset()
=> Used = 0;
public unsafe void Reset(int start = 0)
{
if (start > Used) throw new ArgumentOutOfRangeException(nameof(start));
new Span<byte>((void*)(_buffer + start), Used - start).Clear();
Used = start;
}
}
public sealed class RingAllocator
@ -124,14 +141,14 @@ public sealed class RingAllocator
public RingAllocator(int capacity)
{
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
_buffer = Marshal.AllocHGlobal(capacity);
_buffer = Marshal.AllocHGlobal(capacity);
Capacity = capacity;
}
public void Dispose()
{
Marshal.FreeHGlobal(_buffer);
_buffer = default;
_buffer = default;
Capacity = 0;
}

@ -1,3 +1,4 @@
using System;
using System.Runtime.InteropServices;
using static flecs_hub.flecs;
using static flecs_hub.flecs.Runtime;
@ -6,6 +7,20 @@ namespace gaemstone.Utility;
public unsafe static class CStringExtensions
{
public static CString Empty { get; } = (CString)"";
public static unsafe byte[]? FlecsToBytes(this CString str)
{
if (str.IsNull) return null;
var length = 0;
var pointer = (byte*)(nint)str;
while (true) if (pointer[length++] == 0) break;
return new Span<byte>(pointer, length).ToArray();
}
public static byte[]? FlecsToBytesAndFree(this CString str)
{ var result = str.FlecsToBytes(); str.FlecsFree(); return result; }
public static string? FlecsToString(this CString str)
=> Marshal.PtrToStringUTF8(str);
@ -13,5 +28,5 @@ public unsafe static class CStringExtensions
{ var result = str.FlecsToString(); str.FlecsFree(); return result; }
public static void FlecsFree(this CString str)
=> ecs_os_get_api().free_.Data.Pointer((void*)(nint)str);
{ if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); }
}

Loading…
Cancel
Save