Split gaemstone.ECS into its own project For older history, see copygirl/gaemstone@0c6d63af21d086f71d1c46acda5ee4c1d0220914wip/bindgen
commit
127e957c97
30 changed files with 1825 additions and 0 deletions
@ -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 |
@ -0,0 +1,2 @@ |
||||
**/obj/ |
||||
**/bin/ |
@ -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) |
||||
=> CallbackContextHelper.Free((nint)context); |
||||
} |
@ -0,0 +1,116 @@ |
||||
using System; |
||||
using gaemstone.Utility; |
||||
using static flecs_hub.flecs; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
public class Term |
||||
{ |
||||
public Identifier Id { get; set; } |
||||
public TermId? Source { get; set; } |
||||
public TermId? Relation { get; set; } |
||||
public TermId? Target { get; set; } |
||||
public TermInOutKind InOut { get; set; } |
||||
public TermOperKind Oper { get; set; } |
||||
public IdentifierFlags Flags { get; set; } |
||||
|
||||
public Term() { } |
||||
public Term(Identifier id) => Id = id; |
||||
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(IdentifierRef id) => new(id); |
||||
public static implicit operator Term(Identifier id) => new(id); |
||||
|
||||
public Term None { get { InOut = TermInOutKind.None; return this; } } |
||||
public Term In { get { InOut = TermInOutKind.In; return this; } } |
||||
public Term Out { get { InOut = TermInOutKind.Out; return this; } } |
||||
|
||||
public Term Or { get { Oper = TermOperKind.Or; return this; } } |
||||
public Term Not { get { Oper = TermOperKind.Not; return this; } } |
||||
public Term Optional { get { Oper = TermOperKind.Optional; return this; } } |
||||
|
||||
public ecs_term_t ToFlecs(IAllocator allocator) => new() { |
||||
id = Id, |
||||
src = Source?.ToFlecs(allocator) ?? default, |
||||
first = Relation?.ToFlecs(allocator) ?? default, |
||||
second = Target?.ToFlecs(allocator) ?? default, |
||||
inout = (ecs_inout_kind_t)InOut, |
||||
oper = (ecs_oper_kind_t)Oper, |
||||
id_flags = (ecs_id_t)(ulong)Flags, |
||||
}; |
||||
} |
||||
|
||||
public enum TermInOutKind |
||||
{ |
||||
Default = ecs_inout_kind_t.EcsInOutDefault, |
||||
None = ecs_inout_kind_t.EcsInOutNone, |
||||
InOut = ecs_inout_kind_t.EcsInOut, |
||||
In = ecs_inout_kind_t.EcsIn, |
||||
Out = ecs_inout_kind_t.EcsOut, |
||||
} |
||||
|
||||
public enum TermOperKind |
||||
{ |
||||
And = ecs_oper_kind_t.EcsAnd, |
||||
Or = ecs_oper_kind_t.EcsOr, |
||||
Not = ecs_oper_kind_t.EcsNot, |
||||
Optional = ecs_oper_kind_t.EcsOptional, |
||||
AndFrom = ecs_oper_kind_t.EcsAndFrom, |
||||
OrFrom = ecs_oper_kind_t.EcsOrFrom, |
||||
NotFrom = ecs_oper_kind_t.EcsNotFrom, |
||||
} |
||||
|
||||
public class TermId |
||||
{ |
||||
public static TermId This { get; } = new("$This"); |
||||
|
||||
public Entity Id { get; } |
||||
public string? Name { get; } |
||||
public Entity Traverse { get; set; } |
||||
public TermTraversalFlags Flags { get; set; } |
||||
|
||||
public TermId(Entity id) |
||||
=> Id = id; |
||||
public TermId(string name) |
||||
{ |
||||
if (name[0] == '$') { |
||||
Name = name[1..]; |
||||
Flags = TermTraversalFlags.IsVariable; |
||||
} 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), |
||||
trav = Traverse, |
||||
flags = (ecs_flags32_t)(uint)Flags |
||||
}; |
||||
} |
||||
|
||||
[Flags] |
||||
public enum TermTraversalFlags : uint |
||||
{ |
||||
/// <summary> Match on self. </summary> |
||||
Self = EcsSelf, |
||||
/// <summary> Match by traversing upwards. </summary> |
||||
Up = EcsUp, |
||||
/// <summary> Match by traversing downwards (derived, cannot be set). </summary> |
||||
Down = EcsDown, |
||||
/// <summary> Sort results breadth first. </summary> |
||||
Cascade = EcsCascade, |
||||
/// <summary> Short for up(ChildOf). </summary> |
||||
Parent = EcsParent, |
||||
/// <summary> Term id is a variable. </summary> |
||||
IsVariable = EcsIsVariable, |
||||
/// <summary> Term id is an entity. </summary> |
||||
IsEntity = EcsIsEntity, |
||||
/// <summary> Prevent observer from triggering on term. </summary> |
||||
Filter = EcsFilter, |
||||
} |
@ -0,0 +1,67 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using gaemstone.Utility; |
||||
using static flecs_hub.flecs; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
public unsafe partial class World |
||||
{ |
||||
private readonly Dictionary<Type, Entity> _lookupByType = new(); |
||||
|
||||
public void AddLookupByType(Type type, Entity entity) |
||||
{ |
||||
// If an existing lookup already exists with the same entity, don't throw an exception. |
||||
if (_lookupByType.TryGetValue(type, out var existing) && (existing == entity)) return; |
||||
_lookupByType.Add(type, entity); |
||||
} |
||||
public void RemoveLookupByType(Type type) |
||||
{ if (!_lookupByType.Remove(type)) throw new InvalidOperationException( |
||||
$"Lookup for {type} does not exist"); } |
||||
|
||||
|
||||
private EntityRef? CreateOrNull(Entity entity) |
||||
=> EntityRef.CreateOrNull(this, entity); |
||||
|
||||
public EntityRef? Lookup<T>() |
||||
=> Lookup(typeof(T)); |
||||
public EntityRef? Lookup(Type type) |
||||
=> Lookup(_lookupByType.GetValueOrDefault(type)); |
||||
|
||||
public EntityRef? Lookup(Entity value) |
||||
=> CreateOrNull(new(ecs_get_alive(this, value))); |
||||
|
||||
public EntityRef? Lookup(EntityPath path) |
||||
=> Lookup(default, path); |
||||
public EntityRef? Lookup(Entity parent, EntityPath path) |
||||
=> CreateOrNull(EntityPath.Lookup(this, parent, path)); |
||||
public EntityRef? LookupSymbol(string symbol) |
||||
{ |
||||
using var alloc = TempAllocator.Use(); |
||||
return CreateOrNull(new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false))); |
||||
} |
||||
|
||||
public EntityRef LookupOrThrow<T>() => LookupOrThrow(typeof(T)); |
||||
public EntityRef LookupOrThrow(Type type) => Lookup(type) |
||||
?? throw new EntityNotFoundException($"Entity of type {type} not found"); |
||||
public EntityRef LookupOrThrow(Entity entity) => Lookup(entity) |
||||
?? throw new EntityNotFoundException($"Entity {entity} not alive"); |
||||
public EntityRef LookupOrThrow(EntityPath path) => Lookup(default, path) |
||||
?? throw new EntityNotFoundException($"Entity '{path}' not found"); |
||||
public EntityRef LookupOrThrow(Entity parent, EntityPath path) => Lookup(parent, path) |
||||
?? throw new EntityNotFoundException($"Child entity of {parent} '{path}' not found"); |
||||
public EntityRef LookupSymbolOrThrow(string symbol) => LookupSymbol(symbol) |
||||
?? throw new EntityNotFoundException($"Entity with symbol '{symbol}' not found"); |
||||
|
||||
|
||||
public class EntityNotFoundException : Exception |
||||
{ public EntityNotFoundException(string message) : base(message) { } } |
||||
} |
||||
|
||||
public static class LookupExtensions |
||||
{ |
||||
public static EntityRef CreateLookup<T>(this EntityRef entity) |
||||
=> entity.CreateLookup(typeof(T)); |
||||
public static EntityRef CreateLookup(this EntityRef entity, Type type) |
||||
{ entity.World.AddLookupByType(type, entity); return entity; } |
||||
} |
@ -0,0 +1,40 @@ |
||||
using System; |
||||
using static flecs_hub.flecs; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
public unsafe partial class World |
||||
{ |
||||
public ecs_world_t* Handle { get; } |
||||
|
||||
// Flecs built-ins that are important to internals. |
||||
internal EntityRef ChildOf { get; } |
||||
internal EntityRef Disabled { get; } |
||||
internal EntityRef DependsOn { get; } |
||||
|
||||
public bool IsDeferred => ecs_is_deferred(this); |
||||
public bool IsQuitRequested => ecs_should_quit(this); |
||||
|
||||
public World(params string[] args) |
||||
{ |
||||
Handle = ecs_init_w_args(args.Length, null); |
||||
|
||||
ChildOf = LookupOrThrow("/flecs/core/ChildOf"); |
||||
Disabled = LookupOrThrow("/flecs/core/Disabled"); |
||||
DependsOn = LookupOrThrow("/flecs/core/DependsOn"); |
||||
} |
||||
|
||||
public void Dispose() |
||||
=> ecs_fini(this); |
||||
|
||||
public EntityBuilder New(EntityPath? path = null) |
||||
=> new(this, path); |
||||
|
||||
public bool Progress(TimeSpan delta) |
||||
=> ecs_progress(this, (float)delta.TotalSeconds); |
||||
|
||||
public void Quit() |
||||
=> ecs_quit(this); |
||||
|
||||
public static implicit operator ecs_world_t*(World w) => w.Handle; |
||||
} |
@ -0,0 +1,170 @@ |
||||
using System; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Runtime.InteropServices; |
||||
using System.Text; |
||||
using System.Threading; |
||||
using static flecs_hub.flecs.Runtime; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public interface IAllocator |
||||
{ |
||||
nint Allocate(int byteCount); |
||||
void Free(nint pointer); |
||||
} |
||||
|
||||
public unsafe static class AllocatorExtensions |
||||
{ |
||||
public static Span<T> Allocate<T>(this IAllocator allocator, int count) where T : unmanaged |
||||
=> new((void*)allocator.Allocate(sizeof(T) * count), count); |
||||
public static void Free<T>(this IAllocator allocator, Span<T> span) where T : unmanaged |
||||
=> allocator.Free((nint)Unsafe.AsPointer(ref span[0])); |
||||
|
||||
public static Span<T> AllocateCopy<T>(this IAllocator allocator, ReadOnlySpan<T> orig) where T : unmanaged |
||||
{ 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))); |
||||
public static void Free<T>(this IAllocator allocator, ref T value) where T : unmanaged |
||||
=> allocator.Free((nint)Unsafe.AsPointer(ref value)); |
||||
|
||||
public static CString AllocateCString(this IAllocator allocator, string? value) |
||||
{ |
||||
if (value == null) return default; |
||||
var bytes = Encoding.UTF8.GetByteCount(value); |
||||
var span = allocator.Allocate<byte>(bytes + 1); |
||||
Encoding.UTF8.GetBytes(value, span); |
||||
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) |
||||
{ |
||||
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 static class TempAllocator |
||||
{ |
||||
public const int Capacity = 1024 * 1024; // 1 MB |
||||
private static readonly ThreadLocal<ArenaAllocator> _allocator |
||||
= new(() => new(Capacity)); |
||||
|
||||
public static ResetOnDispose Use() |
||||
{ |
||||
var allocator = _allocator.Value!; |
||||
return new(allocator, allocator.Used); |
||||
} |
||||
|
||||
public sealed class ResetOnDispose |
||||
: IAllocator |
||||
, IDisposable |
||||
{ |
||||
private readonly ArenaAllocator _allocator; |
||||
private readonly int _start; |
||||
|
||||
public ResetOnDispose(ArenaAllocator allocator, int start) |
||||
{ _allocator = allocator; _start = start; } |
||||
// TODO: Print warning in finalizer if Dispose wasn't called manually. |
||||
|
||||
// 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 |
||||
: IAllocator |
||||
{ |
||||
public static GlobalHeapAllocator Instance { get; } = new(); |
||||
|
||||
public nint Allocate(int byteCount) |
||||
=> Marshal.AllocHGlobal(byteCount); |
||||
public void Free(nint pointer) |
||||
=> Marshal.FreeHGlobal(pointer); |
||||
} |
||||
|
||||
public sealed class ArenaAllocator |
||||
: IAllocator |
||||
, IDisposable |
||||
{ |
||||
private nint _buffer; |
||||
public int Capacity { get; private set; } |
||||
public int Used { get; private set; } = 0; |
||||
|
||||
public ArenaAllocator(int capacity) |
||||
{ |
||||
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); |
||||
_buffer = Marshal.AllocHGlobal(capacity); |
||||
Capacity = capacity; |
||||
} |
||||
|
||||
public void Dispose() |
||||
{ |
||||
Marshal.FreeHGlobal(_buffer); |
||||
_buffer = default; |
||||
Capacity = 0; |
||||
} |
||||
|
||||
public nint Allocate(int byteCount) |
||||
{ |
||||
if (_buffer == default) throw new ObjectDisposedException(nameof(ArenaAllocator)); |
||||
if (Used + byteCount > Capacity) throw new InvalidOperationException( |
||||
$"Cannot allocate more than {Capacity} bytes with this {nameof(ArenaAllocator)}"); |
||||
var ptr = _buffer + Used; |
||||
Used += byteCount; |
||||
return ptr; |
||||
} |
||||
|
||||
public void Free(nint pointer) |
||||
{ /* Do nothing. */ } |
||||
|
||||
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 |
||||
: IAllocator |
||||
, IDisposable |
||||
{ |
||||
private nint _buffer; |
||||
private int _current = 0; |
||||
public int Capacity { get; private set; } |
||||
|
||||
public RingAllocator(int capacity) |
||||
{ |
||||
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); |
||||
_buffer = Marshal.AllocHGlobal(capacity); |
||||
Capacity = capacity; |
||||
} |
||||
|
||||
public void Dispose() |
||||
{ |
||||
Marshal.FreeHGlobal(_buffer); |
||||
_buffer = default; |
||||
Capacity = 0; |
||||
} |
||||
|
||||
public nint Allocate(int byteCount) |
||||
{ |
||||
if (_buffer == default) throw new ObjectDisposedException(nameof(RingAllocator)); |
||||
if (byteCount > Capacity) throw new ArgumentOutOfRangeException(nameof(byteCount)); |
||||
if (_current + byteCount > Capacity) _current = 0; |
||||
var ptr = _buffer + _current; |
||||
_current += byteCount; |
||||
return ptr; |
||||
} |
||||
|
||||
public void Free(nint pointer) |
||||
{ /* IGNORE */ } |
||||
} |
@ -0,0 +1,34 @@ |
||||
using System; |
||||
using System.Runtime.InteropServices; |
||||
using static flecs_hub.flecs; |
||||
using static flecs_hub.flecs.Runtime; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public unsafe static class CStringExtensions |
||||
{ |
||||
public static CString Empty { get; } = (CString)""; |
||||
public static CString ETX { get; } = (CString)"\x3"; // TODO: Temporary, until flecs supports Empty. |
||||
|
||||
public static unsafe byte[]? FlecsToBytes(this CString str) |
||||
{ |
||||
if (str.IsNull) return null; |
||||
var pointer = (byte*)(nint)str; |
||||
// Find length of the string by locating the NUL character. |
||||
var length = 0; while (true) if (pointer[length++] == 0) break; |
||||
// Create span over the region, NUL included, copy it into an array. |
||||
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); |
||||
|
||||
public static string? FlecsToStringAndFree(this CString str) |
||||
{ var result = str.FlecsToString(); str.FlecsFree(); return result; } |
||||
|
||||
public static void FlecsFree(this CString str) |
||||
{ if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); } |
||||
} |
@ -0,0 +1,31 @@ |
||||
using System.Collections.Generic; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public static class CallbackContextHelper |
||||
{ |
||||
private static readonly Dictionary<nint, object> _contexts = new(); |
||||
private static nint _counter = 0; |
||||
|
||||
public static nint Create<T>(T context) where T : notnull |
||||
{ |
||||
lock (_contexts) { |
||||
var id = _counter++; |
||||
_contexts.Add(id, context); |
||||
return id; |
||||
} |
||||
} |
||||
|
||||
public static T Get<T>(nint id) |
||||
{ |
||||
lock (_contexts) |
||||
return (T)_contexts[id]; |
||||
} |
||||
|
||||
public static void Free(nint id) |
||||
{ |
||||
lock (_contexts) |
||||
_contexts.Remove(id); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,41 @@ |
||||
using System; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public static class SpanExtensions |
||||
{ |
||||
public static T? GetOrNull<T>(this Span<T> span, int index) |
||||
where T : struct => GetOrNull((ReadOnlySpan<T>)span, index); |
||||
public static T? GetOrNull<T>(this ReadOnlySpan<T> span, int index) |
||||
where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; |
||||
|
||||
|
||||
public static ReadOnlySpanSplitEnumerator<T> Split<T>(this ReadOnlySpan<T> span, T splitOn) |
||||
where T : IEquatable<T> => new(span, splitOn); |
||||
|
||||
public ref struct ReadOnlySpanSplitEnumerator<T> |
||||
where T : IEquatable<T> |
||||
{ |
||||
private readonly ReadOnlySpan<T> _span; |
||||
private readonly T _splitOn; |
||||
private int _index = -1; |
||||
private int _end = -1; |
||||
|
||||
public ReadOnlySpanSplitEnumerator(ReadOnlySpan<T> span, T splitOn) |
||||
{ _span = span; _splitOn = splitOn; } |
||||
|
||||
public ReadOnlySpanSplitEnumerator<T> GetEnumerator() => this; |
||||
|
||||
public ReadOnlySpan<T> Current |
||||
=> (_end >= 0) && (_end <= _span.Length) |
||||
? _span[_index.._end] : throw new InvalidOperationException(); |
||||
|
||||
public bool MoveNext() |
||||
{ |
||||
if (_end == _span.Length) return false; |
||||
_end++; _index = _end; |
||||
while (_end < _span.Length && !_span[_end].Equals(_splitOn)) _end++; |
||||
return true; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
using System; |
||||
using System.Runtime.InteropServices; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public readonly ref struct SpanToRef<T> |
||||
where T : class
|
||||
{ |
||||
private readonly Span<nint> _span; |
||||
|
||||
public int Length => _span.Length; |
||||
public T? this[int index] => (_span[index] != 0) |
||||
? (T)((GCHandle)_span[index]).Target! : null; |
||||
|
||||
internal SpanToRef(Span<nint> span) => _span = span; |
||||
|
||||
public void Clear() => _span.Clear(); |
||||
public void CopyTo(SpanToRef<T> dest) => _span.CopyTo(dest._span); |
||||
} |
Loading…
Reference in new issue