Compare commits

..

34 Commits

Author SHA1 Message Date
copygirl 28d2b3e077 Fix link in README and some whitespace 1 year ago
copygirl 99174ece5c Use ecs_iter_is_true in Iterator.Any 1 year ago
copygirl 1607506e32 Remove ETX as Flecs now supports Empty 1 year ago
copygirl 5600a6f0ee Fix compile errors due to changes in flecs-cs 1 year ago
copygirl 063fb40c5c Update to newest flecs-cs 1 year ago
copygirl 3bea2a6ba0 Add Term, Filter, Query, Rule shorthands to World 1 year ago
copygirl ef00efd4bc Add Iterator<>.First() 1 year ago
copygirl 2c5d7f6c65 Add Iterator.Any() 1 year ago
copygirl 720a90549c Change $This variable to $this 1 year ago
copygirl 1dca1e7988 Add Entity<>.IsEnabled 1 year ago
copygirl c575046a61 Minimal world init, remove args constructor 1 year ago
copygirl 6cd699efbc Update README 1 year ago
copygirl eec968d636 Update flecs-cs and Flecs to fix missing functions 1 year ago
copygirl 5264ffdc5f Fix World not handling args correctly 1 year ago
copygirl 9eade0adb1 Add Entity<T>.Valid/AliveOrNull/Throw 1 year ago
copygirl 7830e72c29 Fix IsAlive throwing if entity is None 1 year ago
copygirl b85a533ca4 Throw in EntityPath.From if entity is None 1 year ago
copygirl 42f843e690 Update to newest flecs-cs 1 year ago
copygirl f7d17d46ab Fix Observer not using new generic Iterator 1 year ago
copygirl b93beed5ec Fix CreateLookup throwing on same lookup creation 1 year ago
copygirl 948268e9ba WIP 1 year ago
copygirl 1d1ba4fe4d Remove EntityBase 2 years ago
copygirl 60919ef1f6 Add EntityBuilder constructor with parent arg 2 years ago
copygirl 0527919d47 Make World implement IDisposable 2 years ago
copygirl c24052cfa7 Move EntityNotFoundException out of World class 2 years ago
copygirl dc776ace1f Remove LookupByType and related methods 2 years ago
copygirl e4f3020a87 Exclude flecs-cs from compile items 2 years ago
copygirl 2d72e87290 Many minor changes 2 years ago
copygirl 411f94c412 Fix IdFlags not being up-to-date with Flecs 2 years ago
copygirl 0ff026d639 Cleanup 2 years ago
copygirl 33bbd75fbc Rename Term.InOut and .Oper, add InOut property 2 years ago
copygirl 60c031a119 Don't add phase in InitSystem 2 years ago
copygirl d6c8129676 Allow passing multiple events to InitObserver 2 years ago
copygirl 6bc3ab8bfb Add SpanToRef.GetOrNull 2 years ago
  1. 3
      .gitmodules
  2. 17
      .vscode/launch.json
  3. 8
      .vscode/settings.json
  4. 14
      .vscode/tasks.json
  5. 55
      README.md
  6. 14548
      flecs.h-ast
  7. 6
      gaemstone.ECS.csproj
  8. 34
      gaemstone.ECS.sln
  9. 1
      src/flecs
  10. 2
      src/flecs-cs
  11. 421
      src/gaemstone.ECS.BindGen/Program.cs
  12. 21
      src/gaemstone.ECS.BindGen/gaemstone.ECS.BindGen.csproj
  13. 87
      src/gaemstone.ECS/Component.cs
  14. 35
      src/gaemstone.ECS/Entity+Bare.cs
  15. 176
      src/gaemstone.ECS/Entity.cs
  16. 61
      src/gaemstone.ECS/EntityBase.cs
  17. 89
      src/gaemstone.ECS/EntityBuilder.cs
  18. 72
      src/gaemstone.ECS/EntityPath.cs
  19. 169
      src/gaemstone.ECS/EntityRef.cs
  20. 16
      src/gaemstone.ECS/EntityType.cs
  21. 31
      src/gaemstone.ECS/Exceptions.cs
  22. 27
      src/gaemstone.ECS/Filter.cs
  23. 36
      src/gaemstone.ECS/Id+Bare.cs
  24. 81
      src/gaemstone.ECS/Id.cs
  25. 61
      src/gaemstone.ECS/IdRef.cs
  26. 117
      src/gaemstone.ECS/Internal/EntityAccess.cs
  27. 11
      src/gaemstone.ECS/Internal/FlecsBuiltIn.cs
  28. 113
      src/gaemstone.ECS/Internal/Iterator.cs
  29. 9
      src/gaemstone.ECS/Internal/Lookup.cs
  30. 178
      src/gaemstone.ECS/Iterator.cs
  31. 26
      src/gaemstone.ECS/Observer.cs
  32. 31
      src/gaemstone.ECS/Query.cs
  33. 53
      src/gaemstone.ECS/Rule.cs
  34. 41
      src/gaemstone.ECS/System.cs
  35. 40
      src/gaemstone.ECS/Term.cs
  36. 15
      src/gaemstone.ECS/Utility/Allocators.cs
  37. 1
      src/gaemstone.ECS/Utility/CStringExtensions.cs
  38. 26
      src/gaemstone.ECS/Utility/FlecsException.cs
  39. 71
      src/gaemstone.ECS/Utility/ReferenceHandle.cs
  40. 2
      src/gaemstone.ECS/Utility/SpanExtensions.cs
  41. 75
      src/gaemstone.ECS/World+Bare.cs
  42. 69
      src/gaemstone.ECS/World+Lookup.cs
  43. 96
      src/gaemstone.ECS/World.cs
  44. 182
      src/gaemstone.ECS/flecs/flecs+Constants.g.cs
  45. 3773
      src/gaemstone.ECS/flecs/flecs+Functions.g.cs
  46. 1754
      src/gaemstone.ECS/flecs/flecs+Structs.g.cs

3
.gitmodules vendored

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

@ -1,17 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch BindGen",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"env": { "LIBCLANG_DISABLE_CRASH_RECOVERY": "true" },
"cwd": "${workspaceFolder}/src/gaemstone.ECS.BindGen",
"program": "${workspaceFolder}/src/gaemstone.ECS.BindGen/bin/Debug/net7.0/gaemstone.ECS.BindGen.dll",
"args": [],
"console": "internalConsole",
"stopAtEntry": false,
}
]
}

@ -1,8 +0,0 @@
{
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bin": true,
"**/obj": true,
}
}

14
.vscode/tasks.json vendored

@ -1,14 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"group": "build",
"label": "build",
"type": "shell", "command": "dotnet",
"options": { "cwd": "src/gaemstone.ECS.BindGen" },
"args": [ "build", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ],
"presentation": { "reveal": "silent" },
"problemMatcher": "$msCompile"
}
]
}

