Compare commits

..

2 Commits

  1. 3
      .gitmodules
  2. 17
      .vscode/launch.json
  3. 8
      .vscode/settings.json
  4. 14
      .vscode/tasks.json
  5. 59
      README.md
  6. 14548
      flecs.h-ast
  7. 34
      gaemstone.ECS.sln
  8. 1
      src/flecs
  9. 2
      src/flecs-cs
  10. 421
      src/gaemstone.ECS.BindGen/Program.cs
  11. 21
      src/gaemstone.ECS.BindGen/gaemstone.ECS.BindGen.csproj
  12. 87
      src/gaemstone.ECS/Component.cs
  13. 35
      src/gaemstone.ECS/Entity+Bare.cs
  14. 176
      src/gaemstone.ECS/Entity.cs
  15. 61
      src/gaemstone.ECS/EntityBase.cs
  16. 89
      src/gaemstone.ECS/EntityBuilder.cs
  17. 76
      src/gaemstone.ECS/EntityPath.cs
  18. 169
      src/gaemstone.ECS/EntityRef.cs
  19. 16
      src/gaemstone.ECS/EntityType.cs
  20. 31
      src/gaemstone.ECS/Exceptions.cs
  21. 27
      src/gaemstone.ECS/Filter.cs
  22. 36
      src/gaemstone.ECS/Id+Bare.cs
  23. 81
      src/gaemstone.ECS/Id.cs
  24. 61
      src/gaemstone.ECS/IdRef.cs
  25. 117
      src/gaemstone.ECS/Internal/EntityAccess.cs
  26. 11
      src/gaemstone.ECS/Internal/FlecsBuiltIn.cs
  27. 113
      src/gaemstone.ECS/Internal/Iterator.cs
  28. 9
      src/gaemstone.ECS/Internal/Lookup.cs
  29. 194
      src/gaemstone.ECS/Iterator.cs
  30. 26
      src/gaemstone.ECS/Observer.cs
  31. 31
      src/gaemstone.ECS/Query.cs
  32. 53
      src/gaemstone.ECS/Rule.cs
  33. 41
      src/gaemstone.ECS/System.cs
  34. 60
      src/gaemstone.ECS/Term.cs
  35. 15
      src/gaemstone.ECS/Utility/Allocators.cs
  36. 1
      src/gaemstone.ECS/Utility/CStringExtensions.cs
  37. 26
      src/gaemstone.ECS/Utility/FlecsException.cs
  38. 71
      src/gaemstone.ECS/Utility/ReferenceHandle.cs
  39. 2
      src/gaemstone.ECS/Utility/SpanExtensions.cs
  40. 75
      src/gaemstone.ECS/World+Bare.cs
  41. 69
      src/gaemstone.ECS/World+Lookup.cs
  42. 96
      src/gaemstone.ECS/World.cs
  43. 182
      src/gaemstone.ECS/flecs/flecs+Constants.g.cs
  44. 3773
      src/gaemstone.ECS/flecs/flecs+Functions.g.cs
  45. 1754
      src/gaemstone.ECS/flecs/flecs+Structs.g.cs
  46. 6
      src/gaemstone.ECS/gaemstone.ECS.csproj

3
.gitmodules vendored

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

@ -0,0 +1,17 @@
{
"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,
}
]
}

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

14
.vscode/tasks.json vendored

