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