@ -1,10 +1,8 @@
# gaemstone.ECS # gaemstone.ECS
.. is a medium-level managed wrapper library around the [flecs-cs] bindings .. 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.
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 These classes have been split from the main [gæmstone] project. It is still a little unclear what functionality belongs where, and structural changes may occur. In its current state, I recommend to use this repository simply as a reference for building similar projects.
as well. To efficiently use this library, a thorough understanding of [Flecs]
is required.
[ECS]: https://en.wikipedia.org/wiki/Entity_component_system [ECS]: https://en.wikipedia.org/wiki/Entity_component_system
[Flecs]: https://github.com/SanderMertens/flecs [Flecs]: https://github.com/SanderMertens/flecs
@ -13,20 +11,18 @@ is required.
## Features ## 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. - Convenient wrapper types such as [Id], [Entity] and [EntityType].
- Simple wrapper structs such as [Entity] and [Identifier].
- Classes with convenience functions like [EntityRef] and [EntityType].
- [EntityPath] uses a unix-like path, for example `/Game/Players/copygirl`. - [EntityPath] uses a unix-like path, for example `/Game/Players/copygirl`.
- Fast type-to-entity lookup using generic context on [World]. (See below.)
- Define your own [Components] as both value or reference types. - Define your own [Components] as both value or reference types.
- Query the ECS with [Iterators], [Filters], [Queries] and [Rules]. - Query the ECS with [Iterators], [Filters], [Queries] and [Rules].
- Create [Systems] for game logic and [Observers] to act on changes. - Create [Systems] for game logic and [Observers] to act on changes.
[Id]: ./src/gaemstone.ECS/Id.cs
[Entity]: ./src/gaemstone.ECS/Entity.cs [Entity]: ./src/gaemstone.ECS/Entity.cs
[Identifier]: ./src/gaemstone.ECS/Identifier.cs
[EntityRef]: ./src/gaemstone.ECS/EntityRef.cs
[EntityType]: ./src/gaemstone.ECS/EntityType.cs [EntityType]: ./src/gaemstone.ECS/EntityType.cs
[EntityPath]: ./src/gaemstone.ECS/EntityPath.cs [EntityPath]: ./src/gaemstone.ECS/EntityPath.cs
[World]: ./src/gaemstone.ECS/World.cs
[Components]: ./src/gaemstone.ECS/Component.cs [Components]: ./src/gaemstone.ECS/Component.cs
[Iterators]: ./src/gaemstone.ECS/Iterator.cs [Iterators]: ./src/gaemstone.ECS/Iterator.cs
[Filters]: ./src/gaemstone.ECS/Filter.cs [Filters]: ./src/gaemstone.ECS/Filter.cs
@ -38,7 +34,7 @@ These classes have only recently been split from the main **gæmstone** project.
## Example ## Example
```cs ```cs
var world = new World(); var world = new World<Program>();
var position = world var position = world
.New("Position") // Create a new EntityBuilder, and set its name. .New("Position") // Create a new EntityBuilder, and set its name.
@ -59,22 +55,27 @@ foreach (var child in entities.GetChildren()) {
pos = new(pos.X * 10, pos.Y * 10); pos = new(pos.X * 10, pos.Y * 10);
} }
var onUpdate = world.LookupByPathOrThrow("/flecs/pipeline/OnUpdate"); // The following systems run in the "OnUpdate"
// phase of the default pipeline provided by Flecs.
var dependsOn = world.LookupPathOrThrow("/flecs/core/DependsOn");
var onUpdate = world.LookupPathOrThrow("/flecs/pipeline/OnUpdate");
// Create a system that will move all entities with // Create a system that will move all entities with
// the "Position" component downwards by 2 every frame. // the "Position" component downwards by 2 every frame.
world.New("FallSystem").Build() world.New("FallSystem")
.InitSystem(onUpdate, new("Position"), iter => { .Add(dependsOn, onUpdate)
.Build().InitSystem(new("Position"), iter => {
var posColumn = iter.Field<Position>(1); var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) { for (var i = 0; i < iter.Count; i++) {
ref var pos = ref posColumn[i]; ref var pos = ref posColumn[i];
pos = new(pos.X, pos.Y + 2); pos = new(pos.X, pos.Y - 2);
} }
}); });
// Create a system that will print out entities' positions. // Create a system that will print out entities' positions.
world.New("PrintPositionSystem").Build() world.New("PrintPositionSystem").Build()
.InitSystem(onUpdate, new("[in] Position"), iter => { .Add(dependsOn, onUpdate)
.InitSystem(new("[in] Position"), iter => {
var posColumn = iter.Field<Position>(1); var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) { for (var i = 0; i < iter.Count; i++) {
var entity = iter.Entity(i); var entity = iter.Entity(i);
@ -104,8 +105,26 @@ git clone --recurse-submodules https://git.mcft.net/copygirl/gaemstone.ECS.git
git submodule add https://git.mcft.net/copygirl/gaemstone.ECS.git git submodule add https://git.mcft.net/copygirl/gaemstone.ECS.git
# To add a reference to this library to your .NET project: # To add a reference to this library to your .NET project:
dotnet add reference gaemstone.ECS/src/gaemstone.ECS/gaemstone.ECS.csproj dotnet add reference gaemstone.ECS/gaemstone.ECS.csproj
# To generate flecs-cs' bindings: # To generate flecs-cs' bindings:
./gaemstone.ECS/src/flecs-cs/library.sh ./gaemstone.ECS/src/flecs-cs/library.sh
``` ```
## On the `TContext` type parameter
Entities may be looked up simply by their type, once they're registered with `CreateLookup` or `InitComponent`. Under the hood, this is made possible using a nested static generic class that hold onto an `Entity` field. Theoretically this could be compiled into a simple field lookup and therefore be faster than dictionary lookups.
To support scenarios where multiple worlds may be used simultaneously, each with their own unique type lookups, we specify a generic type as that context.
In cases where only a single world is used, the amount of typing can be reduced by including a file similar to the following, defining global aliases:
```cs
global using Entity = gaemstone.ECS.Entity<Context>;
global using Id = gaemstone.ECS.Id<Context>;
global using Iterator = gaemstone.ECS.Iterator<Context>;
global using World = gaemstone.ECS.World<Context>;
// Add more aliases as you feel they are needed.
public struct Context { }
```

File diff suppressed because it is too large Load Diff

@ -8,7 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" /> <Compile Remove="src/flecs-cs/**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="src/flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -1,34 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0FC51081-529F-4DC2-91D0-18002C10B733}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.ECS", "src\gaemstone.ECS\gaemstone.ECS.csproj", "{7CDAB372-16EB-452C-B984-0BD5F1D0D411}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.ECS.BindGen", "src\gaemstone.ECS.BindGen\gaemstone.ECS.BindGen.csproj", "{4DA4F739-1C38-41D3-804B-B1114090FF53}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7CDAB372-16EB-452C-B984-0BD5F1D0D411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CDAB372-16EB-452C-B984-0BD5F1D0D411}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CDAB372-16EB-452C-B984-0BD5F1D0D411}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CDAB372-16EB-452C-B984-0BD5F1D0D411}.Release|Any CPU.Build.0 = Release|Any CPU
{4DA4F739-1C38-41D3-804B-B1114090FF53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DA4F739-1C38-41D3-804B-B1114090FF53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DA4F739-1C38-41D3-804B-B1114090FF53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DA4F739-1C38-41D3-804B-B1114090FF53}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7CDAB372-16EB-452C-B984-0BD5F1D0D411} = {0FC51081-529F-4DC2-91D0-18002C10B733}
{4DA4F739-1C38-41D3-804B-B1114090FF53} = {0FC51081-529F-4DC2-91D0-18002C10B733}
EndGlobalSection
EndGlobal

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

@ -1 +1 @@
Subproject commit a2047983917aa462a8c2f34d5315aea48502f4d8 Subproject commit 1ae8ffade56a279d450dbf42545afbf873bdbafe

@ -1,421 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using ClangSharp.Interop;
namespace gaemstone.ECS.BindGen;
public unsafe static class Program
{
// TODO: Handle being called from other working directories gracefully.
private const string FlecsRepositoryLocation = "../flecs";
private const string OutputFolderLocation = "../gaemstone.ECS/flecs";
private static readonly Dictionary<string, string> TypeAliases = new() {
{ "int8_t" , "sbyte" },
{ "int16_t", "short" },
{ "int32_t", "int" },
{ "int64_t", "long" },
{ "uint8_t" , "byte" },
{ "uint16_t", "ushort" },
{ "uint32_t", "uint" },
{ "uint64_t", "ulong" },
{ "ecs_flags8_t" , "byte" },
{ "ecs_flags16_t", "ushort" },
{ "ecs_flags32_t", "uint" },
{ "ecs_flags64_t", "ulong" },
{ "ecs_size_t", "int" },
};
public static void Main()
{
using var fileConstants = File.CreateText(Path.Combine(OutputFolderLocation, "flecs+Constants.g.cs"));
using var fileStructs = File.CreateText(Path.Combine(OutputFolderLocation, "flecs+Structs.g.cs"));
using var fileFunctions = File.CreateText(Path.Combine(OutputFolderLocation, "flecs+Functions.g.cs"));
static string StartProcessAndReturnOutput(string program, string arguments, string workDir = "")
{
var gitProcess = new Process { StartInfo = new() {
FileName = "git", Arguments = "describe --tags --always", WorkingDirectory = workDir,
RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true,
} };
gitProcess.Start();
var result = gitProcess.StandardOutput.ReadToEnd().Trim();
gitProcess.WaitForExit();
return result;
}
var bindgenVersion = StartProcessAndReturnOutput("git", "describe --tags --always");
var flecsVersion = StartProcessAndReturnOutput("git", "describe --tags --always", FlecsRepositoryLocation);
var autoGeneratedHeader = $$"""
// <auto-generated>
// Generated by gaemstone.ECS.BindGen {{ bindgenVersion }}
// Time: {{ DateTime.UtcNow :u}}
// Flecs version: {{ flecsVersion }}
// </auto-generated>
""";
fileConstants.WriteLine($$"""
{{ autoGeneratedHeader }}
#pragma warning disable CS8981
public static partial class flecs
{
""");
fileStructs.Write($$"""
{{ autoGeneratedHeader }}
using System.Runtime.InteropServices;
#pragma warning disable CS8981
public static unsafe partial class flecs
{
""");
fileFunctions.WriteLine($$"""
{{ autoGeneratedHeader }}
using System;
using System.Runtime.InteropServices;
#pragma warning disable CS8981
public static unsafe partial class flecs
{
private const string LibraryName = "flecs";
/// <summary> Indicates types are marked as "const" on C's end. </summary>
[AttributeUsage(AttributeTargets.Parameter, AttributeTargets.ReturnValue)]
public class ConstAttribute : Attribute { }
""");
using var unit = CXTranslationUnit.CreateFromSourceFile(
CXIndex.Create(), Path.Combine(FlecsRepositoryLocation, "flecs.h"),
default, default);
unit.Cursor.VisitChildren((cursor, _, _) => {
switch (cursor) {
case { Location.IsFromMainFile: false }: // Not from the file we're trying to parse.
case { Kind: CXCursorKind.CXCursor_MacroInstantiation
or CXCursorKind.CXCursor_InclusionDirective }: // Ignore these altogether.
break;
case { Kind: CXCursorKind.CXCursor_MacroDefinition }:
WriteMacro(fileConstants, 1, cursor);
break;
// case { Kind: CXCursorKind.CXCursor_TypedefDecl }:
// WriteTypedef(cursor, output);
// break;
case { Kind: CXCursorKind.CXCursor_EnumDecl }:
fileStructs.WriteLine();
WriteEnum(fileStructs, 1, cursor);
break;
case { Kind: CXCursorKind.CXCursor_StructDecl }:
// Skip forward declarations, unless they're not defined at all.
if (!cursor.IsDefinition && !cursor.Definition.IsNull) break;
fileStructs.WriteLine();
WriteStruct(fileStructs, 1, cursor);
break;
case { Kind: CXCursorKind.CXCursor_FunctionDecl }:
fileFunctions.WriteLine();
WriteFunction(fileFunctions, 1, cursor);
break;
default:
Console.WriteLine($"{cursor.Kind} {cursor}");
Console.WriteLine(GetSource(unit, cursor.Extent));
break;
}
return CXChildVisitResult.CXChildVisit_Continue;
}, default);
fileConstants.WriteLine("}");
fileStructs .WriteLine("}");
fileFunctions.WriteLine("}");
}
private static void WriteMacro(StreamWriter writer, int indent, CXCursor cursor)
{
if (cursor.IsMacroFunctionLike) return;
var unit = cursor.TranslationUnit;
var tokens = unit.Tokenize(cursor.Extent).ToArray();
if (tokens.Length < 2) return; // No value.
var type = "uint";
var name = cursor.Spelling.ToString();
if (name is "NULL" or "ECS_VECTOR_T_SIZE"
or "EcsLastInternalComponentId"
or "ECS_FUNC_NAME_BACK") return;
var value = GetSource(unit, Range(unit, tokens[1], tokens[^1]));
if (value is not [ '(', .., ')']) return;
if (value.Contains('\\')) value = value.Replace("\\\n", "").Replace(" ", "");
if (value.Contains("ull")) { value = value.Replace("ull", "ul"); type = "ulong"; }
if (name == "ECS_MAX_COMPONENT_ID") value = value.Replace("(uint32_t)", "(uint)");
WriteComment(writer, indent, cursor);
WriteIndent(writer, indent).WriteLine($"public const {type} {name} = {value};");
}
private static void WriteEnum(StreamWriter writer, int indent, CXCursor cursor)
{
WriteComment(writer, indent, cursor);
WriteIndent(writer, indent).WriteLine($"public {cursor.Type}");
WriteIndent(writer, indent).WriteLine("{");
cursor.VisitChildren((value, _, _) => {
WriteComment(writer, indent + 1, value);
WriteIndent(writer, indent + 1).WriteLine($"{value},");
return CXChildVisitResult.CXChildVisit_Continue;
}, default);
WriteIndent(writer, indent).WriteLine("}");
}
private static void WriteStruct(StreamWriter writer, int indent, CXCursor cursor,
string? name = null, bool noComment = false)
{
if (!noComment) WriteComment(writer, indent, cursor);
if (cursor.Kind == CXCursorKind.CXCursor_UnionDecl)
WriteIndent(writer, indent).WriteLine("[StructLayout(LayoutKind.Explicit)]");
name ??= cursor.Type.ToString();
if (name.StartsWith("struct ")) name = name[("struct ".Length)..];
WriteIndent(writer, indent).Write($"public struct {name}");
var hasFields = false; // For creating a shorthand struct definition that doesn't take up 3 lines.
cursor.VisitChildren((field, _, _) => {
// Nested struct and union declarations will be handled when field is written.
if (field.Kind is CXCursorKind.CXCursor_StructDecl
or CXCursorKind.CXCursor_UnionDecl)
return CXChildVisitResult.CXChildVisit_Continue;
if (field.Kind is not CXCursorKind.CXCursor_FieldDecl)
throw new NotSupportedException();
if (!hasFields) {
writer.WriteLine();
WriteIndent(writer, indent).WriteLine("{");
hasFields = true;
}
if (cursor.Kind == CXCursorKind.CXCursor_UnionDecl)
WriteIndent(writer, indent).WriteLine("[FieldOffset(0)]");
WriteComment(writer, indent + 1, field);
WriteIndent(writer, indent + 1);
var name = field.DisplayName.ToString();
if (IsReservedKeyword(name)) name = $"@{name}";
if (field.Type.Declaration.IsAnonymous) {
writer.WriteLine($"public _{field.DisplayName} {name};");
WriteStruct(writer, indent + 1, field.Type.Declaration,
name: $"_{field.DisplayName}", noComment: true);
} else if (field.Type.kind == CXTypeKind.CXType_ConstantArray) {
writer.WriteLine($"public fixed {field.Type.ArrayElementType} {name}[{field.Type.ArraySize}];");
} else {
var type = GetTypeString(field.Type, out var isConst);
if (isConst) writer.Write($"[Const] ");
writer.WriteLine($"public {type} {name};");
}
return CXChildVisitResult.CXChildVisit_Continue;
}, default);
if (!hasFields) writer.WriteLine(" { }");
else WriteIndent(writer, indent).WriteLine("}");
}
private static void WriteFunction(StreamWriter writer, int indent, CXCursor cursor)
{
WriteComment(writer, indent, cursor);
var returnType = GetTypeString(cursor.ReturnType, out var returnIsConst);
if (returnIsConst) WriteIndent(writer, indent).WriteLine("[return: Const]");
WriteIndent(writer, indent).WriteLine("[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)]");
WriteIndent(writer, indent).Write($"public static extern {returnType} {cursor.Spelling}(");
for (var i = 0; i < cursor.NumArguments; i++) {
var arg = cursor.GetArgument((uint)i);
if (i > 0) writer.Write(", ");
var argType = GetTypeString(arg.Type, out var argIsConst);
if (argIsConst) writer.Write("[Const] ");
var name = arg.DisplayName.ToString();
if (IsReservedKeyword(name)) name = $"@{name}";
writer.Write($"{argType} {name}");
}
writer.WriteLine(");");
}
private static void WriteComment(StreamWriter writer, int indent, CXCursor cursor)
{
var comment = cursor.ParsedComment;
if (comment.Kind == CXCommentKind.CXComment_Null) return;
if (comment.Kind != CXCommentKind.CXComment_FullComment) throw new NotSupportedException();
var children = Children(comment).ToArray();
var paragraphs = children
.TakeWhile(c => c.Kind == CXCommentKind.CXComment_Paragraph)
.Select(GetCommentText).Where(p => p != null).ToArray();
var @params = children
.Where (c => c.Kind == CXCommentKind.CXComment_ParamCommand)
.Select(c => (c.ParamCommandComment_ParamName, GetCommentText(Children(c).Single()))).ToArray();
var @return = children
.Where (c => c.Kind == CXCommentKind.CXComment_BlockCommand)
.Where (c => c.BlockCommandComment_CommandName.ToString() == "return")
.Select(c => GetCommentText(Children(c).Single())).FirstOrDefault();
if (paragraphs.Length > 1) {
WriteIndent(writer, indent).WriteLine($"/// <summary>");
foreach (var paragraph in paragraphs)
WriteIndent(writer, indent).WriteLine($"/// <p> {paragraph} </p>");
WriteIndent(writer, indent).WriteLine($"/// </summary>");
} else if (paragraphs.Length == 1)
WriteIndent(writer, indent).WriteLine($"/// <summary> {paragraphs[0]} </summary>");
foreach (var (name, text) in @params)
WriteIndent(writer, indent).WriteLine($$"""/// <param name="{{ name }}"> {{ text }} </param>""");
if (@return != null)
WriteIndent(writer, indent).WriteLine($$"""/// <returns> {{ @return }} </returns>""");
foreach (var child in Children(comment))
switch (child.Kind) {
case CXCommentKind.CXComment_Paragraph:
case CXCommentKind.CXComment_ParamCommand:
case CXCommentKind.CXComment_BlockCommand when child.BlockCommandComment_CommandName.ToString() == "return":
break;
default:
WriteIndent(writer, indent);
writer.WriteLine($"//// {child.Kind}");
break;
}
static IEnumerable<CXComment> Children(CXComment parent)
=> Enumerable.Range(0, (int)parent.NumChildren).Select(i => parent.GetChild((uint)i));
static string? GetCommentText(CXComment paragraph)
{
if (paragraph.Kind != CXCommentKind.CXComment_Paragraph) throw new NotSupportedException();
var sb = new StringBuilder();
foreach (var part in Children(paragraph)) {
if (part.Kind != CXCommentKind.CXComment_Text) throw new NotSupportedException();
var str = part.TextComment_Text.ToString().Trim();
if (str == "") continue;
if (sb.Length > 0) sb.Append(' ');
sb.Append(str);
}
if (sb.Length == 0) return null;
sb.Replace("<", "&lt;").Replace(">", "&gt;");
if (sb[^1] != '.') sb.Append('.');
return sb.ToString();
}
}
private static StreamWriter WriteIndent(StreamWriter writer, int indent)
{
for (var i = 0; i < indent; i++)
writer.Write('\t');
return writer;
}
private static CXSourceRange Range(CXTranslationUnit unit, CXToken start, CXToken end)
=> CXSourceRange.Create(start.GetExtent(unit).Start, end.GetExtent(unit).End);
private static string GetTypeString(CXType type, out bool isConst)
{
isConst = false;
switch (type) {
case { kind: CXTypeKind.CXType_Pointer,
PointeeType: { kind: CXTypeKind.CXType_FunctionProto } funcType }:
var resultType = GetTypeString(funcType.ResultType, out _);
var argTypes = Enumerable.Range(0, funcType.NumArgTypes)
.Select(i => funcType.GetArgType((uint)i))
.Select(a => GetTypeString(a, out _))
.ToArray();
return $"delegate* unmanaged<{string.Join(", ", argTypes)}, {resultType}>";
case { kind: CXTypeKind.CXType_Pointer,
PointeeType: { kind: CXTypeKind.CXType_Elaborated,
Declaration.IsDefined: false} }:
return "void*";
case { kind: CXTypeKind.CXType_Pointer }:
return GetTypeString(type.PointeeType, out isConst) + "*";
case { kind: CXTypeKind.CXType_VariableArray
or CXTypeKind.CXType_IncompleteArray }:
return GetTypeString(type.ArrayElementType, out isConst) + "[]";
case { kind: CXTypeKind.CXType_Record }:
throw new NotSupportedException();
case { kind: CXTypeKind.CXType_Elaborated }:
var name = type.Declaration.ToString();
if (name == "") throw new Exception();
return name;
case { kind: CXTypeKind.CXType_FunctionProto }:
throw new NotSupportedException();
default:
name = type.ToString();
if (name == "") throw new Exception();
if (type.IsConstQualified) {
if (!name.StartsWith("const ")) throw new Exception();
name = name[("const ".Length)..];
isConst = true;
}
if (name.Contains(' ')) throw new Exception();
if (TypeAliases.TryGetValue(name, out var alias)) name = alias;
return name;
}
}
private static string GetSource(CXTranslationUnit unit, CXSourceRange range)
{
range.Start.GetFileLocation(out var startFile, out _, out _, out var start);
range.End .GetFileLocation(out var endFile , out _, out _, out var end);
if (startFile != endFile) return string.Empty;
return Encoding.UTF8.GetString(
unit.GetFileContents(startFile, out _)
.Slice((int)start, (int)(end - start)));
}
// See https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/
// Reserved keywords are unlikely to change in future C# versions, as they'd break
// existing programs. New ones are instead added as contextual keywords.
private static bool IsReservedKeyword(string str)
=> str is "abstract" or "as" or "base" or "bool" or "break" or "byte"
or "case" or "catch" or "char" or "checked" or "class"
or "const" or "continue" or "decimal" or "default"
or "delegate" or "do" or "double" or "else" or "enum"
or "event" or "explicit" or "extern" or "false" or "finally"
or "fixed" or "float" or "for" or "foreach" or "goto" or "if"
or "implicit" or "in" or "int" or "interface" or "internal"
or "is" or "lock" or "long" or "namespace" or "new" or "null"
or "object" or "operator" or "out" or "override" or "params"
or "private" or "protected" or "public" or "readonly" or "ref"
or "return" or "sbyte" or "sealed" or "short" or"sizeof"
or "stackalloc" or "static" or "string" or "struct" or "switch"
or "this" or "throw" or "true" or "try" or "typeof" or "uint"
or "ulong" or "unchecked" or "unsafe" or "ushort" or "using"
or "virtual" or "void" or "volatile" or "while";
}

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<LangVersion>preview</LangVersion>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<RuntimeIdentifier>ubuntu.22.04-x64</RuntimeIdentifier>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClangSharp" Version="15.0.2" />
</ItemGroup>
</Project>

@ -1,18 +1,13 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using gaemstone.ECS.Utility;
using static flecs_hub.flecs; using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public static unsafe class ComponentExtensions public unsafe partial struct Entity<TContext>
{ {
public static EntityRef InitComponent(this EntityRef entity, Type type) public Entity<TContext> InitComponent<T>()
=> (EntityRef)typeof(ComponentExtensions)
.GetMethod(nameof(InitComponent), new[] { typeof(EntityRef) })!
.MakeGenericMethod(type).Invoke(null, new[]{ entity })!;
public static EntityRef InitComponent<T>(this EntityRef entity)
{ {
if (typeof(T).IsPrimitive) throw new ArgumentException( if (typeof(T).IsPrimitive) throw new ArgumentException(
"Must not be primitive"); "Must not be primitive");
@ -22,8 +17,8 @@ public static unsafe class ComponentExtensions
var size = typeof(T).IsValueType ? Unsafe.SizeOf<T>() : sizeof(ReferenceHandle); var size = typeof(T).IsValueType ? Unsafe.SizeOf<T>() : sizeof(ReferenceHandle);
var typeInfo = new ecs_type_info_t { size = size, alignment = size }; var typeInfo = new ecs_type_info_t { size = size, alignment = size };
var componentDesc = new ecs_component_desc_t { entity = entity, type = typeInfo }; var componentDesc = new ecs_component_desc_t { entity = this, type = typeInfo };
ecs_component_init(entity.World, &componentDesc); ecs_component_init(World, &componentDesc);
if (!typeof(T).IsValueType) { if (!typeof(T).IsValueType) {
// Set up component hooks for proper freeing of GCHandles. // Set up component hooks for proper freeing of GCHandles.
@ -34,77 +29,9 @@ public static unsafe class ComponentExtensions
move = new() { Data = new() { Pointer = &ReferenceHandle.Move } }, move = new() { Data = new() { Pointer = &ReferenceHandle.Move } },
copy = new() { Data = new() { Pointer = &ReferenceHandle.Copy } }, copy = new() { Data = new() { Pointer = &ReferenceHandle.Copy } },
}; };
ecs_set_hooks_id(entity.World, entity, &typeHooks); ecs_set_hooks_id(World, this, &typeHooks);
}
return entity.CreateLookup(typeof(T));
}
}
public unsafe readonly struct ReferenceHandle
: IDisposable
{
public static int NumActiveHandles { get; private set; }
private readonly nint _value;
public object? Target =>
(_value != default)
? ((GCHandle)_value).Target
: null;
private ReferenceHandle(nint value)
=> _value = value;
public static ReferenceHandle Alloc(object? target)
{
if (target == null) return default;
NumActiveHandles++;
return new((nint)GCHandle.Alloc(target));
} }
public ReferenceHandle Clone() return CreateLookup<T>();
=> Alloc(Target);
public void Dispose()
{
if (_value == default) return;
NumActiveHandles--;
((GCHandle)_value).Free();
}
[UnmanagedCallersOnly]
internal static void Construct(void* ptr, int count, ecs_type_info_t* _)
=> new Span<ReferenceHandle>(ptr, count).Clear();
[UnmanagedCallersOnly]
internal static void Destruct(void* ptr, int count, ecs_type_info_t* _)
{
var span = new Span<ReferenceHandle>(ptr, count);
foreach (var handle in span) handle.Dispose();
span.Clear();
}
[UnmanagedCallersOnly]
internal static void Move(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _)
{
var dst = new Span<ReferenceHandle>(dstPtr, count);
var src = new Span<ReferenceHandle>(srcPtr, count);
foreach (var handle in dst) handle.Dispose();
src.CopyTo(dst);
src.Clear();
}
[UnmanagedCallersOnly]
internal static void Copy(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _)
{
var dst = new Span<ReferenceHandle>(dstPtr, count);
var src = new Span<ReferenceHandle>(srcPtr, count);
for (var i = 0; i < count; i++) {
dst[i].Dispose();
dst[i] = src[i].Clone();
}
} }
} }

@ -0,0 +1,35 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public readonly partial struct Entity
: IEquatable<Entity>
{
public static readonly Entity None = default;
public readonly ecs_entity_t Value;
public uint NumericId => (uint)Value.Data;
public bool IsSome => Value.Data != 0;
public bool IsNone => Value.Data == 0;
public Entity(ecs_entity_t value) => Value = value;
public bool Equals(Entity other) => Value.Data == other.Value.Data;
public override bool Equals(object? obj) => (obj is Entity other) && Equals(other);
public override int GetHashCode() => Value.Data.GetHashCode();
public override string? ToString()
=> IsSome ? $"Entity({Value.Data.Data})"
: "Entity.None";
public static bool operator ==(Entity left, Entity right) => left.Equals(right);
public static bool operator !=(Entity left, Entity right) => !left.Equals(right);
public static implicit operator Id (Entity e) => new(e.Value.Data);
public static implicit operator ecs_entity_t(Entity e) => e.Value;
public static implicit operator ecs_id_t (Entity e) => e.Value.Data;
public static implicit operator Term (Entity entity) => new(entity);
public static implicit operator TermId(Entity entity) => new(entity);
}

@ -1,32 +1,172 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using gaemstone.ECS.Internal;
using static flecs_hub.flecs; using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public readonly struct Entity public unsafe readonly partial struct Entity<TContext>
: IEquatable<Entity> : IEquatable<Entity<TContext>>
{ {
public static readonly Entity None = default; public static readonly Entity<TContext> None = default;
public readonly ecs_entity_t Value; public readonly World<TContext> World;
public uint Id => (uint)Value.Data; public readonly Entity Value;
public bool IsSome => Value.Data != 0; public uint NumericId => Value.NumericId;
public bool IsNone => Value.Data == 0; public bool IsNone => Value.IsNone;
public bool IsSome => Value.IsSome;
public Entity(ecs_entity_t value) => Value = value; public bool IsValid => EntityAccess.IsValid(World, this);
public bool IsAlive => IsSome && EntityAccess.IsAlive(World, this);
public bool Equals(Entity other) => Value.Data == other.Value.Data; public string? Name { get => EntityAccess.GetName(World, this); set => EntityAccess.SetName(World, this, value); }
public override bool Equals(object? obj) => (obj is Entity other) && Equals(other); public string? Symbol { get => EntityAccess.GetSymbol(World, this); set => EntityAccess.SetSymbol(World, this, value); }
public override int GetHashCode() => Value.Data.GetHashCode();
public EntityPath Path => EntityPath.From(World, this);
public EntityType<TContext> Type => new(World, ecs_get_type(World, this));
public Entity<TContext>? Parent
=> GetOrNull(World, GetTargets(FlecsBuiltIn.ChildOf).FirstOrDefault());
public IEnumerable<Entity<TContext>> Children
=> World.Term(new(FlecsBuiltIn.ChildOf, this)).GetAllEntities();
private Entity(World<TContext> world, Entity value)
{ World = world; Value = value; }
public static Entity<TContext> GetOrInvalid(World<TContext> world, Entity value)
=> new(world, value);
public static Entity<TContext>? GetOrNull(World<TContext> world, Entity value)
=> new Entity<TContext>(world, value).ValidOrNull();
public static Entity<TContext> GetOrThrow(World<TContext> world, Entity value)
=> new Entity<TContext>(world, value).ValidOrThrow();
public Entity<TContext>? ValidOrNull() => IsValid ? this : null;
public Entity<TContext> ValidOrThrow() => IsValid ? this
: throw new InvalidOperationException($"The entity {this} is not valid");
public Entity<TContext>? AliveOrNull() => IsAlive ? this : null;
public Entity<TContext> AliveOrThrow() => IsAlive ? this
: throw new InvalidOperationException($"The entity {this} is not alive");
public void Delete()
=> ecs_delete(World, this);
public Entity<TContext> CreateLookup<T>()
{
ref var lookup = ref Lookup<TContext>.Entity<T>.Value;
if (lookup == this) { /* Don't throw if lookup already has the same entity set. */ }
else if (lookup.IsSome) throw new InvalidOperationException(
$"The lookup for type {typeof(T)} in context {typeof(TContext)} is already in use by {lookup}");
lookup = this;
return this;
}
public EntityBuilder<TContext> NewChild(EntityPath? path = null)
=> World.New(path?.ThrowIfAbsolute(), this);
public Entity<TContext>? LookupChildOrNull(EntityPath path)
=> World.LookupPathOrNull(path.ThrowIfAbsolute(), this);
public Entity<TContext> LookupChildOrThrow(EntityPath path)
=> World.LookupPathOrThrow(path.ThrowIfAbsolute()!, this);
public Entity<TContext> ChildOf(Entity parent)
=> Add(FlecsBuiltIn.ChildOf, parent);
public bool IsDisabled => Has(FlecsBuiltIn.Disabled);
public bool IsEnabled => !Has(FlecsBuiltIn.Disabled);
public Entity<TContext> Disable() => Add(FlecsBuiltIn.Disabled);
public Entity<TContext> Enable() => Remove(FlecsBuiltIn.Disabled);
public Entity<TContext> Add(Id id) { EntityAccess.Add(World, this, id); return this; }
public Entity<TContext> Add(string symbol) => Add(World.LookupSymbolOrThrow(symbol));
public Entity<TContext> Add(Entity relation, Entity target) => Add(Id.Pair(relation, target));
public Entity<TContext> Add<TEntity>() => Add(World.Entity<TEntity>());
public Entity<TContext> Add<TRelation>(Entity target) => Add(World.Entity<TRelation>(), target);
public Entity<TContext> Add<TRelation, TTarget>() => Add(World.Entity<TRelation>(), World.Entity<TTarget>());
public Entity<TContext> Remove(Id id) { EntityAccess.Remove(World, this, id); return this; }
public Entity<TContext> Remove(string symbol) => Remove(World.LookupSymbolOrThrow(symbol));
public Entity<TContext> Remove(Entity relation, Entity target) => Remove(Id.Pair(relation, target));
public Entity<TContext> Remove<TEntity>() => Remove(World.Entity<TEntity>());
public Entity<TContext> Remove<TRelation>(Entity target) => Remove(World.Entity<TRelation>(), target);
public Entity<TContext> Remove<TRelation, TTarget>() => Remove(World.Entity<TRelation>(), World.Entity<TTarget>());
public bool Has(Id id) => EntityAccess.Has(World, this, id);
public bool Has(string symbol) => Has(World.LookupSymbolOrThrow(symbol));
public bool Has(Entity relation, Entity target) => Has(Id.Pair(relation, target));
public bool Has<TEntity>() => Has(World.Entity<TEntity>());
public bool Has<TRelation>(Entity target) => Has(World.Entity<TRelation>(), target);
public bool Has<TRelation, TTarget>() => Has(World.Entity<TRelation>(), World.Entity<TTarget>());
public T? GetOrNull<T>(Id id) where T : unmanaged => EntityAccess.GetOrNull<T>(World, this, id);
public T? GetOrNull<T>(Id id, T _ = null!) where T : class => EntityAccess.GetOrNull<T>(World, this, id);
public T GetOrThrow<T>(Id id) => EntityAccess.GetOrThrow<T>(World, this, id);
public ref T GetMut<T>(Id id) where T : unmanaged => ref EntityAccess.GetMut<T>(World, this, id);
public ref T GetRefOrNull<T>(Id id) where T : unmanaged => ref EntityAccess.GetRefOrNull<T>(World, this, id);
public ref T GetRefOrThrow<T>(Id id) where T : unmanaged => ref EntityAccess.GetRefOrThrow<T>(World, this, id);
public Entity<TContext> Modified(Id id) { EntityAccess.Modified(World, this, id); return this; }
public Entity<TContext> Set<T>(Id id, in T value) where T : unmanaged { EntityAccess.Set(World, this, id, value); return this; }
public Entity<TContext> Set<T>(Id id, T value) where T : class { EntityAccess.Set(World, this, id, value); return this; }
public T? GetOrNull<T>() where T : unmanaged => GetOrNull<T>(World.Entity<T>());
public T? GetOrNull<T>(T _ = null!) where T : class => GetOrNull<T>(World.Entity<T>());
public T GetOrThrow<T>() => GetOrThrow<T>(World.Entity<T>());
public ref T GetMut<T>() where T : unmanaged => ref GetMut<T>(World.Entity<T>());
public ref T GetRefOrNull<T>() where T : unmanaged => ref GetRefOrNull<T>(World.Entity<T>());
public ref T GetRefOrThrow<T>() where T : unmanaged => ref GetRefOrThrow<T>(World.Entity<T>());
public Entity<TContext> Modified<T>() => Modified(World.Entity<T>());
public Entity<TContext> Set<T>(in T value) where T : unmanaged => Set(World.Entity<T>(), value);
public Entity<TContext> Set<T>(T value) where T : class => Set(World.Entity<T>(), value);
public IEnumerable<Entity<TContext>> GetTargets(Entity relation)
{
foreach (var entity in EntityAccess.GetTargets(World, this, relation))
yield return new(World, entity);
}
public IEnumerable<Entity<TContext>> GetTargets(string symbol)
=> GetTargets(World.LookupSymbolOrThrow(symbol));
public IEnumerable<Entity<TContext>> GetTargets<TRelation>()
=> GetTargets(World.Entity<TRelation>());
public bool Equals(Entity<TContext> other)
{
#if DEBUG
// In DEBUG mode, we additionally check if the worlds the two compared
// values are from the same world. This accounts for the world being a
// stage, hence why it might not be the cheapest operation.
if (World != other.World) throw new ArgumentException(
"The specified values are not from the same world");
#endif
return Value == other.Value;
}
public override bool Equals(object? obj)
=> (obj is Entity<TContext> other) && Equals(other);
public override int GetHashCode()
=> Value.GetHashCode();
public override string? ToString() public override string? ToString()
=> IsSome ? $"Entity(0x{Value.Data.Data:X})" => Value.ToString();
: "Entity.None";
public static bool operator ==(Entity<TContext> left, Entity<TContext> right) => left.Equals(right);
public static bool operator !=(Entity<TContext> left, Entity<TContext> right) => !left.Equals(right);
public static implicit operator Entity (Entity<TContext> entity) => entity.Value;
public static implicit operator ecs_entity_t(Entity<TContext> entity) => entity.Value.Value;
public static bool operator ==(Entity left, Entity right) => left.Equals(right); public static implicit operator Id<TContext>(Entity<TContext> entity) => Id<TContext>.GetUnsafe(entity.World, entity);
public static bool operator !=(Entity left, Entity right) => !left.Equals(right); public static implicit operator Id (Entity<TContext> entity) => new(entity.Value.Value.Data);
public static implicit operator ecs_id_t (Entity<TContext> entity) => entity.Value.Value.Data;
public static implicit operator ecs_entity_t(Entity e) => e.Value; public static implicit operator Term (Entity<TContext> entity) => new(entity.Value);
public static implicit operator Id(Entity e) => new(e.Value.Data); public static implicit operator TermId(Entity<TContext> entity) => new(entity.Value);
public static implicit operator ecs_id_t(Entity e) => e.Value.Data;
} }

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

@ -1,17 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using gaemstone.ECS.Internal;
using gaemstone.ECS.Utility; using gaemstone.ECS.Utility;
using static flecs_hub.flecs; using static flecs_hub.flecs;
using static gaemstone.ECS.Internal.FlecsBuiltIn;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public class EntityBuilder public class EntityBuilder<TContext>
: EntityBase<EntityBuilder>
{ {
public override World World { get; } public World<TContext> World { get; }
/// <summary> Set to modify existing entity (optional). </summary> /// <summary> Set to modify existing entity (optional). </summary>
public Entity Id { get; set; } public Entity<TContext> Id { get; set; }
/// <summary> /// <summary>
/// Path of the entity. If no entity is provided, an entity with this path /// Path of the entity. If no entity is provided, an entity with this path
@ -27,7 +28,7 @@ public class EntityBuilder
/// function name, where these identifiers differ from the name they are /// function name, where these identifiers differ from the name they are
/// registered with in flecs. /// registered with in flecs.
/// </summary> /// </summary>
public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; } public EntityBuilder<TContext> Symbol(string symbol) { _symbol = symbol; return this; }
private string? _symbol = null; private string? _symbol = null;
/// <summary> /// <summary>
@ -38,51 +39,63 @@ public class EntityBuilder
/// <summary> Ids to add to the new or existing entity. </summary> /// <summary> Ids to add to the new or existing entity. </summary>
private readonly HashSet<Id> _toAdd = new(); private readonly HashSet<Id> _toAdd = new();
// (ChildOf, *) is handled explicitly, it won't be added to _toAdd.
private Entity _parent = Entity.None; private Entity _parent = Entity.None;
/// <summary> String expression with components to add. </summary> /// <summary> String expression with components to add. </summary>
public string? Expression { get; } public string? Expression { get; }
/// <summary> Actions to run once the entity has been created. </summary> /// <summary> Actions to run once the entity has been created. </summary>
private readonly List<Action<EntityRef>> _toSet = new(); private readonly List<Action<Entity>> _toSet = new();
public EntityBuilder(World world, EntityPath? path = null) public EntityBuilder(World<TContext> world, EntityPath? path = null)
{ World = world; Path = path; } { World = world; Path = path; }
public override EntityBuilder Add(Id id) public EntityBuilder(World<TContext> world, Entity parent, EntityPath? path = null)
: this(world, path)
{ {
// If adding a ChildOf relation, store the parent separately. // If given path is absolute, the new entity won't be created as a
if (id.AsPair(World) is (EntityRef relation, EntityRef target) && // child of the specified parent. Alternatively, EntityRef.NewChild
(relation == World.ChildOf)) { _parent = target; return this; } // can be used, which will throw when an absolute path is given.
if ((path?.IsRelative != false) && parent.IsSome) Add(ChildOf, parent);
}
public EntityBuilder<TContext> Add(Id id)
{
// If adding a ChildOf relation, store the parent separately.
if (id.RelationUnsafe == ChildOf)
{ _parent = id.TargetUnsafe; return this; }
if (_toAdd.Count == 31) throw new NotSupportedException( if (_toAdd.Count == 31) throw new NotSupportedException(
"Must not add more than 31 Ids at once with EntityBuilder"); "Must not add more than 31 Ids at once with EntityBuilder");
_toAdd.Add(id); _toAdd.Add(id);
return this; return this;
} }
public override EntityBuilder Remove(Id id) public EntityBuilder<TContext> Add(string symbol)
=> throw new NotSupportedException(); => Add(World.LookupSymbolOrThrow(symbol));
public override bool Has(Id id)
=> !id.IsWildcard ? _toAdd.Contains(id) public EntityBuilder<TContext> Add<TEntity>()
: throw new NotSupportedException(); // TODO: Support wildcard. => Add(World.Entity<TEntity>());
public override T? GetOrNull<T>(Id id) => throw new NotSupportedException(); public EntityBuilder<TContext> Add(Entity relation, Entity target)
public override T? GetOrNull<T>(Id id, T _ = null!) where T : class => throw new NotSupportedException(); => Add(World.Pair(relation, target));
public override T GetOrThrow<T>(Id id) => throw new NotSupportedException(); public EntityBuilder<TContext> Add<TRelation>(Entity target)
public override ref T GetMut<T>(Id id) => throw new NotSupportedException(); => Add(World.Pair<TRelation>(target));
public override ref T GetRefOrNull<T>(Id id) => throw new NotSupportedException(); public EntityBuilder<TContext> Add<TRelation, TTarget>()
public override ref T GetRefOrThrow<T>(Id id) => throw new NotSupportedException(); => Add(World.Pair<TRelation, TTarget>());
public override void Modified<T>(Id id) => throw new NotImplementedException();
public EntityBuilder<TContext> Set<T>(Id id, in T value) where T : unmanaged
public override EntityBuilder Set<T>(Id id, in T value)
// "in" can't be used with lambdas, so we make a local copy. // "in" can't be used with lambdas, so we make a local copy.
{ var copy = value; _toSet.Add(e => e.Set(id, copy)); return this; } { var copy = value; _toSet.Add(e => EntityAccess.Set(World, e, id, copy)); return this; }
public EntityBuilder<TContext> Set<T>(Id id, T value) where T : class
{ _toSet.Add(e => EntityAccess.Set(World, e, id, value)); return this; }
public override EntityBuilder Set<T>(Id id, T obj) public EntityBuilder<TContext> Set<T>(in T value) where T : unmanaged
{ _toSet.Add(e => e.Set(id, obj)); return this; } => Set(World.Entity<T>(), value);
public EntityBuilder<TContext> Set<T>(T value) where T : class
=> Set(World.Entity<T>(), value);
public unsafe EntityRef Build() public unsafe Entity<TContext> Build()
{ {
var parent = _parent; var parent = _parent;
@ -90,25 +103,25 @@ public class EntityBuilder
if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException( if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException(
"Entity already has parent set (via ChildOf), so path must not be absolute"); "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 specifies more than just a name, ensure the parent entity exists.
if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(World, parent, Path.Parent!); if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(World, Path.Parent!, parent);
} }
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var desc = new ecs_entity_desc_t { var desc = new ecs_entity_desc_t {
id = Id, id = Id,
name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default, _name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default,
symbol = alloc.AllocateCString(_symbol), _symbol = alloc.AllocateCString(_symbol),
add_expr = alloc.AllocateCString(Expression), _add_expr = alloc.AllocateCString(Expression),
use_low_id = UseLowId, use_low_id = UseLowId,
sep = CStringExtensions.ETX, _sep = CStringExtensions.Empty,
}; };
var add = desc.add; var index = 0; var add = desc.add; var index = 0;
if (parent.IsSome) add[index++] = ECS.Id.Pair(World.ChildOf, parent); if (parent.IsSome) add[index++] = World.Pair(ChildOf, parent);
foreach (var id in _toAdd) add[index++] = id; foreach (var id in _toAdd) add[index++] = id;
var entityId = ecs_entity_init(World, &desc); var entityId = ecs_entity_init(World, &desc);
var entity = new EntityRef(World, new(entityId)); var entity = Entity<TContext>.GetOrInvalid(World, new(entityId));
foreach (var action in _toSet) action(entity); foreach (var action in _toSet) action(entity);
return entity; return entity;

@ -4,11 +4,13 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using gaemstone.ECS.Internal;
using gaemstone.ECS.Utility; using gaemstone.ECS.Utility;
using static flecs_hub.flecs; using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
// TODO: Redo this with a single UTF8 byte array.
public class EntityPath public class EntityPath
{ {
private readonly byte[][] _parts; private readonly byte[][] _parts;
@ -48,6 +50,29 @@ public class EntityPath
return bytes; return bytes;
}).ToArray()) { } }).ToArray()) { }
public static unsafe EntityPath From(World world, Entity entity)
{
if (entity.IsNone) throw new ArgumentException(
"entity is Entity.None", nameof(entity));
var parts = new List<byte[]>(32);
do {
var name = ecs_get_name(world, entity).FlecsToBytes();
if (name != null) parts.Add(name);
else {
// If name is not set, use the numeric Id, instead.
var id = entity.NumericId.ToString();
var bytes = new byte[Encoding.UTF8.GetByteCount(id) + 1];
Encoding.UTF8.GetBytes(id, bytes);
parts.Add(bytes);
}
} while ((entity = new(ecs_get_target(world, entity, EcsChildOf, 0))).IsSome);
parts.Reverse();
return new(true, parts.ToArray());
}
public static bool TryParse(string str, [NotNullWhen(true)] out EntityPath? result) public static bool TryParse(string str, [NotNullWhen(true)] out EntityPath? result)
{ {
result = null; result = null;
@ -106,6 +131,10 @@ public class EntityPath
// throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character"); // throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character");
// } // }
public EntityPath ThrowIfAbsolute()
=> IsRelative ? this : throw new InvalidOperationException(
$"Path '{this}' must not be absolute");
public string[] GetParts() public string[] GetParts()
{ {
var result = new string[Count]; var result = new string[Count];
@ -135,7 +164,7 @@ public class EntityPath
internal static unsafe Entity Lookup( internal static unsafe Entity Lookup(
World world, Entity parent, EntityPath path, bool throwOnNotFound) World world, EntityPath path, Entity parent, bool throwOnNotFound)
{ {
var start = path.IsAbsolute ? Entity.None // If path is absolute, ignore parent and use root. var start = path.IsAbsolute ? Entity.None // If path is absolute, ignore parent and use root.
: parent.IsNone ? new(ecs_get_scope(world)) // If no parent is specified, use the current scope. : parent.IsNone ? new(ecs_get_scope(world)) // If no parent is specified, use the current scope.
@ -145,14 +174,13 @@ public class EntityPath
foreach (var part in path) foreach (var part in path)
fixed (byte* ptr = part.AsSpan()) { fixed (byte* ptr = part.AsSpan()) {
current = new(ecs_lookup_child(world, current, ptr)); current = new(ecs_lookup_child(world, current, ptr));
if (current.IsSome && ecs_is_alive(world, current)) continue; if (current.IsNone || !ecs_is_alive(world, current)) {
if (!throwOnNotFound) return Entity.None; if (!throwOnNotFound) return Entity.None;
else throw new EntityNotFoundException(
var startStr = EntityRef.CreateOrNull(world, start)?.GetFullPath().ToString() ?? start.ToString();
throw new World.EntityNotFoundException(
start.IsNone ? $"Entity at '{path}' not found" start.IsNone ? $"Entity at '{path}' not found"
: (start == parent) ? $"Child entity of '{startStr}' at '{path}' not found" : (start == parent) ? $"Child entity of '{From(world, start)}' at '{path}' not found"
: $"Entity at scope '{startStr}' at '{path}' not found"); : $"Entity at scope '{From(world, start)}' at '{path}' not found");
}
} }
return current; return current;
@ -160,7 +188,7 @@ public class EntityPath
/// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary> /// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary>
internal static unsafe Entity EnsureEntityExists( internal static unsafe Entity EnsureEntityExists(
World world, Entity parent, EntityPath path) World world, EntityPath path, Entity parent)
{ {
// If no parent is specified and path is relative, use the current scope. // If no parent is specified and path is relative, use the current scope.
if (parent.IsNone && path.IsRelative) parent = new(ecs_get_scope(world)); if (parent.IsNone && path.IsRelative) parent = new(ecs_get_scope(world));
@ -169,8 +197,8 @@ public class EntityPath
foreach (var part in path) foreach (var part in path)
fixed (byte* ptr = part.AsSpan()) fixed (byte* ptr = part.AsSpan())
if (skipLookup || (parent = new(ecs_lookup_child(world, parent, ptr))).IsNone) { if (skipLookup || (parent = new(ecs_lookup_child(world, parent, ptr))).IsNone) {
var desc = new ecs_entity_desc_t { name = ptr, sep = CStringExtensions.ETX }; var desc = new ecs_entity_desc_t { _name = ptr, _sep = CStringExtensions.Empty };
if (parent.IsSome) desc.add[0] = Id.Pair(world.ChildOf, parent); if (parent.IsSome) desc.add[0] = Id.Pair(FlecsBuiltIn.ChildOf, parent);
parent = new(ecs_entity_init(world, &desc)); parent = new(ecs_entity_init(world, &desc));
skipLookup = true; skipLookup = true;
} }
@ -179,30 +207,6 @@ public class EntityPath
} }
} }
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 public readonly ref struct UTF8View
{ {
private readonly ReadOnlySpan<byte> _bytes; private readonly ReadOnlySpan<byte> _bytes;

@ -1,169 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using gaemstone.ECS.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(this, EnsureRelativePath(path));
public EntityRef? LookupChild(EntityPath path)
=> World.LookupByPath(this, EnsureRelativePath(path)!);
public EntityRef LookupChildOrThrow(EntityPath path)
=> World.LookupByPathOrThrow(this, EnsureRelativePath(path)!);
private static EntityPath? EnsureRelativePath(EntityPath? path)
{
if (path?.IsAbsolute == true) throw new ArgumentException(
$"Path '{path}' must not be absolute", nameof(path)); return path;
}
public EntityRef? Parent
=> GetTargets(World.ChildOf).FirstOrDefault();
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(Id id) { ecs_add_id(World, this, id); return this; }
public override EntityRef Remove(Id id) { ecs_remove_id(World, this, id); return this; }
public override bool Has(Id id) => ecs_has_id(World, this, id);
public override T? GetOrNull<T>(Id id)
{
var ptr = ecs_get_id(World, this, id);
return (ptr != null) ? Unsafe.Read<T>(ptr) : null;
}
public override T? GetOrNull<T>(Id id, T _ = null!)
where T : class
{
var ptr = ecs_get_id(World, this, id);
return (T?)Unsafe.Read<ReferenceHandle>(ptr).Target;
}
public override T GetOrThrow<T>(Id id)
{
var ptr = ecs_get_id(World, this, id);
if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}");
if (typeof(T).IsValueType) return Unsafe.Read<T>(ptr);
else return (T)Unsafe.Read<ReferenceHandle>(ptr).Target!;
}
public override ref T GetRefOrNull<T>(Id 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>(Id 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>(Id 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>(Id id)
=> ecs_modified_id(World, this, id);
public override EntityRef Set<T>(Id 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>(Id id, T obj) where T : class
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
var size = (ulong)sizeof(ReferenceHandle);
// Dispose this handle afterwards, since Flecs clones it.
using var handle = ReferenceHandle.Alloc(obj);
if (ecs_set_id(World, this, id, size, &handle).Data == 0)
throw new InvalidOperationException();
return this;
}
private EntityRef? GetTarget(Entity relation, int index)
=> CreateOrNull(World, new(ecs_get_target(World, this, relation, index)));
public IEnumerable<EntityRef> GetTargets(Entity relation)
{ var index = 0; while (GetTarget(relation, index++) is EntityRef target) yield return target; }
public IEnumerable<EntityRef> GetTargets(string symbol)
=> GetTargets(World.LookupBySymbolOrThrow(symbol));
public IEnumerable<EntityRef> GetTargets<T>()
=> GetTargets(World.LookupByTypeOrThrow(typeof(T)));
public bool Equals(EntityRef? other) => (other is not null) && (World == other.World) && (Entity == other.Entity);
public override bool Equals(object? obj) => Equals(obj as EntityRef);
public override int GetHashCode() => HashCode.Combine(World, Entity);
public override string? ToString() => ecs_entity_str(World, this).FlecsToStringAndFree()!;
public static bool operator ==(EntityRef? left, EntityRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false);
public static bool operator !=(EntityRef? left, EntityRef? right) => !(left == right);
public static implicit operator Entity(EntityRef? e) => e?.Entity ?? default;
public static implicit operator ecs_entity_t(EntityRef? e) => e?.Entity.Value ?? default;
public static implicit operator Id(EntityRef? e) => new(e?.Entity.Value.Data ?? default);
public static implicit operator ecs_id_t(EntityRef? e) => e?.Entity.Value.Data ?? default;
}

@ -5,13 +5,13 @@ using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public unsafe readonly struct EntityType public unsafe readonly struct EntityType<TContext>
: IReadOnlyList<IdRef> : IReadOnlyList<Id<TContext>>
{ {
public World World { get; } public readonly World<TContext> World;
public ecs_type_t* Handle { get; } public readonly ecs_type_t* Handle;
public EntityType(World world, ecs_type_t* handle) public EntityType(World<TContext> world, ecs_type_t* handle)
{ World = world; Handle = handle; } { World = world; Handle = handle; }
public override string ToString() public override string ToString()
@ -19,7 +19,9 @@ public unsafe readonly struct EntityType
// IReadOnlyList implementation // IReadOnlyList implementation
public int Count => Handle->count; public int Count => Handle->count;
public IdRef this[int index] => new(World, new(Handle->array[index])); public Id<TContext> this[int index]
public IEnumerator<IdRef> GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; } => Id<TContext>.GetUnsafe(World, new(Handle->array[index]));
public IEnumerator<Id<TContext>> GetEnumerator()
{ for (var i = 0; i < Count; i++) yield return this[i]; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
} }

@ -0,0 +1,31 @@
using System;
namespace gaemstone.ECS;
public class EntityNotFoundException
: Exception
{
public EntityNotFoundException(string message)
: base(message) { }
}
public class ComponentNotFoundException
: Exception
{
public ComponentNotFoundException(string message)
: base(message) { }
}
public class FlecsException
: Exception
{
public FlecsException() : base() { }
public FlecsException(string message) : base(message) { }
}
public class FlecsAbortException
: FlecsException
{
internal FlecsAbortException()
: base("Abort was called by flecs") { }
}

@ -6,21 +6,21 @@ using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public unsafe class Filter public unsafe class Filter<TContext>
: IDisposable : IDisposable
{ {
public World World { get; } public World<TContext> World { get; }
public ecs_filter_t* Handle { get; } public ecs_filter_t* Handle { get; }
public Iterator.Variable? ThisVar { get { public Variable? ThisVar { get {
var index = ecs_filter_find_this_var(this); var index = ecs_filter_find_this_var(this);
return (index >= 0) ? new(index, "This") : null; return (index >= 0) ? new(index, "This") : null;
} } } }
internal Filter(World world, ecs_filter_t* handle) internal Filter(World<TContext> world, ecs_filter_t* handle)
{ World = world; Handle = handle; } { World = world; Handle = handle; }
public Filter(World world, FilterDesc desc) public Filter(World<TContext> world, FilterDesc desc)
{ {
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc); var flecsDesc = desc.ToFlecs(alloc);
@ -31,13 +31,22 @@ public unsafe class Filter
public void Dispose() public void Dispose()
=> ecs_filter_fini(this); => ecs_filter_fini(this);
public Iterator Iter() public FilterIterator<TContext> Iter()
=> new(World, IteratorType.Filter, ecs_filter_iter(World, this)); => new(ecs_filter_iter(World, this));
public override string ToString() public override string ToString()
=> ecs_filter_str(World, this).FlecsToStringAndFree()!; => ecs_filter_str(World, this).FlecsToStringAndFree()!;
public static implicit operator ecs_filter_t*(Filter q) => q.Handle; public static implicit operator ecs_filter_t*(Filter<TContext> filter) => filter.Handle;
}
public unsafe class FilterIterator<TContext>
: Iterator<TContext>
{
internal FilterIterator(ecs_iter_t value)
: base(value) { }
public override bool Next()
=> ecs_filter_next(Handle);
} }
public class FilterDesc public class FilterDesc
@ -68,7 +77,7 @@ public class FilterDesc
public unsafe ecs_filter_desc_t ToFlecs(IAllocator allocator) public unsafe ecs_filter_desc_t ToFlecs(IAllocator allocator)
{ {
var desc = new ecs_filter_desc_t { var desc = new ecs_filter_desc_t {
expr = allocator.AllocateCString(Expression), _expr = allocator.AllocateCString(Expression),
instanced = Instanced, instanced = Instanced,
entity = Entity, entity = Entity,
}; };

@ -0,0 +1,36 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public readonly struct Id
: IEquatable<Id>
{
public readonly ecs_id_t Value;
public IdFlags Flags => (IdFlags)(Value & ECS_ID_FLAGS_MASK);
public bool IsPair => ecs_id_is_pair(this);
public bool IsWildcard => ecs_id_is_wildcard(this);
public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 });
public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK });
public Id(ecs_id_t value) => Value = value;
public static Id Pair(Entity relation, Entity target)
=> new(ecs_make_pair(relation, target));
public bool Equals(Id other) => Value.Data == other.Value.Data;
public override bool Equals(object? obj) => (obj is Id other) && Equals(other);
public override int GetHashCode() => Value.Data.GetHashCode();
public override string? ToString()
=> (Flags != default) ? $"Id({Value.Data}, Flags={Flags})"
: $"Id({Value.Data})";
public static bool operator ==(Id left, Id right) => left.Equals(right);
public static bool operator !=(Id left, Id right) => !left.Equals(right);
public static implicit operator ecs_id_t(Id id) => id.Value;
public static implicit operator Term(Id id) => new(id);
}

@ -1,47 +1,66 @@
using System; using System;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs; using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public readonly struct Id public unsafe struct Id<TContext>
: IEquatable<Id> : IEquatable<Id<TContext>>
{ {
public readonly ecs_id_t Value; public readonly World<TContext> World;
public readonly Id Value;
public bool IsPair => ecs_id_is_pair(this); public IdFlags Flags => Value.Flags;
public bool IsWildcard => ecs_id_is_wildcard(this); public bool IsPair => Value.IsPair;
public bool IsWildcard => Value.IsWildcard;
public IdFlags Flags => (IdFlags)(Value & ECS_ID_FLAGS_MASK); public bool IsValid => ecs_id_is_valid(World, this);
public bool IsTag => ecs_id_is_tag(World, this);
public bool IsInUse => ecs_id_in_use(World, this);
public int Count => ecs_count_id(World, this);
public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 }); private Id(World<TContext> world, Id id)
public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK }); { World = world; Value = id; }
public Id(ecs_id_t value) => Value = value; public static Id<TContext> GetUnsafe(World<TContext> world, Id value)
=> new(world, value);
public static Id<TContext>? GetOrNull(World<TContext> world, Id value)
=> ecs_id_is_valid(world, value) ? new(world, value) : null;
public static Id<TContext> GetOrThrow(World<TContext> world, Id value)
=> ecs_id_is_valid(world, value) ? new(world, value) : throw new InvalidOperationException($"The id {value} is not valid");
public static Id Combine(IdFlags flags, Id id) public Entity<TContext>? AsEntity()
=> new((ulong)flags | id.Value); => !IsPair ? World.LookupAliveOrNull(new Entity(new() { Data = Value })) : null;
public (Entity<TContext> Relation, Entity<TContext> Target)? AsPair()
=> IsPair && (World.LookupAliveOrNull(Value.RelationUnsafe) is Entity<TContext> relation) &&
(World.LookupAliveOrNull(Value.TargetUnsafe ) is Entity<TContext> target )
? (relation, target) : null;
public static Id Pair(Entity relation, Entity target) public bool Equals(Id<TContext> other)
=> Combine(IdFlags.Pair, new( {
((relation.Value.Data << 32) & ECS_COMPONENT_MASK) | #if DEBUG
( target.Value.Data & ECS_ENTITY_MASK ))); // In DEBUG mode, we additionally check if the worlds the two compared
// values are from the same world. This accounts for the world being a
public EntityRef? AsEntity(World world) // stage, hence why it might not be the cheapest operation.
=> new IdRef(world, this).AsEntity(); if (World != other.World) throw new ArgumentException(
public (EntityRef Relation, EntityRef Target)? AsPair(World world) "The specified values are not from the same world");
=> new IdRef(world, this).AsPair(); #endif
return Value == other.Value;
public bool Equals(Id other) => Value.Data == other.Value.Data; }
public override bool Equals(object? obj) => (obj is Id other) && Equals(other); public override bool Equals(object? obj)
public override int GetHashCode() => Value.Data.GetHashCode(); => (obj is Id<TContext> other) && Equals(other);
public override int GetHashCode()
=> Value.GetHashCode();
public override string? ToString() public override string? ToString()
=> (Flags != default) ? $"Identifier(0x{Value.Data:X}, Flags={Flags})" => ecs_id_str(World, this).FlecsToStringAndFree()!;
: $"Identifier(0x{Value.Data:X})";
public static bool operator ==(Id<TContext> left, Id<TContext> right) => left.Equals(right);
public static bool operator !=(Id<TContext> left, Id<TContext> right) => !left.Equals(right);
public static bool operator ==(Id left, Id right) => left.Equals(right); public static implicit operator Id (Id<TContext> id) => id.Value;
public static bool operator !=(Id left, Id right) => !left.Equals(right); public static implicit operator ecs_id_t(Id<TContext> id) => id.Value.Value;
public static implicit operator ecs_id_t(Id i) => i.Value; public static implicit operator Term(Id<TContext> id) => new(id.Value);
} }
[Flags] [Flags]
@ -50,7 +69,5 @@ public enum IdFlags : ulong
Pair = 1ul << 63, Pair = 1ul << 63,
Override = 1ul << 62, Override = 1ul << 62,
Toggle = 1ul << 61, Toggle = 1ul << 61,
Or = 1ul << 60, And = 1ul << 60,
And = 1ul << 59,
Not = 1ul << 58,
} }