@ -0,0 +1,14 @@
{
"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,8 +1,10 @@
# gaemstone.ECS
.. is a medium-level managed wrapper library around the [flecs-cs] bindings for the amazing [Entity Component System (ECS)][ECS] framework [Flecs]. It is used as part of the [gæmstone] game engine, but may be used in other projects as well. To efficiently use this library, a thorough understanding of [Flecs] is required.
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.
.. is a medium-level managed wrapper library around the [flecs-cs] bindings
for the amazing [Entity Component System (ECS)][ECS] framework [Flecs]. It is
used as part of the [gæmstone] game engine, but may be used in other projects
as well. To efficiently use this library, a thorough understanding of [Flecs]
is required.
[ECS]: https://en.wikipedia.org/wiki/Entity_component_system
[Flecs]: https://github.com/SanderMertens/flecs
@ -11,18 +13,20 @@ These classes have been split from the main [gæmstone] project. It is still a l
## Features
- Convenient wrapper types such as [Id], [Entity] and [EntityType].
These classes have only recently been split from the main **gæmstone** project. It is currently unclear what functionality belongs where, and structural changes are still very likely. In its current state, feel free to use **gæmstone.ECS** as a reference for building similar projects, or be aware that things are in flux, and in general nowhere near stable.
- Simple wrapper structs such as [Entity] and [Identifier].
- Classes with convenience functions like [EntityRef] and [EntityType].
- [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.
- Query the ECS with [Iterators], [Filters], [Queries] and [Rules].
- Create [Systems] for game logic and [Observers] to act on changes.
[Id]: ./src/gaemstone.ECS/Id.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
[EntityPath]: ./src/gaemstone.ECS/EntityPath.cs
[World]: ./src/gaemstone.ECS/World.cs
[Components]: ./src/gaemstone.ECS/Component.cs
[Iterators]: ./src/gaemstone.ECS/Iterator.cs
[Filters]: ./src/gaemstone.ECS/Filter.cs
@ -34,7 +38,7 @@ These classes have been split from the main [gæmstone] project. It is still a l
## Example
```cs
var world = new World<Program>();
var world = new World();
var position = world
.New("Position") // Create a new EntityBuilder, and set its name.
@ -51,31 +55,26 @@ entities.NewChild("Two").Set(new Position(10, 20)).Build();
// Changed my mind: Let's multiply each entity's position by 10.
foreach (var child in entities.GetChildren()) {
ref var pos = ref child.GetRefOrThrow<Position>();
pos = new(pos.X * 10, pos.Y * 10);
ref var pos = ref child.GetRefOrThrow<Position>();
pos = new(pos.X * 10, pos.Y * 10);
}
// 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");
var onUpdate = world.LookupByPathOrThrow("/flecs/pipeline/OnUpdate");
// Create a system that will move all entities with
// the "Position" component downwards by 2 every frame.
world.New("FallSystem")
.Add(dependsOn, onUpdate)
.Build().InitSystem(new("Position"), iter => {
world.New("FallSystem").Build()
.InitSystem(onUpdate, new("Position"), iter => {
var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; 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.
world.New("PrintPositionSystem").Build()
.Add(dependsOn, onUpdate)
.InitSystem(new("[in] Position"), iter => {
.InitSystem(onUpdate, new("[in] Position"), iter => {
var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
var entity = iter.Entity(i);
@ -105,26 +104,8 @@ git clone --recurse-submodules 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:
dotnet add reference gaemstone.ECS/gaemstone.ECS.csproj
dotnet add reference gaemstone.ECS/src/gaemstone.ECS/gaemstone.ECS.csproj
# To generate flecs-cs' bindings:
./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

@ -0,0 +1,34 @@

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

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

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

@ -0,0 +1,421 @@
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";
}

@ -0,0 +1,21 @@
<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,13 +1,18 @@
using System;
using System.Runtime.CompilerServices;
using gaemstone.ECS.Utility;
using System.Runtime.InteropServices;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe partial struct Entity<TContext>
public static unsafe class ComponentExtensions
{
public Entity<TContext> InitComponent<T>()
public static EntityRef InitComponent(this EntityRef entity, Type type)
=> (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(
"Must not be primitive");
@ -17,8 +22,8 @@ public unsafe partial struct Entity<TContext>
var size = typeof(T).IsValueType ? Unsafe.SizeOf<T>() : sizeof(ReferenceHandle);
var typeInfo = new ecs_type_info_t { size = size, alignment = size };
var componentDesc = new ecs_component_desc_t { entity = this, type = typeInfo };
ecs_component_init(World, &componentDesc);
var componentDesc = new ecs_component_desc_t { entity = entity, type = typeInfo };
ecs_component_init(entity.World, &componentDesc);
if (!typeof(T).IsValueType) {
// Set up component hooks for proper freeing of GCHandles.
@ -29,9 +34,77 @@ public unsafe partial struct Entity<TContext>
move = new() { Data = new() { Pointer = &ReferenceHandle.Move } },
copy = new() { Data = new() { Pointer = &ReferenceHandle.Copy } },
};
ecs_set_hooks_id(World, this, &typeHooks);
ecs_set_hooks_id(entity.World, entity, &typeHooks);
}
return CreateLookup<T>();
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()
=> 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();
}
}
}

@ -1,35 +0,0 @@
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,172 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using gaemstone.ECS.Internal;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe readonly partial struct Entity<TContext>
: IEquatable<Entity<TContext>>
public readonly struct Entity
: IEquatable<Entity>
{
public static readonly Entity<TContext> None = default;
public static readonly Entity None = default;
public readonly World<TContext> World;
public readonly Entity Value;
public readonly ecs_entity_t Value;
public uint Id => (uint)Value.Data;
public uint NumericId => Value.NumericId;
public bool IsNone => Value.IsNone;
public bool IsSome => Value.IsSome;
public bool IsSome => Value.Data != 0;
public bool IsNone => Value.Data == 0;
public bool IsValid => EntityAccess.IsValid(World, this);
public bool IsAlive => IsSome && EntityAccess.IsAlive(World, this);
public Entity(ecs_entity_t value) => Value = value;
public string? Name { get => EntityAccess.GetName(World, this); set => EntityAccess.SetName(World, this, value); }
public string? Symbol { get => EntityAccess.GetSymbol(World, this); set => EntityAccess.SetSymbol(World, this, value); }
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 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()
=> Value.ToString();
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;
=> IsSome ? $"Entity(0x{Value.Data.Data:X})"
: "Entity.None";
public static implicit operator Id<TContext>(Entity<TContext> entity) => Id<TContext>.GetUnsafe(entity.World, entity);
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 bool operator ==(Entity left, Entity right) => left.Equals(right);
public static bool operator !=(Entity left, Entity right) => !left.Equals(right);
public static implicit operator Term (Entity<TContext> entity) => new(entity.Value);
public static implicit operator TermId(Entity<TContext> entity) => new(entity.Value);
public static implicit operator ecs_entity_t(Entity e) => e.Value;
public static implicit operator Id(Entity e) => new(e.Value.Data);
public static implicit operator ecs_id_t(Entity e) => e.Value.Data;
}

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

@ -4,13 +4,11 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using gaemstone.ECS.Internal;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
// TODO: Redo this with a single UTF8 byte array.
public class EntityPath
{
private readonly byte[][] _parts;