@ -1,61 +0,0 @@
using System;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class IdRef
: IEquatable<IdRef>
{
public World World { get; }
public Id Id { get; }
public IdFlags Flags => Id.Flags;
public bool IsPair => Id.IsPair;
public bool IsWildcard => Id.IsWildcard;
public bool IsValid => ecs_id_is_valid(World, this);
public bool IsInUse => ecs_id_in_use(World, this);
public int Count => ecs_count_id(World, this);
public IdRef(World world, Id id)
{ World = world; Id = id; }
public static IdRef Combine(IdFlags flags, IdRef id)
=> new(id.World, Id.Combine(flags, id));
public static IdRef Pair(World world, Entity relation, Entity target)
=> new(world, Id.Pair(relation, target));
public static IdRef Pair(Entity relation, EntityRef target)
=> Pair(target.World, relation, target);
public static IdRef Pair(EntityRef relation, Entity target)
=> Pair(relation.World, relation, target);
public static IdRef Pair(EntityRef relation, EntityRef target)
=> Pair(relation.World, relation, target);
public static IdRef Pair<TRelation>(World world, Entity target)
=> Pair(world, world.LookupByTypeOrThrow<TRelation>(), target);
public static IdRef Pair<TRelation>(EntityRef target)
=> Pair(target.World.LookupByTypeOrThrow<TRelation>(), target);
public static IdRef Pair<TRelation, TTarget>(World world)
=> Pair(world, world.LookupByTypeOrThrow<TRelation>(),
world.LookupByTypeOrThrow<TTarget>());
public EntityRef? AsEntity()
=> (Flags == default) ? World.LookupAlive(new Entity(new() { Data = Id })) : null;
public (EntityRef Relation, EntityRef Target)? AsPair()
=> IsPair && (World.LookupAlive(Id.RelationUnsafe) is EntityRef relation) &&
(World.LookupAlive(Id.TargetUnsafe ) is EntityRef target )
? (relation, target) : null;
public bool Equals(IdRef? other) => (other is not null) && (World == other.World) && (Id == other.Id);
public override bool Equals(object? obj) => Equals(obj as IdRef);
public override int GetHashCode() => HashCode.Combine(World, Id);
public override string? ToString() => ecs_id_str(World, this).FlecsToStringAndFree()!;
public static bool operator ==(IdRef? left, IdRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false);
public static bool operator !=(IdRef? left, IdRef? right) => !(left == right);
public static implicit operator Id(IdRef i) => i.Id;
public static implicit operator ecs_id_t(IdRef i) => i.Id.Value;
}

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS.Internal;
public unsafe static class EntityAccess
{
public static bool IsValid(World world, Entity entity)
=> ecs_is_valid(world, entity);
public static bool IsAlive(World world, Entity entity)
=> ecs_is_alive(world, entity);
public static string? GetName(World world, Entity entity)
=> ecs_get_name(world, entity).FlecsToString()!;
public static void SetName(World world, Entity entity, string? value)
{ using var alloc = TempAllocator.Use(); ecs_set_name(world, entity, alloc.AllocateCString(value)); }
public static string? GetSymbol(World world, Entity entity)
=> ecs_get_symbol(world, entity).FlecsToString()!;
public static void SetSymbol(World world, Entity entity, string? value)
{ using var alloc = TempAllocator.Use(); ecs_set_symbol(world, entity, alloc.AllocateCString(value)); }
public static void Add(World world, Entity entity, Id id)
=> ecs_add_id(world, entity, id);
public static void Remove(World world, Entity entity, Id id)
=> ecs_remove_id(world, entity, id);
public static bool Has(World world, Entity entity, Id id)
=> ecs_has_id(world, entity, id);
public static IEnumerable<Entity> GetTargets(World world, Entity entity, Entity relation)
{
Entity GetTarget(int index)
=> new(ecs_get_target(world, entity, relation, index));
for (var i = 0; GetTarget(i) is { IsSome: true } target; i++)
yield return target;
}
public static T? GetOrNull<T>(World world, Entity entity, Id id)
where T : unmanaged
{
var ptr = ecs_get_id(world, entity, id);
return (ptr != null) ? Unsafe.Read<T>(ptr) : null;
}
public static T? GetOrNull<T>(World world, Entity entity, Id id, T _ = null!)
where T : class
{
var ptr = ecs_get_id(world, entity, id);
return (T?)Unsafe.Read<ReferenceHandle>(ptr).Target;
}
public static T GetOrThrow<T>(World world, Entity entity, Id id)
{
var ptr = ecs_get_id(world, entity, id);
if (ptr == null) throw new ComponentNotFoundException($"Component {id} not found on {entity}");
if (typeof(T).IsValueType) return Unsafe.Read<T>(ptr);
else return (T)Unsafe.Read<ReferenceHandle>(ptr).Target!;
}
public static ref T GetMut<T>(World world, Entity entity, Id id)
where T : unmanaged
{
var ptr = ecs_get_mut_id(world, entity, id);
// NOTE: Value is added if it doesn't exist on the entity.
return ref Unsafe.AsRef<T>(ptr);
}
public static ref T GetRefOrNull<T>(World world, Entity entity, Id id)
where T : unmanaged
{
var @ref = ecs_ref_init_id(world, entity, id);
var ptr = ecs_ref_get_id(world, &@ref, id);
return ref (ptr != null) ? ref Unsafe.AsRef<T>(ptr)
: ref Unsafe.NullRef<T>();
}
public static ref T GetRefOrThrow<T>(World world, Entity entity, Id id)
where T : unmanaged
{
ref var ptr = ref GetRefOrNull<T>(world, entity, id);
if (Unsafe.IsNullRef(ref ptr)) throw new Exception(
$"Component {typeof(T)} not found on {entity}");
return ref ptr;
}
public static void Modified(World world, Entity entity, Id id)
=> ecs_modified_id(world, entity, id);
public static Entity Set<T>(World world, Entity entity, Id id, in T value)
where T : unmanaged
{
var size = (ulong)Unsafe.SizeOf<T>();
fixed (T* ptr = &value)
if (ecs_set_id(world, entity, id, size, ptr).Data == 0)
throw new InvalidOperationException();
return entity;
}
public static Entity Set<T>(World world, Entity entity, Id id, T value)
where T : class
{
if (value == null) throw new ArgumentNullException(nameof(value));
var size = (ulong)sizeof(ReferenceHandle);
// Dispose entity handle afterwards, since Flecs clones it.
using var handle = ReferenceHandle.Alloc(value);
if (ecs_set_id(world, entity, id, size, &handle).Data == 0)
throw new InvalidOperationException();
return entity;
}
}

@ -0,0 +1,11 @@
using static flecs_hub.flecs;
namespace gaemstone.ECS.Internal;
public static class FlecsBuiltIn
{
public static Entity ChildOf { get; } = new(pinvoke_EcsChildOf());
// FIXME: Hopefully flecs-cs will expose this one day.
public static Entity Disabled { get; } = new(new() { Data = FLECS_HI_COMPONENT_ID + 7 });
}

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS.Internal;
public unsafe class Iterator
: IDisposable
{
public ecs_iter_t* Handle;
private readonly bool _handleIsOwned;
public World World => new(Handle->world);
public int Count => Handle->count;
public IteratorFlags Flags => (IteratorFlags)(uint)Handle->flags;
public bool IsValid => (Flags & IteratorFlags.IsValid) != 0;
public TimeSpan DeltaTime => TimeSpan.FromSeconds(Handle->delta_time);
public TimeSpan DeltaSystemTime => TimeSpan.FromSeconds(Handle->delta_system_time);
public Iterator(ecs_iter_t* handle)
{
Handle = handle;
_handleIsOwned = false;
}
public Iterator(ecs_iter_t value)
{
Handle = GlobalHeapAllocator.Instance.AllocateCopy(value);
_handleIsOwned = true;
}
public void Dispose()
{
if (Handle == null) return;
// When an iterator is iterated until completion,
// ecs_iter_fini will be called automatically.
if (IsValid) ecs_iter_fini(Handle);
if (_handleIsOwned)
GlobalHeapAllocator.Instance.Free(Handle);
Handle = null;
}
public Entity GetVar(Variable var)
=> new(ecs_iter_get_var(Handle, var.Index));
public Iterator SetVar(Variable var, Entity entity)
{ ecs_iter_set_var(Handle, var.Index, entity); return this; }
public virtual bool Next()
=> ecs_iter_next(Handle);
public Entity First()
=> new(ecs_iter_first(Handle));
public bool Any()
=> ecs_iter_is_true(Handle);
public IEnumerable<Entity> GetAllEntities()
{
while (Next())
for (var i = 0; i < Count; i++)
yield return Entity(i);
}
public bool FieldIsSet(int index)
=> ecs_field_is_set(Handle, index);
public Id FieldId(int index)
=> new(ecs_field_id(Handle, index));
public bool FieldIs(int index, Id id)
=> ecs_field_id(Handle, index) == id.Value;
public Entity Entity(int index)
=> new(Handle->entities[index]);
public Span<T> Field<T>(int index)
where T : unmanaged
{
var size = (ulong)Unsafe.SizeOf<T>();
var isSelf = ecs_field_is_self(Handle, index);
var pointer = ecs_field_w_size(Handle, size, index);
return new(pointer, isSelf ? Count : 1);
}
public Span<T> FieldOrEmpty<T>(int index) where T : unmanaged
=> FieldIsSet(index) ? Field<T>(index) : Span<T>.Empty;
public SpanToRef<T> Field<T>(int index, T _ = null!) where T : class
=> new(Field<ReferenceHandle>(index));
public SpanToRef<T> FieldOrEmpty<T>(int index, T _ = null!) where T : class
=> FieldIsSet(index) ? Field<T>(index) : SpanToRef<T>.Empty;
public override string ToString()
=> ecs_iter_str(Handle).FlecsToStringAndFree()!;
}
public readonly ref struct SpanToRef<T>
where T : class
{
public static SpanToRef<T> Empty => default;
private readonly Span<ReferenceHandle> _span;
public int Length => _span.Length;
public T this[int index] => (T)_span[index].Target!;
public T? GetOrNull(int index) => ((index >= 0) && (index < Length)) ? this[index] : null;
internal SpanToRef(Span<ReferenceHandle> span) => _span = span;
}

@ -0,0 +1,9 @@
namespace gaemstone.ECS.Internal;
internal static class Lookup<TContext>
{
internal static class Entity<T>
{
public static Entity Value;
}
}

@ -1,135 +1,79 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Linq;
using gaemstone.ECS.Internal;
using gaemstone.ECS.Utility; using gaemstone.ECS.Utility;
using static flecs_hub.flecs; using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public unsafe class Iterator public unsafe class Iterator<TContext> : Iterator
: IEnumerable<Iterator> , IEnumerable<Iterator<TContext>>
, IDisposable
{ {
public World World { get; } public new World<TContext> World => new(base.World);
public IteratorType? Type { get; }
public ecs_iter_t Value;
public bool Completed { get; private set; } public Iterator(ecs_iter_t* handle) : base(handle) { }
public int Count => Value.count; public Iterator(ecs_iter_t value) : base(value) { }
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) public static TermIterator<TContext> FromTerm(World world, Term term)
{ World = world; Type = type; Value = value; }
public static Iterator FromTerm(World world, Term term)
{ {
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var flecsTerm = term.ToFlecs(alloc); var flecsTerm = term.ToFlecs(alloc);
var flecsIter = ecs_term_iter(world, &flecsTerm); var flecsIter = ecs_term_iter(world, &flecsTerm);
return new(world, IteratorType.Term, flecsIter); return new(flecsIter);
}
public void Dispose()
{
// When an iterator is iterated until completion,
// ecs_iter_fini will be called automatically.
if (!Completed)
fixed (ecs_iter_t* ptr = &Value)
ecs_iter_fini(ptr);
}
public EntityRef GetVar(Variable var)
{
fixed (ecs_iter_t* ptr = &Value)
return new(World, new(ecs_iter_get_var(ptr, var.Index)));
}
public Iterator SetVar(Variable var, Entity entity)
{
fixed (ecs_iter_t* ptr = &Value)
ecs_iter_set_var(ptr, var.Index, entity);
return this;
}
public bool Next()
{
fixed (ecs_iter_t* ptr = &Value) {
var result = Type switch {
IteratorType.Term => ecs_term_next(ptr),
IteratorType.Filter => ecs_filter_next(ptr),
IteratorType.Query => ecs_query_next(ptr),
IteratorType.Rule => ecs_rule_next(ptr),
_ => ecs_iter_next(ptr),
};
Completed = !result;
return result;
}
} }
public new Entity<TContext>? GetVar(Variable var)
=> Entity<TContext>.GetOrNull(World, base.GetVar(var));
public new Iterator<TContext> SetVar(Variable var, Entity entity)
=> (Iterator<TContext>)base.SetVar(var, entity);
public bool FieldIsSet(int index) public new Entity<TContext>? First()
{ => Entity<TContext>.GetOrNull(World, base.First());
fixed (ecs_iter_t* ptr = &Value) public new IEnumerable<Entity<TContext>> GetAllEntities()
return ecs_field_is_set(ptr, index); => base.GetAllEntities().Select(e => Entity<TContext>.GetOrInvalid(World, e));
}
public bool FieldIs(int index, Id id) public new Id<TContext> FieldId(int index)
{ => Id<TContext>.GetUnsafe(World, base.FieldId(index));
fixed (ecs_iter_t* ptr = &Value)
return ecs_field_id(ptr, index) == id.Value;
}
// TODO: Potentially misleading, doesn't check the field's backing data type. public new Entity<TContext> Entity(int index)
// The id might be "(Identifier, Name)", but its data type "Identifier". => Entity<TContext>.GetOrInvalid(World, base.Entity(index));
public bool FieldIs<T>(int index)
=> FieldIs(index, World.LookupByType<T>());
public IdRef FieldId(int index) // IEnumerable implementation
{ public virtual IEnumerator<Iterator<TContext>> GetEnumerator()
fixed (ecs_iter_t* ptr = &Value) { while (Next()) yield return this; }
return new(World, new(ecs_field_id(ptr, index))); IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
} }
[Flags]
public EntityRef Entity(int index) public enum IteratorFlags : uint
=> new(World, new(Value.entities[index]));
public Span<T> Field<T>(int index)
where T : unmanaged
{ {
fixed (ecs_iter_t* ptr = &Value) { /// <summary> Does iterator contain valid result. </summary>
var size = (ulong)Unsafe.SizeOf<T>(); IsValid = EcsIterIsValid,
var isSelf = ecs_field_is_self(ptr, index); /// <summary> Does iterator provide (component). </summary>
var pointer = ecs_field_w_size(ptr, size, index); NoData = EcsIterNoData,
return new(pointer, isSelf ? Count : 1); /// <summary> Is iterator instanced. </summary>
} IsInstanced = EcsIterIsInstanced,
/// <summary> Does result have shared terms. </summary>
HasShared = EcsIterHasShared,
/// <summary> Result only populates table. </summary>
TableOnly = EcsIterTableOnly,
/// <summary> Treat terms with entity subject as optional. </summary>
EntityOptional = EcsIterEntityOptional,
/// <summary> Iterator has no results. </summary>
NoResults = EcsIterNoResults,
/// <summary> Only evaluate non-this terms. </summary>
IgnoreThis = EcsIterIgnoreThis,
/// <summary> Does iterator have conditionally set fields. </summary>
MatchVar = EcsIterMatchVar,
/// <summary> </summary>
HasCondSet = EcsIterHasCondSet,
/// <summary> Profile iterator performance. </summary>
Profile = EcsIterProfile,
} }
public SpanToRef<T> Field<T>(int index, T _ = null!) where T : class
=> new(Field<ReferenceHandle>(index));
public Span<T> FieldOrEmpty<T>(int index) where T : unmanaged
=> FieldIsSet(index) ? Field<T>(index) : Span<T>.Empty;
public SpanToRef<T> FieldOrEmpty<T>(int index, T _ = null!) where T : class
=> FieldIsSet(index) ? Field(index, _) : SpanToRef<T>.Empty;
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 class Variable public class Variable
{ {
public int Index { get; } public int Index { get; }
@ -138,21 +82,11 @@ public unsafe class Iterator
{ Index = index; Name = name; } { Index = index; Name = name; }
} }
public readonly ref struct SpanToRef<T> public unsafe class TermIterator<TContext>
where T : class : Iterator<TContext>
{
public static SpanToRef<T> Empty => default;
private readonly Span<ReferenceHandle> _span;
public int Length => _span.Length;
public T? this[int index] => (T?)_span[index].Target;
internal SpanToRef(Span<ReferenceHandle> span) => _span = span;
}
}
public enum IteratorType
{ {
Term, internal TermIterator(ecs_iter_t value)
Filter, : base(value) { }
Query, public override bool Next()
Rule, => ecs_term_next(Handle);
} }

@ -7,29 +7,35 @@ namespace gaemstone.ECS;
public static class ObserverExtensions public static class ObserverExtensions
{ {
public static unsafe EntityRef InitObserver(this EntityRef entity, public static unsafe Entity<TContext> InitObserver<TContext>(this Entity<TContext> entity,
Entity @event, FilterDesc filter, Action<Iterator> callback) FilterDesc filter, Action<Iterator<TContext>> callback, params Entity[] events)
{ {
if (events.Length == 0) throw new ArgumentException("Must specify at least 1 event", nameof(events));
if (events.Length > 8) throw new ArgumentException("Must specify at most 8 events", nameof(events));
var world = entity.World; var world = entity.World;
var internalCallback = (nint iterPtr) => callback(new((ecs_iter_t*)iterPtr));
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var desc = new ecs_observer_desc_t { var desc = new ecs_observer_desc_t {
entity = entity, entity = entity,
filter = filter.ToFlecs(alloc), filter = filter.ToFlecs(alloc),
binding_ctx = (void*)CallbackContextHelper.Create((world, callback)), binding_ctx = (void*)CallbackContextHelper.Create(internalCallback),
binding_ctx_free = new() { Data = new() { Pointer = &FreeContext } }, binding_ctx_free = new() { Data = new() { Pointer = &FreeContext } },
callback = new() { Data = new() { Pointer = &Callback } }, callback = new() { Data = new() { Pointer = &Callback } },
}; };
desc.events[0] = @event; var span = desc.events;
return new(world, new(ecs_observer_init(world, &desc))); for (var i = 0; i < events.Length; i++)
span[i] = events[i];
if (ecs_observer_init(world, &desc).Data == default)
throw new InvalidOperationException();
return entity;
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
private static unsafe void Callback(ecs_iter_t* iter) private static unsafe void Callback(ecs_iter_t* iter)
{ => CallbackContextHelper
var (world, callback) = CallbackContextHelper .Get<Action<nint>>((nint)iter->binding_ctx)
.Get<(World, Action<Iterator>)>((nint)iter->binding_ctx); .Invoke((nint)iter);
callback(new Iterator(world, null, *iter));
}
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
private static unsafe void FreeContext(void* context) private static unsafe void FreeContext(void* context)

@ -4,16 +4,16 @@ using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public unsafe class Query public unsafe class Query<TContext>
: IDisposable : IDisposable
{ {
public World World { get; } public World<TContext> World { get; }
public ecs_query_t* Handle { get; } public ecs_query_t* Handle { get; }
public Filter Filter { get; } public Filter<TContext> Filter { get; }
public Iterator.Variable? ThisVar => Filter.ThisVar; public Variable? ThisVar => Filter.ThisVar;
public Query(World world, QueryDesc desc) public Query(World<TContext> world, QueryDesc desc)
{ {
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc); var flecsDesc = desc.ToFlecs(alloc);
@ -25,24 +25,33 @@ public unsafe class Query
public void Dispose() public void Dispose()
=> ecs_query_fini(this); => ecs_query_fini(this);
public Iterator Iter() public QueryIterator<TContext> Iter()
=> new(World, IteratorType.Query, ecs_query_iter(World, this)); => new(ecs_query_iter(World, this));
public override string ToString() public override string ToString()
=> ecs_query_str(Handle).FlecsToStringAndFree()!; => ecs_query_str(Handle).FlecsToStringAndFree()!;
public static implicit operator ecs_query_t*(Query q) => q.Handle; public static implicit operator ecs_query_t*(Query<TContext> query) => query.Handle;
}
public unsafe class QueryIterator<TContext>
: Iterator<TContext>
{
internal QueryIterator(ecs_iter_t value)
: base(value) { }
public override bool Next()
=> ecs_query_next(Handle);
} }
public class QueryDesc : FilterDesc public class QueryDesc : FilterDesc
{ {
public QueryDesc(string expression) : base(expression) { }
public QueryDesc(params Term[] terms) : base(terms) { } public QueryDesc(params Term[] terms) : base(terms) { }
public QueryDesc(string expression) : base(expression) { }
public new unsafe ecs_query_desc_t ToFlecs(IAllocator allocator) public new unsafe ecs_query_desc_t ToFlecs(IAllocator alloc)
{ {
var desc = new ecs_query_desc_t { var desc = new ecs_query_desc_t {
filter = base.ToFlecs(allocator), filter = base.ToFlecs(alloc),
// TODO: Implement more Query features. // TODO: Implement more Query features.
}; };
return desc; return desc;

@ -6,18 +6,18 @@ using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public unsafe class Rule public unsafe class Rule<TContext>
: IDisposable : IDisposable
{ {
public World World { get; } public World<TContext> World { get; }
public ecs_rule_t* Handle { get; } public ecs_rule_t* Handle { get; }
public Filter Filter { get; } public Filter<TContext> Filter { get; }
private VariableCollection? _variables; private VariableCollection? _variables;
public VariableCollection Variables => _variables ??= new(this); public VariableCollection Variables => _variables ??= new(this);
public Iterator.Variable? ThisVar => Filter.ThisVar; public Variable? ThisVar => Filter.ThisVar;
public Rule(World world, FilterDesc desc) public Rule(World<TContext> world, FilterDesc desc)
{ {
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var flecsDesc = desc.ToFlecs(alloc); var flecsDesc = desc.ToFlecs(alloc);
@ -29,48 +29,53 @@ public unsafe class Rule
public void Dispose() public void Dispose()
=> ecs_rule_fini(this); => ecs_rule_fini(this);
public Iterator Iter() public RuleIterator<TContext> Iter()
=> new(World, IteratorType.Rule, ecs_rule_iter(World, this)); => new(ecs_rule_iter(World, this));
public override string ToString() public override string ToString()
=> ecs_rule_str(Handle).FlecsToStringAndFree()!; => ecs_rule_str(Handle).FlecsToStringAndFree()!;
public static implicit operator ecs_rule_t*(Rule q) => q.Handle; public static implicit operator ecs_rule_t*(Rule<TContext> rule) => rule.Handle;
public unsafe class VariableCollection public unsafe class VariableCollection
: IReadOnlyCollection<Iterator.Variable> : IReadOnlyCollection<Variable>
{ {
private readonly List<Iterator.Variable> _variables = new(); private readonly List<Variable> _variables = new();
public Rule Rule { get; }
public int Count => _variables.Count; public int Count => _variables.Count;
public Iterator.Variable? this[int index] public Variable? this[int index]
=> _variables.Find(v => v.Index == index); => _variables.Find(v => v.Index == index);
public Iterator.Variable? this[string name] public Variable? this[string name]
=> _variables.Find(v => string.Equals( => _variables.Find(v => string.Equals(
v.Name, name, StringComparison.OrdinalIgnoreCase)); v.Name, name, StringComparison.OrdinalIgnoreCase));
internal VariableCollection(Rule rule) internal VariableCollection(ecs_rule_t* handle)
{ {
Rule = rule; // Find the $this variable, if the rule has one.
var thisIndex = ecs_filter_find_this_var(ecs_rule_get_filter(handle));
// Find the $This variable, if the rule has one. if (thisIndex >= 0) _variables.Add(new(thisIndex, "this"));
var thisIndex = ecs_filter_find_this_var(ecs_rule_get_filter(Rule));
if (thisIndex >= 0) _variables.Add(new(thisIndex, "This"));
// Find all the other "accessible" variables. // Find all the other "accessible" variables.
var count = ecs_rule_var_count(Rule); var count = ecs_rule_var_count(handle);
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
if ((i == thisIndex) || !ecs_rule_var_is_entity(Rule, i)) continue; if ((i == thisIndex) || !ecs_rule_var_is_entity(handle, i)) continue;
var name = ecs_rule_var_name(Rule, i).FlecsToString()!; var name = ecs_rule_var_name(handle, i).FlecsToString()!;
_variables.Add(new(i, name)); _variables.Add(new(i, name));
} }
} }
// IEnumerable implementation // IEnumerable implementation
public IEnumerator<Iterator.Variable> GetEnumerator() => _variables.GetEnumerator(); public IEnumerator<Variable> GetEnumerator() => _variables.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _variables.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _variables.GetEnumerator();
} }
} }
public unsafe class RuleIterator<TContext>
: Iterator<TContext>
{
internal RuleIterator(ecs_iter_t value)
: base(value) { }
public override bool Next()
=> ecs_rule_next(Handle);
}

@ -7,48 +7,31 @@ namespace gaemstone.ECS;
public static class SystemExtensions public static class SystemExtensions
{ {
public static unsafe EntityRef InitSystem(this EntityRef entity, public static unsafe Entity<TContext> InitSystem<TContext>(this Entity<TContext> entity,
Entity phase, QueryDesc query, Action<Iterator> callback) QueryDesc query, Action<Iterator<TContext>> callback)
{ {
var world = entity.World; var world = entity.World;
entity.Add(world.DependsOn, phase); var internalCallback = (nint iter) =>
callback(new((ecs_iter_t*)iter));
using var alloc = TempAllocator.Use(); using var alloc = TempAllocator.Use();
var desc = new ecs_system_desc_t { var desc = new ecs_system_desc_t {
entity = entity, entity = entity,
query = query.ToFlecs(alloc), query = query.ToFlecs(alloc),
binding_ctx = (void*)CallbackContextHelper.Create((world, callback)), binding_ctx = (void*)CallbackContextHelper.Create(internalCallback),
binding_ctx_free = new() { Data = new() { Pointer = &FreeContext } }, binding_ctx_free = new() { Data = new() { Pointer = &FreeContext } },
callback = new() { Data = new() { Pointer = &Callback } }, callback = new() { Data = new() { Pointer = &Callback } },
}; };
return new(world, new(ecs_system_init(world, &desc))); if (ecs_system_init(world, &desc).Data == default)
throw new InvalidOperationException();
return entity;
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
private static unsafe void Callback(ecs_iter_t* iter) private static unsafe void Callback(ecs_iter_t* iter)
{ => CallbackContextHelper
var (world, callback) = CallbackContextHelper .Get<Action<nint>>((nint)iter->binding_ctx)
.Get<(World, Action<Iterator>)>((nint)iter->binding_ctx); .Invoke((nint)iter);
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] [UnmanagedCallersOnly]
private static unsafe void FreeContext(void* context) private static unsafe void FreeContext(void* context)

@ -10,8 +10,8 @@ public class Term
public TermId? Source { get; set; } public TermId? Source { get; set; }
public TermId? Relation { get; set; } public TermId? Relation { get; set; }
public TermId? Target { get; set; } public TermId? Target { get; set; }
public TermInOutKind InOut { get; set; } public TermInOutKind InOutKind { get; set; }
public TermOperKind Oper { get; set; } public TermOperKind OperKind { get; set; }
public IdFlags Flags { get; set; } public IdFlags Flags { get; set; }
public Term() { } public Term() { }
@ -19,26 +19,22 @@ public class Term
public Term(TermId relation, TermId target) public Term(TermId relation, TermId target)
{ Relation = relation; Target = target; } { Relation = relation; Target = target; }
public static implicit operator Term(EntityRef entity) => new(entity); public Term None { get { InOutKind = TermInOutKind.None; return this; } }
public static implicit operator Term(Entity entity) => new(entity); public Term In { get { InOutKind = TermInOutKind.In; return this; } }
public static implicit operator Term(IdRef id) => new(id); public Term Out { get { InOutKind = TermInOutKind.Out; return this; } }
public static implicit operator Term(Id id) => new(id); public Term InOut { get { InOutKind = TermInOutKind.InOut; return this; } }
public Term None { get { InOut = TermInOutKind.None; return this; } } public Term Or { get { OperKind = TermOperKind.Or; return this; } }
public Term In { get { InOut = TermInOutKind.In; return this; } } public Term Not { get { OperKind = TermOperKind.Not; return this; } }
public Term Out { get { InOut = TermInOutKind.Out; return this; } } public Term Optional { get { OperKind = TermOperKind.Optional; 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() { public ecs_term_t ToFlecs(IAllocator allocator) => new() {
id = Id, id = Id,
src = Source ?.ToFlecs(allocator) ?? default, src = Source ?.ToFlecs(allocator) ?? default,
first = Relation?.ToFlecs(allocator) ?? default, first = Relation?.ToFlecs(allocator) ?? default,
second = Target ?.ToFlecs(allocator) ?? default, second = Target ?.ToFlecs(allocator) ?? default,
inout = (ecs_inout_kind_t)InOut, inout = (ecs_inout_kind_t)InOutKind,
oper = (ecs_oper_kind_t)Oper, oper = (ecs_oper_kind_t)OperKind,
id_flags = (ecs_id_t)(ulong)Flags, id_flags = (ecs_id_t)(ulong)Flags,
}; };
} }
@ -65,7 +61,7 @@ public enum TermOperKind
public class TermId public class TermId
{ {
public static TermId This { get; } = new("$This"); public static TermId This { get; } = new("$this");
public Entity Id { get; } public Entity Id { get; }
public string? Name { get; } public string? Name { get; }
@ -82,13 +78,9 @@ public class TermId
} else Name = name; } 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() { public ecs_term_id_t ToFlecs(IAllocator allocator) => new() {
id = Id, id = Id,
name = allocator.AllocateCString(Name), _name = allocator.AllocateCString(Name),
trav = Traverse, trav = Traverse,
flags = (ecs_flags32_t)(uint)Flags flags = (ecs_flags32_t)(uint)Flags
}; };
@ -103,6 +95,8 @@ public enum TermTraversalFlags : uint
Up = EcsUp, Up = EcsUp,
/// <summary> Match by traversing downwards (derived, cannot be set). </summary> /// <summary> Match by traversing downwards (derived, cannot be set). </summary>
Down = EcsDown, Down = EcsDown,
/// <summary> Match all entities encountered through traversal. </summary>
TraverseAll = EcsTraverseAll,
/// <summary> Sort results breadth first. </summary> /// <summary> Sort results breadth first. </summary>
Cascade = EcsCascade, Cascade = EcsCascade,
/// <summary> Short for up(ChildOf). </summary> /// <summary> Short for up(ChildOf). </summary>
@ -111,6 +105,8 @@ public enum TermTraversalFlags : uint
IsVariable = EcsIsVariable, IsVariable = EcsIsVariable,
/// <summary> Term id is an entity. </summary> /// <summary> Term id is an entity. </summary>
IsEntity = EcsIsEntity, IsEntity = EcsIsEntity,
/// <summary> Prevent observer from triggering on term. </summary> /// <summary> Term id is a name (don't attempt to lookup as entity). </summary>
IsName = EcsIsName,
/// <summary> revent observer from triggering on term. </summary>
Filter = EcsFilter, Filter = EcsFilter,
} }

@ -15,18 +15,19 @@ public interface IAllocator
public unsafe static class AllocatorExtensions public unsafe static class AllocatorExtensions
{ {
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 Span<T> Allocate<T>(this IAllocator allocator, int count) where T : unmanaged public static Span<T> Allocate<T>(this IAllocator allocator, int count) where T : unmanaged
=> new((void*)allocator.Allocate(sizeof(T) * count), count); => new((void*)allocator.Allocate(sizeof(T) * count), count);
public static void Free<T>(this IAllocator allocator, Span<T> span) where T : unmanaged public static void Free<T>(this IAllocator allocator, Span<T> span) where T : unmanaged
=> allocator.Free((nint)Unsafe.AsPointer(ref span[0])); => allocator.Free((nint)Unsafe.AsPointer(ref span[0]));
public static Span<T> AllocateCopy<T>(this IAllocator allocator, ReadOnlySpan<T> orig) where T : unmanaged public static T* AllocateCopy<T>(this IAllocator allocator, T orig) where T : unmanaged
{ var copy = allocator.Allocate<T>(orig.Length); orig.CopyTo(copy); return copy; } { var ptr = allocator.Allocate<T>(); *ptr = orig; return ptr; }
public static T* Allocate<T>(this IAllocator allocator) where T : unmanaged
public static ref T Allocate<T>(this IAllocator allocator) where T : unmanaged => (T*)allocator.Allocate(sizeof(T));
=> ref Unsafe.AsRef<T>((void*)allocator.Allocate(sizeof(T))); public static void Free<T>(this IAllocator allocator, T* ptr) where T : unmanaged
public static void Free<T>(this IAllocator allocator, ref T value) where T : unmanaged => allocator.Free((nint)ptr);
=> allocator.Free((nint)Unsafe.AsPointer(ref value));
public static CString AllocateCString(this IAllocator allocator, string? value) public static CString AllocateCString(this IAllocator allocator, string? value)
{ {

@ -8,7 +8,6 @@ namespace gaemstone.ECS.Utility;
public unsafe static class CStringExtensions public unsafe static class CStringExtensions
{ {
public static CString Empty { get; } = (CString)""; public static CString Empty { get; } = (CString)"";
public static CString ETX { get; } = (CString)"\x3"; // FIXME: Temporary, until flecs supports Empty.
public static unsafe byte[]? FlecsToBytes(this CString str) public static unsafe byte[]? FlecsToBytes(this CString str)
{ {

@ -1,26 +0,0 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace gaemstone.ECS.Utility;
public class FlecsException
: Exception
{
public FlecsException() : base() { }
public FlecsException(string message) : base(message) { }
}
public class FlecsAbortException
: FlecsException
{
private readonly string _stackTrace = new StackTrace(2, true).ToString();
public override string? StackTrace => _stackTrace;
private FlecsAbortException()
: base("Abort was called by flecs") { }
[UnmanagedCallersOnly]
internal static void Callback()
=> throw new FlecsAbortException();
}

@ -0,0 +1,71 @@
using System;
using System.Runtime.InteropServices;
using static flecs_hub.flecs;
namespace gaemstone.ECS.Utility;
public unsafe readonly struct ReferenceHandle
: IDisposable
{
public static int NumActiveHandles { get; private set; }
private readonly nint _value;
public object? Target => (_value != default)
? ((GCHandle)_value).Target : null;
private ReferenceHandle(nint value)
=> _value = value;
public static ReferenceHandle Alloc(object? target)
{
if (target == null) return default;
NumActiveHandles++;
return new((nint)GCHandle.Alloc(target));
}
public ReferenceHandle Clone()
=> Alloc(Target);
public void Dispose()
{
if (_value == default) return;
NumActiveHandles--;
((GCHandle)_value).Free();
}
[UnmanagedCallersOnly]
internal static void Construct(void* ptr, int count, ecs_type_info_t* _)
=> new Span<ReferenceHandle>(ptr, count).Clear();
[UnmanagedCallersOnly]
internal static void Destruct(void* ptr, int count, ecs_type_info_t* _)
{
var span = new Span<ReferenceHandle>(ptr, count);
foreach (var handle in span) handle.Dispose();
span.Clear();
}
[UnmanagedCallersOnly]
internal static void Move(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _)
{
var dst = new Span<ReferenceHandle>(dstPtr, count);
var src = new Span<ReferenceHandle>(srcPtr, count);
foreach (var handle in dst) handle.Dispose();
src.CopyTo(dst);
src.Clear();
}
[UnmanagedCallersOnly]
internal static void Copy(void* dstPtr, void* srcPtr, int count, ecs_type_info_t* _)
{
var dst = new Span<ReferenceHandle>(dstPtr, count);
var src = new Span<ReferenceHandle>(srcPtr, count);
for (var i = 0; i < count; i++) {
dst[i].Dispose();
dst[i] = src[i].Clone();
}
}
}

@ -7,7 +7,7 @@ public static class SpanExtensions
public static T? GetOrNull<T>(this Span<T> span, int index) public static T? GetOrNull<T>(this Span<T> span, int index)
where T : struct => GetOrNull((ReadOnlySpan<T>)span, index); where T : struct => GetOrNull((ReadOnlySpan<T>)span, index);
public static T? GetOrNull<T>(this ReadOnlySpan<T> span, int index) public static T? GetOrNull<T>(this ReadOnlySpan<T> span, int index)
where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; where T : struct => ((index >= 0) && (index < span.Length)) ? span[index] : null;
public static ReadOnlySpanSplitEnumerator<T> Split<T>(this ReadOnlySpan<T> span, T splitOn) public static ReadOnlySpanSplitEnumerator<T> Split<T>(this ReadOnlySpan<T> span, T splitOn)

@ -0,0 +1,75 @@
using System;
using System.Runtime.InteropServices;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe struct World
: IEquatable<World>
, IDisposable
{
public ecs_world_t* Handle { get; }
public bool IsDeferred => ecs_is_deferred(this);
public bool IsQuitRequested => ecs_should_quit(this);
public World(ecs_world_t* handle)
=> Handle = handle;
/// <summary> Initializes a new Flecs world. </summary>
/// <param name="minimal"> If true, doesn't automatically import built-in modules. </param>
public World(bool minimal = false)
{
[UnmanagedCallersOnly]
static void Abort() => throw new FlecsAbortException();
ecs_os_set_api_defaults();
var api = ecs_os_get_api();
api.abort_ = new FnPtr_Void { Pointer = &Abort };
ecs_os_set_api(&api);
Handle = minimal ? ecs_mini() : ecs_init();
}
public bool Progress(TimeSpan delta)
=> ecs_progress(this, (float)delta.TotalSeconds);
public void Quit()
=> ecs_quit(this);
public void Dispose()
=> ecs_fini(this);
public Entity LookupAlive(Entity value)
=> new(ecs_get_alive(this, value));
public Entity LookupPath(EntityPath path, Entity parent = default,
bool throwOnNotFound = false)
=> new(EntityPath.Lookup(this, path, parent, throwOnNotFound));
// TODO: Provide overload that uses a UTF-8 byte span?
public Entity LookupSymbol(string symbol)
{
using var alloc = TempAllocator.Use();
return new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false));
}
public bool Equals(World other)
{
if (Handle == other.Handle) return true;
return ecs_get_world((ecs_poly_t*)Handle)
== ecs_get_world((ecs_poly_t*)other.Handle);
}
public override bool Equals(object? obj)
=> (obj is World other) && Equals(other);
public override int GetHashCode()
=> ((nint)ecs_get_world((ecs_poly_t*)Handle)).GetHashCode();
public static bool operator ==(World left, World right) => left.Equals(right);
public static bool operator !=(World left, World right) => !left.Equals(right);
public static implicit operator ecs_world_t*(World w) => w.Handle;
}

@ -1,69 +0,0 @@
using System;
using System.Collections.Generic;
using gaemstone.ECS.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"); }
public EntityRef? LookupByType<T>()
=> LookupByType(typeof(T));
public EntityRef? LookupByType(Type type)
=> LookupAlive(_lookupByType.GetValueOrDefault(type));
public EntityRef LookupByTypeOrThrow<T>()
=> LookupByTypeOrThrow(typeof(T));
public EntityRef LookupByTypeOrThrow(Type type)
=> LookupByType(type) ?? throw new EntityNotFoundException(
$"Entity of type {type} not found");
public EntityRef? LookupAlive(Entity value)
=> EntityRef.CreateOrNull(this, new(ecs_get_alive(this, value)));
public EntityRef LookupAliveOrThrow(Entity entity)
=> LookupAlive(entity) ?? throw new EntityNotFoundException(
$"Entity {entity} is not alive");
public EntityRef? LookupByPath(EntityPath path)
=> LookupByPath(default, path);
public EntityRef? LookupByPath(Entity parent, EntityPath path)
=> EntityRef.CreateOrNull(this, EntityPath.Lookup(this, parent, path, false));
public EntityRef LookupByPathOrThrow(EntityPath path)
=> LookupByPathOrThrow(default, path);
public EntityRef LookupByPathOrThrow(Entity parent, EntityPath path)
=> new(this, EntityPath.Lookup(this, parent, path, true));
public EntityRef? LookupBySymbol(string symbol)
{
using var alloc = TempAllocator.Use();
var entity = ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false);
return EntityRef.CreateOrNull(this, new(entity));
}
public EntityRef LookupBySymbolOrThrow(string symbol)
=> LookupBySymbol(symbol) ?? throw new EntityNotFoundException(
$"Entity with symbol '{symbol}' not found");
public class EntityNotFoundException : Exception
{ public EntityNotFoundException(string message) : base(message) { } }
}
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; }
}

@ -1,60 +1,78 @@
using System; using System;
using gaemstone.ECS.Utility; using gaemstone.ECS.Internal;
using static flecs_hub.flecs; using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
public unsafe partial class World public unsafe struct World<TContext>
: IEquatable<World<TContext>>
, IDisposable
{ {
public ecs_world_t* Handle { get; } public readonly World Value;
// Flecs built-ins that are important to internals. public bool IsDeferred => Value.IsDeferred;
internal EntityRef ChildOf { get; } public bool IsQuitRequested => Value.IsQuitRequested;
internal EntityRef Disabled { get; }
internal EntityRef DependsOn { get; }
public bool IsDeferred => ecs_is_deferred(this); public World(World value)
public bool IsQuitRequested => ecs_should_quit(this); => Value = value;
public World(params string[] args) /// <summary> Initializes a new Flecs world. </summary>
{ /// <param name="minimal"> If true, doesn't automatically import built-in modules. </param>
ecs_os_set_api_defaults(); public World(bool minimal = false)
var api = ecs_os_get_api(); : this(new World(minimal)) { }
api.abort_ = new FnPtr_Void { Pointer = &FlecsAbortException.Callback };
ecs_os_set_api(&api);
Handle = ecs_init_w_args(args.Length, null); public bool Progress(TimeSpan delta) => Value.Progress(delta);
public void Quit() => Value.Quit();
public void Dispose() => Value.Dispose();
ChildOf = LookupByPathOrThrow("/flecs/core/ChildOf");
Disabled = LookupByPathOrThrow("/flecs/core/Disabled");
DependsOn = LookupByPathOrThrow("/flecs/core/DependsOn");
}
public void Dispose() public EntityBuilder<TContext> New(EntityPath? path = null, Entity parent = default)
=> ecs_fini(this); => new(this, parent, path);
public EntityBuilder New(EntityPath? path = null) public Entity<TContext>? LookupAliveOrNull(Entity value)
=> new(this, path); => ECS.Entity<TContext>.GetOrNull(this, Value.LookupAlive(value));
public Entity<TContext> LookupAliveOrThrow(Entity value)
=> LookupAliveOrNull(value) ?? throw new EntityNotFoundException(
$"Entity '{value}' could not be found");
public Entity<TContext>? LookupPathOrNull(EntityPath path, Entity parent = default)
=> ECS.Entity<TContext>.GetOrNull(this, Value.LookupPath(path, parent, false));
public Entity<TContext> LookupPathOrThrow(EntityPath path, Entity parent = default)
=> ECS.Entity<TContext>.GetOrInvalid(this, Value.LookupPath(path, parent, true));
public Entity<TContext>? LookupSymbolOrNull(string symbol)
=> ECS.Entity<TContext>.GetOrNull(this, Value.LookupSymbol(symbol));
public Entity<TContext> LookupSymbolOrThrow(string symbol)
=> LookupSymbolOrNull(symbol) ?? throw new EntityNotFoundException(
$"Entity with symbol '{symbol}' could not be found");
public Entity<TContext> Entity<T>()
=> ECS.Entity<TContext>.GetOrThrow(this,
Lookup<TContext>.Entity<T>.Value);
public Id<TContext> Pair(Entity relation, Entity target)
=> Id<TContext>.GetOrThrow(this, Id.Pair(relation, target));
public Id<TContext> Pair<TRelation>(Entity target)
=> Pair(Entity<TRelation>(), target);
public Id<TContext> Pair<TRelation, TTarget>()
=> Pair(Entity<TRelation>(), Entity<TTarget>());
public EntityBuilder New(EntityRef? parent, EntityPath? path = null)
{
var entity = New(path);
// If given path is absolute, the new entity won't be created as a
// child of the specified parent. Alternatively, EntityRef.NewChild
// can be used, which will throw when an absolute path is given.
if ((path?.IsRelative != false) && (parent != null))
entity.ChildOf(parent);
return entity;
}
public Iterator<TContext> Term(Term term) => Iterator<TContext>.FromTerm(this, term);
public Filter<TContext> Filter(FilterDesc desc) => new(this, desc);
public Query<TContext> Query(QueryDesc desc) => new(this, desc);
public Rule<TContext> Rule(FilterDesc desc) => new(this, desc);
public bool Progress(TimeSpan delta)
=> ecs_progress(this, (float)delta.TotalSeconds);
public void Quit() public bool Equals(World<TContext> other) => Value == other.Value;
=> ecs_quit(this); public override bool Equals(object? obj) => (obj is World<TContext> other) && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(World<TContext> left, World<TContext> right) => left.Equals(right);
public static bool operator !=(World<TContext> left, World<TContext> right) => !left.Equals(right);
public static implicit operator ecs_world_t*(World w) => w.Handle; public static implicit operator World (World<TContext> world) => world.Value;
public static implicit operator ecs_world_t*(World<TContext> world) => world.Value.Handle;
} }

@ -1,182 +0,0 @@
// <auto-generated>
// Generated by gaemstone.ECS.BindGen c661367
// Time: 2023-01-09 18:48:29Z
// Flecs version: v3.1.3
// </auto-generated>
#pragma warning disable CS8981
public static partial class flecs
{
public const uint ECS_ID_CACHE_SIZE = (32);
public const uint ECS_TERM_DESC_CACHE_SIZE = (16);
public const uint ECS_OBSERVER_DESC_EVENT_COUNT_MAX = (8);
public const uint ECS_VARIABLE_COUNT_MAX = (64);
public const uint EcsWorldQuitWorkers = (1u << 0);
public const uint EcsWorldReadonly = (1u << 1);
public const uint EcsWorldQuit = (1u << 2);
public const uint EcsWorldFini = (1u << 3);
public const uint EcsWorldMeasureFrameTime = (1u << 4);
public const uint EcsWorldMeasureSystemTime = (1u << 5);
public const uint EcsWorldMultiThreaded = (1u << 6);
public const uint EcsOsApiHighResolutionTimer = (1u << 0);
public const uint EcsOsApiLogWithColors = (1u << 1);
public const uint EcsOsApiLogWithTimeStamp = (1u << 2);
public const uint EcsOsApiLogWithTimeDelta = (1u << 3);
public const uint EcsEntityObserved = (1u << 31);
public const uint EcsEntityObservedId = (1u << 30);
public const uint EcsEntityObservedTarget = (1u << 29);
public const uint EcsEntityObservedAcyclic = (1u << 28);
public const uint EcsIdOnDeleteRemove = (1u << 0);
public const uint EcsIdOnDeleteDelete = (1u << 1);
public const uint EcsIdOnDeletePanic = (1u << 2);
public const uint EcsIdOnDeleteMask = (EcsIdOnDeletePanic|EcsIdOnDeleteRemove|EcsIdOnDeleteDelete);
public const uint EcsIdOnDeleteObjectRemove = (1u << 3);
public const uint EcsIdOnDeleteObjectDelete = (1u << 4);
public const uint EcsIdOnDeleteObjectPanic = (1u << 5);
public const uint EcsIdOnDeleteObjectMask = (EcsIdOnDeleteObjectPanic|EcsIdOnDeleteObjectRemove|EcsIdOnDeleteObjectDelete);
public const uint EcsIdExclusive = (1u << 6);
public const uint EcsIdDontInherit = (1u << 7);
public const uint EcsIdAcyclic = (1u << 8);
public const uint EcsIdTag = (1u << 9);
public const uint EcsIdWith = (1u << 10);
public const uint EcsIdUnion = (1u << 11);
public const uint EcsIdHasOnAdd = (1u << 15);
public const uint EcsIdHasOnRemove = (1u << 16);
public const uint EcsIdHasOnSet = (1u << 17);
public const uint EcsIdHasUnSet = (1u << 18);
public const uint EcsIdEventMask = (EcsIdHasOnAdd|EcsIdHasOnRemove|EcsIdHasOnSet|EcsIdHasUnSet);
public const uint EcsIdMarkedForDelete = (1u << 30);
public const uint EcsIterIsValid = (1u << 0u);
public const uint EcsIterIsFilter = (1u << 1u);
public const uint EcsIterIsInstanced = (1u << 2u);
public const uint EcsIterHasShared = (1u << 3u);
public const uint EcsIterTableOnly = (1u << 4u);
public const uint EcsIterEntityOptional = (1u << 5u);
public const uint EcsIterNoResults = (1u << 6u);
public const uint EcsIterIgnoreThis = (1u << 7u);
public const uint EcsIterMatchVar = (1u << 8u);
public const uint EcsEventTableOnly = (1u << 8u);
public const uint EcsEventNoOnSet = (1u << 16u);
public const uint EcsFilterMatchThis = (1u << 1u);
public const uint EcsFilterMatchOnlyThis = (1u << 2u);
public const uint EcsFilterMatchPrefab = (1u << 3u);
public const uint EcsFilterMatchDisabled = (1u << 4u);
public const uint EcsFilterMatchEmptyTables = (1u << 5u);
public const uint EcsFilterMatchAnything = (1u << 6u);
public const uint EcsFilterIsFilter = (1u << 7u);
public const uint EcsFilterIsInstanced = (1u << 8u);
public const uint EcsFilterPopulate = (1u << 9u);
public const uint EcsTableHasBuiltins = (1u << 1u);
public const uint EcsTableIsPrefab = (1u << 2u);
public const uint EcsTableHasIsA = (1u << 3u);
public const uint EcsTableHasChildOf = (1u << 4u);
public const uint EcsTableHasPairs = (1u << 5u);
public const uint EcsTableHasModule = (1u << 6u);
public const uint EcsTableIsDisabled = (1u << 7u);
public const uint EcsTableHasCtors = (1u << 8u);
public const uint EcsTableHasDtors = (1u << 9u);
public const uint EcsTableHasCopy = (1u << 10u);
public const uint EcsTableHasMove = (1u << 11u);
public const uint EcsTableHasUnion = (1u << 12u);
public const uint EcsTableHasToggle = (1u << 13u);
public const uint EcsTableHasOverrides = (1u << 14u);
public const uint EcsTableHasOnAdd = (1u << 15u);
public const uint EcsTableHasOnRemove = (1u << 16u);
public const uint EcsTableHasOnSet = (1u << 17u);
public const uint EcsTableHasUnSet = (1u << 18u);
public const uint EcsTableHasObserved = (1u << 20u);
public const uint EcsTableMarkedForDelete = (1u << 30u);
public const uint EcsTableHasLifecycle = (EcsTableHasCtors | EcsTableHasDtors);
public const uint EcsTableIsComplex = (EcsTableHasLifecycle | EcsTableHasUnion | EcsTableHasToggle);
public const uint EcsTableHasAddActions = (EcsTableHasIsA | EcsTableHasUnion | EcsTableHasCtors | EcsTableHasOnAdd | EcsTableHasOnSet);
public const uint EcsTableHasRemoveActions = (EcsTableHasIsA | EcsTableHasDtors | EcsTableHasOnRemove | EcsTableHasUnSet);
public const uint EcsQueryHasRefs = (1u << 1u);
public const uint EcsQueryIsSubquery = (1u << 2u);
public const uint EcsQueryIsOrphaned = (1u << 3u);
public const uint EcsQueryHasOutColumns = (1u << 4u);
public const uint EcsQueryHasMonitor = (1u << 5u);
public const uint EcsAperiodicEmptyTables = (1u << 1u);
public const uint EcsAperiodicComponentMonitors = (1u << 2u);
public const uint EcsAperiodicEmptyQueries = (1u << 4u);
public const uint ecs_world_t_magic = (0x65637377);
public const uint ecs_stage_t_magic = (0x65637373);
public const uint ecs_query_t_magic = (0x65637371);
public const uint ecs_rule_t_magic = (0x65637375);
public const uint ecs_table_t_magic = (0x65637374);
public const uint ecs_filter_t_magic = (0x65637366);
public const uint ecs_trigger_t_magic = (0x65637372);
public const uint ecs_observer_t_magic = (0x65637362);
public const uint ECS_ROW_MASK = (0x0FFFFFFFu);
public const uint ECS_ROW_FLAGS_MASK = (~ECS_ROW_MASK);
public const ulong ECS_ID_FLAGS_MASK = (0xFFul << 60);
public const ulong ECS_ENTITY_MASK = (0xFFFFFFFFul);
public const ulong ECS_GENERATION_MASK = (0xFFFFul << 32);
public const uint ECS_COMPONENT_MASK = (~ECS_ID_FLAGS_MASK);
public const uint FLECS_SPARSE_CHUNK_SIZE = (4096);
public const uint ECS_STRBUF_ELEMENT_SIZE = (511);
public const uint ECS_STRBUF_MAX_LIST_DEPTH = (32);
public const uint EcsSelf = (1u << 1);
public const uint EcsUp = (1u << 2);
public const uint EcsDown = (1u << 3);
public const uint EcsTraverseAll = (1u << 4);
public const uint EcsCascade = (1u << 5);
public const uint EcsParent = (1u << 6);
public const uint EcsIsVariable = (1u << 7);
public const uint EcsIsEntity = (1u << 8);
public const uint EcsFilter = (1u << 9);
public const uint EcsTraverseFlags = (EcsUp|EcsDown|EcsTraverseAll|EcsSelf|EcsCascade|EcsParent);
public const uint flecs_iter_cache_ids = (1u << 0u);
public const uint flecs_iter_cache_columns = (1u << 1u);
public const uint flecs_iter_cache_sources = (1u << 2u);
public const uint flecs_iter_cache_sizes = (1u << 3u);
public const uint flecs_iter_cache_ptrs = (1u << 4u);
public const uint flecs_iter_cache_match_indices = (1u << 5u);
public const uint flecs_iter_cache_variables = (1u << 6u);
public const uint flecs_iter_cache_all = (255);
public const uint ECS_HI_COMPONENT_ID = (256);
public const uint ECS_MAX_COMPONENT_ID = (~((uint)(ECS_ID_FLAGS_MASK >> 32)));
public const uint ECS_MAX_RECURSION = (512);
public const uint ECS_MAX_TOKEN_SIZE = (256);
public const ulong ECS_ID_FLAG_BIT = (1ul << 63);
public const uint EcsFirstUserComponentId = (32);
public const uint EcsFirstUserEntityId = (ECS_HI_COMPONENT_ID + 128);
public const uint ECS_INVALID_OPERATION = (1);
public const uint ECS_INVALID_PARAMETER = (2);
public const uint ECS_CONSTRAINT_VIOLATED = (3);
public const uint ECS_OUT_OF_MEMORY = (4);
public const uint ECS_OUT_OF_RANGE = (5);
public const uint ECS_UNSUPPORTED = (6);
public const uint ECS_INTERNAL_ERROR = (7);
public const uint ECS_ALREADY_DEFINED = (8);
public const uint ECS_MISSING_OS_API = (9);
public const uint ECS_OPERATION_FAILED = (10);
public const uint ECS_INVALID_CONVERSION = (11);
public const uint ECS_ID_IN_USE = (12);
public const uint ECS_CYCLE_DETECTED = (13);
public const uint ECS_LEAK_DETECTED = (14);
public const uint ECS_INCONSISTENT_NAME = (20);
public const uint ECS_NAME_IN_USE = (21);
public const uint ECS_NOT_A_COMPONENT = (22);
public const uint ECS_INVALID_COMPONENT_SIZE = (23);
public const uint ECS_INVALID_COMPONENT_ALIGNMENT = (24);
public const uint ECS_COMPONENT_NOT_REGISTERED = (25);
public const uint ECS_INCONSISTENT_COMPONENT_ID = (26);
public const uint ECS_INCONSISTENT_COMPONENT_ACTION = (27);
public const uint ECS_MODULE_UNDEFINED = (28);
public const uint ECS_MISSING_SYMBOL = (29);
public const uint ECS_ALREADY_IN_USE = (30);
public const uint ECS_ACCESS_VIOLATION = (40);
public const uint ECS_COLUMN_INDEX_OUT_OF_RANGE = (41);
public const uint ECS_COLUMN_IS_NOT_SHARED = (42);
public const uint ECS_COLUMN_IS_SHARED = (43);
public const uint ECS_COLUMN_TYPE_MISMATCH = (45);
public const uint ECS_INVALID_WHILE_READONLY = (70);
public const uint ECS_LOCKED_STORAGE = (71);
public const uint ECS_INVALID_FROM_WORKER = (72);
public const uint ECS_REST_DEFAULT_PORT = (27750);
public const uint ECS_STAT_WINDOW = (60);
public const uint ECS_MEMBER_DESC_CACHE_SIZE = (32);
public const uint ECS_META_MAX_SCOPE_DEPTH = (32);
public const uint ECS_HTTP_HEADER_COUNT_MAX = (32);
public const uint ECS_HTTP_QUERY_PARAM_COUNT_MAX = (32);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save