Refactor ModuleManager and built-in modules

- Create a minimal Flecs world (no default addon module imports)
- Wrap all the official Flecs addon modules as [BuiltIn] modules
- Add IModuleImport for [BuiltIn] modules
- Rename IModuleInitializer to IModuleLifetime
- Rename IModule.Initialize to .OnEnable
- Overhaul ModuleManager to use entities and rules
- Use ModuleManager.Import for built-in modules
- Add gaemstone.Doc.Tag to represent tag-like components
- Overhaul EntityInspector to use new features
main
copygirl 12 months ago
parent d80e69006c
commit 6b92e1ce8c
  1. 14
      src/Immersion/Program.cs
  2. 127
      src/gaemstone.Client/Systems/EntityInspector.cs
  3. 1
      src/gaemstone.Client/Systems/InputManager.cs
  4. 2
      src/gaemstone.Client/Systems/Renderer.cs
  5. 1622
      src/gaemstone.Client/Utility/ForkAwesome.cs
  6. 5
      src/gaemstone.Client/Utility/ImGuiUtility.cs
  7. 2
      src/gaemstone.ECS
  8. 23
      src/gaemstone.SourceGen/ModuleGenerator.cs
  9. 2
      src/gaemstone.SourceGen/Structure/MethodEntityInfo.cs
  10. 6
      src/gaemstone.SourceGen/Structure/ModuleEntityInfo.cs
  11. 8
      src/gaemstone/Doc.cs
  12. 22
      src/gaemstone/ECS/Module.cs
  13. 5
      src/gaemstone/Flecs/Core.cs
  14. 25
      src/gaemstone/Flecs/CoreDoc.cs
  15. 16
      src/gaemstone/Flecs/Doc.cs
  16. 23
      src/gaemstone/Flecs/Meta.cs
  17. 26
      src/gaemstone/Flecs/Metrics.cs
  18. 10
      src/gaemstone/Flecs/Monitor.cs
  19. 20
      src/gaemstone/Flecs/Pipeline.cs
  20. 18
      src/gaemstone/Flecs/Rest.cs
  21. 24
      src/gaemstone/Flecs/Script.cs
  22. 23
      src/gaemstone/Flecs/System.cs
  23. 24
      src/gaemstone/Flecs/Timer.cs
  24. 23
      src/gaemstone/Flecs/Units.cs
  25. 166
      src/gaemstone/Universe+Modules.cs
  26. 15
      src/gaemstone/Universe.cs

@ -23,8 +23,12 @@ var universe = new Universe<Program>();
var world = universe.World;
// TODO: Figure out a nice way to get rid of "compile errors" here.
// FIXME: universe.Modules.Register<gaemstone.Flecs.Systems.Monitor>();
universe.Modules.Register<gaemstone.Flecs.Systems.Rest>();
universe.Modules.Import<gaemstone.Flecs.Timer>();
universe.Modules.Import<gaemstone.Flecs.Script>();
universe.Modules.Import<gaemstone.Flecs.Rest>();
universe.Modules.Import<gaemstone.Flecs.Monitor>();
universe.Modules.Import<gaemstone.Flecs.Units>();
universe.Modules.Import<gaemstone.Flecs.Metrics>();
var window = Window.Create(WindowOptions.Default with {
Title = "gæmstone",
@ -57,9 +61,9 @@ universe.Modules.Register<gaemstone.Client.Systems.EntityInspector>();
universe.Modules.Register<gaemstone.Client.Components.CameraComponents>();
universe.Modules.Register<gaemstone.Client.Systems.FreeCameraController>();
foreach (var module in universe.Modules)
if (!module.IsInitialized) throw new InvalidOperationException(
$"Module '{module.Entity.Path}' is not initialized");
using (var disabledModules = world.Filter(new("ModuleInfo, Disabled")))
foreach (var module in disabledModules.Iter().GetAllEntities())
throw new InvalidOperationException($"Module '{module.Path}' is not ednbled");
// Initialize Canvas and GameWindow singletons with actual values.
world.Entity<Canvas>().Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window)));

@ -7,7 +7,6 @@ using gaemstone.ECS;
using gaemstone.Flecs;
using ImGuiNET;
using static gaemstone.Client.Systems.ImGuiManager;
using Icon = gaemstone.Client.Utility.ForkAwesome;
using ImGuiInternal = ImGuiNET.Internal.ImGui;
namespace gaemstone.Client.Systems;
@ -15,7 +14,6 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public partial class EntityInspector
: IModuleInitializer
{
[Tag]
public struct InspectorWindow { }
@ -54,32 +52,49 @@ public partial class EntityInspector
}
[Component]
public struct DocPriority { public float Value; }
public record struct Priority(float Value);
[Component]
public struct DocIcon { public char Value; }
public record struct Icon(char Value);
private const string DefaultWindowTitle = "Inspector Gadget";
[Path("/flecs/core/Module")]
// [Doc(Color = "#FFE4B2")]
[Add<Doc.DisplayType>, Set<Priority>(0), Set<Icon>(ForkAwesome.Archive)]
public struct Module { }
public static void Initialize<T>(Entity<T> module)
{
void SetDocInfo(string path, float priority, string icon, float r, float g, float b)
=> module.World.LookupPathOrThrow(path)
.Add<Doc.DisplayType>()
.Set(new DocPriority { Value = priority })
.Set(new DocIcon { Value = icon[0] })
.SetDocColor(Color.FromRGB(r, g, b).ToHexString());
SetDocInfo("/flecs/core/Module" , 0 , Icon.Archive , 1.0f, 0.9f, 0.7f);
SetDocInfo("/flecs/system/System" , 1 , Icon.Cog , 1.0f, 0.7f, 0.7f);
SetDocInfo("/flecs/core/Observer" , 2 , Icon.Eye , 1.0f, 0.8f, 0.8f);
SetDocInfo("/gaemstone/Doc/Relation" , 3 , Icon.ShareAlt , 0.7f, 1.0f, 0.8f);
SetDocInfo("/flecs/core/Component" , 4 , Icon.PencilSquare , 0.6f, 0.6f, 1.0f);
// TODO: Handle tags like Flecs does.
SetDocInfo("/flecs/core/Tag" , 5 , Icon.Tag , 0.7f, 0.8f, 1.0f);
SetDocInfo("/flecs/core/Prefab" , 6 , Icon.Cube , 0.9f, 0.8f, 1.0f);
}
[Path("/flecs/system/System")]
// [Doc(Color = "#FFB2B2")]
[Add<Doc.DisplayType>, Set<Priority>(1), Set<Icon>(ForkAwesome.Cog)]
public struct System { }
[Path("/flecs/core/Observer")]
// [Doc(Color = "#FFCCCC")]
[Add<Doc.DisplayType>, Set<Priority>(2), Set<Icon>(ForkAwesome.Eye)]
public struct Observer { }
[Path("/gaemstone/Doc/Relation")]
// [Doc(Color = "#B2FFCC")]
[Add<Doc.DisplayType>, Set<Priority>(3), Set<Icon>(ForkAwesome.ShareAlt)]
public struct Relation { }
[Path("/flecs/core/Component")]
// [Doc(Color = "#9999FF")]
[Add<Doc.DisplayType>, Set<Priority>(4), Set<Icon>(ForkAwesome.PencilSquare)]
public struct Component { }
[Path("/gaemstone/Doc/Tag")]
// [Doc(Color = "#B2CCFF")]
[Add<Doc.DisplayType>, Set<Priority>(5), Set<Icon>(ForkAwesome.Tag)]
public struct Tag { }
[Path("/flecs/core/Prefab")]
// [Doc(Color = "#E4CCFF")]
[Add<Doc.DisplayType>, Set<Priority>(6), Set<Icon>(ForkAwesome.Cube)]
public struct Prefab { }
private const string DefaultWindowTitle = "Inspector Gadget";
[System]
@ -87,10 +102,10 @@ public partial class EntityInspector
{
var hasAnyInspector = false;
var inspectorWindow = world.Entity<InspectorWindow>();
foreach (var entity in Iterator<T>.FromTerm(world, new(inspectorWindow)))
foreach (var entity in world.Term(new(inspectorWindow)))
{ hasAnyInspector = true; break; }
if (ImGuiUtility.UIButton(0, Icon.Search, DefaultWindowTitle, hasAnyInspector))
if (ImGuiUtility.UIButton(0, ForkAwesome.Search, DefaultWindowTitle, hasAnyInspector))
NewEntityInspectorWindow(world);
}
@ -104,7 +119,7 @@ public partial class EntityInspector
ImGui.SetNextWindowSize(new(fontSize * 40, fontSize * 25), ImGuiCond.Appearing);
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
var title = window.GetDocName() ?? DefaultWindowTitle;
if (ImGui.Begin($"{Icon.Search} {title}###{window.NumericId}",
if (ImGui.Begin($"{ForkAwesome.Search} {title}###{window.NumericId}",
ref isOpen, ImGuiWindowFlags.NoScrollbar)) {
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]);
@ -132,9 +147,9 @@ public partial class EntityInspector
ImGui.TableNextColumn();
ImGui.BeginChild("EntityView", new(-float.Epsilon, -float.Epsilon));
if (!ImGui.BeginTabBar("Tabs")) return;
Tab($"{Icon.PencilSquare} Components", ComponentsTab);
Tab($"{Icon.ShareAlt} References", ReferencesTab);
Tab($"{Icon.InfoCircle} Documentation", DocumentationTab);
Tab($"{ForkAwesome.PencilSquare} Components", ComponentsTab);
Tab($"{ForkAwesome.ShareAlt} References", ReferencesTab);
Tab($"{ForkAwesome.InfoCircle} Documentation", DocumentationTab);
ImGui.EndTabBar();
ImGui.EndChild();
@ -157,9 +172,9 @@ public partial class EntityInspector
private static void ActionBarAndPath<T>(Entity<T> window, History? history, Entity<T>? selected)
{
static bool IconButtonWithToolTip(string icon, string tooltip, bool enabled = true) {
static bool IconButtonWithToolTip(char icon, string tooltip, bool enabled = true) {
if (!enabled) ImGui.BeginDisabled();
var clicked = ImGui.Button(icon);
var clicked = ImGui.Button(icon.ToString());
if (!enabled) ImGui.EndDisabled();
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(tooltip);
@ -174,22 +189,22 @@ public partial class EntityInspector
ImGui.TableNextColumn();
var hasExpanded = window.Has<Expanded, Core.Wildcard>();
if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded))
if (IconButtonWithToolTip(ForkAwesome.Outdent, "Collapse all items in the Explorer View", hasExpanded))
window.Remove<Expanded, Core.Wildcard>();
if (history != null) {
var hasPrev = ((selected != null) ? history.Current?.Prev : history.Current) != null;
var hasNext = history.Current?.Next != null;
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.ArrowLeft, "Go to the previously viewed entity", hasPrev))
if (IconButtonWithToolTip(ForkAwesome.ArrowLeft, "Go to the previously viewed entity", hasPrev))
GoToPrevious(window, history, selected);
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.ArrowRight, "Go to the next viewed entity", hasNext))
if (IconButtonWithToolTip(ForkAwesome.ArrowRight, "Go to the next viewed entity", hasNext))
GoToNext(window, history);
}
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.Crosshairs, "Scroll to the current entity in the Explorer View", (selected != null)))
if (IconButtonWithToolTip(ForkAwesome.Crosshairs, "Scroll to the current entity in the Explorer View", (selected != null)))
window.Add<ScrollToSelected>();
ImGui.TableNextColumn();
@ -197,22 +212,22 @@ public partial class EntityInspector
PathInput(window, history, selected, availableWidth);
ImGui.TableNextColumn();
if (IconButtonWithToolTip(Icon.PlusCircle, "Create a new child entity", (selected != null)))
if (IconButtonWithToolTip(ForkAwesome.PlusCircle, "Create a new child entity", (selected != null)))
SetSelected(window, history, selected?.NewChild().Build());
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.Pencil, "Rename the current entity", false && (selected != null)))
if (IconButtonWithToolTip(ForkAwesome.Pencil, "Rename the current entity", false && (selected != null)))
{ } // TODO: Implement this!
ImGui.SameLine();
var isDisabled = (selected?.IsDisabled == true);
var icon = !isDisabled ? Icon.BellSlash : Icon.Bell;
var icon = !isDisabled ? ForkAwesome.BellSlash : ForkAwesome.Bell;
var tooltip = $"{(!isDisabled ? "Disable" : "Enable")} the current entity";
if (IconButtonWithToolTip(icon, tooltip, (selected != null)))
{ if (isDisabled) selected?.Enable(); else selected?.Disable(); }
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.Trash, "Delete the current entity", (selected != null))) {
if (IconButtonWithToolTip(ForkAwesome.Trash, "Delete the current entity", (selected != null))) {
// TODO: Delete history for deleted entity?
SetSelected(window, history, selected?.Parent);
selected?.Delete(); // TODO: Confirmation dialog?
@ -361,9 +376,9 @@ public partial class EntityInspector
var expId = world.Entity<Expanded>().NumericId;
List<IExplorerEntry> GetEntries(Entity? parent) {
var result = new List<IExplorerEntry>();
using var rule = new Rule<T>(world, new(
using var rule = world.Rule(new(
$"(ChildOf, {parent?.NumericId ?? 0})" // Must be child of parent, or root entity.
+ $",?{expId}({window.NumericId}, $This)" // Whether entity is expanded in explorer view.
+ $",?{expId}({window.NumericId}, $this)" // Whether entity is expanded in explorer view.
+ $",?Disabled" // Don't filter out disabled entities.
));
foreach (var iter in rule.Iter()) {
@ -471,13 +486,13 @@ public partial class EntityInspector
var ChildOf = world.Entity<Core.ChildOf>();
var Wildcard = world.Entity<Core.Wildcard>();
if (ImGui.CollapsingHeader($"As {Icon.Tag} Component", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator<T>.FromTerm(world, new(selected)))
if (ImGui.CollapsingHeader($"As {ForkAwesome.Tag} Component", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in world.Term(new(selected)))
for (var i = 0; i < iter.Count; i++)
RenderEntity(window, history, iter.Entity(i));
if (ImGui.CollapsingHeader($"As {Icon.ShareAlt} Relation", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator<T>.FromTerm(world, new(selected, Wildcard))) {
if (ImGui.CollapsingHeader($"As {ForkAwesome.ShareAlt} Relation", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in world.Term(new(selected, Wildcard))) {
var id = iter.FieldId(1);
if (id.AsPair() is not (Entity<T> relation, Entity<T> target)) throw new InvalidOperationException();
if (relation == ChildOf) continue; // Hide ChildOf relations.
@ -489,8 +504,8 @@ public partial class EntityInspector
}
}
if (ImGui.CollapsingHeader($"As {Icon.Bullseye} Target", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator<T>.FromTerm(world, new(Wildcard, selected))) {
if (ImGui.CollapsingHeader($"As {ForkAwesome.Bullseye} Target", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in world.Term(new(Wildcard, selected))) {
var id = iter.FieldId(1);
if (id.AsPair() is not (Entity<T> relation, Entity<T> target)) throw new InvalidOperationException();
if (relation == ChildOf) continue; // Hide ChildOf relations.
@ -521,7 +536,7 @@ public partial class EntityInspector
if (fill) ImGui.SetNextItemWidth(-float.Epsilon);
}
Column($"{Icon.Tag} Display Name", """
Column($"{ForkAwesome.Tag} Display Name", """
A display name for this entity.
Names in the entity hierarchy must be unique within the parent entity,
This doesn't apply to display names - they are mostly informational.
@ -532,7 +547,7 @@ public partial class EntityInspector
selected?.SetDocName((name.Length > 0) ? name : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.Comment} Description",
Column($"{ForkAwesome.Comment} Description",
"A brief description of this entity.");
if (!hasSelected) ImGui.BeginDisabled();
var brief = selected?.GetDocBrief() ?? "";
@ -540,7 +555,7 @@ public partial class EntityInspector
selected?.SetDocBrief((brief.Length > 0) ? brief : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.FileText} Documentation", """
Column($"{ForkAwesome.FileText} Documentation", """
A detailed description, or full documentation, of this entity's purpose and behaviors.
It's encouraged to use multiple paragraphs and markdown formatting if necessary.
""");
@ -556,7 +571,7 @@ public partial class EntityInspector
selected?.SetDocDetail((detail.Length > 0) ? detail : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.Link} Link", """
Column($"{ForkAwesome.Link} Link", """
A link to a website relating to this entity, such as
a module's repository, or further documentation.
""");
@ -566,7 +581,7 @@ public partial class EntityInspector
selected?.SetDocLink((link.Length > 0) ? link : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.PaintBrush} Color", """
Column($"{ForkAwesome.PaintBrush} Color", """
A custom color to represent this entity.
Used in the entity inspector's explorer view.
""", false);
@ -649,7 +664,7 @@ public partial class EntityInspector
var world = entity.World;
var component = world.Entity<Core.Component>();
var rule = (Rule<T>)(_findDisplayTypeRule ??= new Rule<T>(world, new(
var rule = (Rule<T>)(_findDisplayTypeRule ??= world.Rule(new(
$"$Type, gaemstone.Doc.DisplayType($Type)")));
var typeVar = rule.Variables["Type"]!;
@ -660,7 +675,7 @@ public partial class EntityInspector
var type = iter.GetVar(typeVar);
if ((type == component) && (entity.GetOrNull<Core.Component>(component)?.Size == 0))
type = world.Entity<Core.Tag>();
var priority = type?.GetOrNull<DocPriority>()?.Value ?? float.MaxValue;
var priority = type?.GetOrNull<Priority>()?.Value ?? float.MaxValue;
if (priority <= curPriority) { curType = type; curPriority = priority; }
}
@ -705,8 +720,8 @@ public partial class EntityInspector
if (!ImGui.IsRectVisible(pos, pos + dummySize)) { ImGui.Dummy(dummySize); return; }
var (displayType, _) = FindDisplayType(entity);
var docColor = Color.TryParseHex(entity.GetDocColor()) ?? Color.TryParseHex(displayType?.GetDocColor());
var docIcon = entity.GetOrNull<DocIcon>()?.Value.ToString() ?? displayType?.GetOrNull<DocIcon>()?.Value.ToString();
var docColor = Color.TryParseHex(entity.GetDocColor()) ?? Color.TryParseHex(displayType?.GetDocColor());
var docIcon = entity.GetOrNull<Icon>()?.Value.ToString() ?? displayType?.GetOrNull<Icon>()?.Value.ToString();
var docName = entity.GetDocName(false);
var isDisabled = entity.IsDisabled;

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using gaemstone.ECS;
using gaemstone.Flecs;
using Silk.NET.Input;

@ -95,7 +95,7 @@ public partial class Renderer
var cameraMatrix = invertedTransform * cameraProjection;
GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.M11);
var rule = (Rule<T>)(_renderEntityRule ??= new Rule<T>(world, new("""
var rule = (Rule<T>)(_renderEntityRule ??= world.Rule(new("""
[in] GlobalTransform,
(Mesh, $mesh), [in] MeshHandle($mesh),
?(Texture, $tex), [in] ?TextureHandle($tex)

File diff suppressed because it is too large Load Diff

@ -6,8 +6,13 @@ namespace gaemstone.Client.Utility;
public static class ImGuiUtility
{
public static bool UIButtonToggle(int index, char icon, string tooltip, ref bool enabled)
=> UIButtonToggle(index, icon.ToString(), tooltip, ref enabled);
public static bool UIButtonToggle(int index, string label, string tooltip, ref bool enabled)
{ if (UIButton(index, label, tooltip, enabled)) enabled = !enabled; return enabled; }
public static bool UIButton(int index, char icon, string tooltip, bool active)
=> UIButton(index, icon.ToString(), tooltip, active);
public static bool UIButton(int index, string label, string tooltip, bool active)
{
var start = new Vector2(4, 4);

@ -1 +1 @@
Subproject commit c575046a61620434e7ee01679ce86bf0beb0f700
Subproject commit 063fb40c5c56adbd5bedd39045f1b896b4e420c3

@ -120,7 +120,7 @@ public class ModuleGenerator
sb.AppendLine();
sb.AppendLine($$"""
static void IModule.Initialize<T>(Entity<T> module)
static void IModule.OnEnable<T>(Entity<T> module)
{
var world = module.World;
""");
@ -134,12 +134,25 @@ public class ModuleGenerator
// TODO: Can BuiltIn modules have systems and such?
if (module.HasInitializer)
sb.AppendLine("\t\tInitialize(module);");
if (module.HasLifetimeInterface)
sb.AppendLine("\t\tOnEnabled(module);");
sb.AppendLine("\t}");
sb.AppendLine($$"""
}
static void IModule.OnDisable<T>(Entity<T> module)
{
""");
sb.AppendLine("}");
if (module.IsBuiltIn)
sb.AppendLine("\t\tthrow new global::System.InvalidOperationException();");
if (module.HasLifetimeInterface)
sb.AppendLine("\t\tOnDisabled(module);");
sb.AppendLine($$"""
}
}
""");
}
private void AppendEntityRegistration(

@ -88,7 +88,7 @@ public class MethodEntityInfo : BaseEntityInfo
param.TermIndex = termIndex++;
// See if we have any [DependsOn<...>] attributes for this system.
// If not, ModuleGenerator will add [DependsOn<Flecs.Pipeline.OnUpdate>].
// If not, ModuleGenerator will add [DependsOn<gaemstone.Flecs.Pipeline.OnUpdate>].
HasPhaseSet = IsSystem && RelationsToAdd.Any(entry => entry.Relation
.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.DependsOnAttribute");

@ -10,7 +10,7 @@ namespace gaemstone.SourceGen.Structure;
public class ModuleEntityInfo : TypeEntityInfo
{
public bool IsPartial { get; }
public bool HasInitializer { get; }
public bool HasLifetimeInterface { get; }
public ModuleEntityInfo(ISymbol symbol)
: base(symbol)
@ -18,8 +18,8 @@ public class ModuleEntityInfo : TypeEntityInfo
var classDecl = (TypeDeclarationSyntax)Symbol.DeclaringSyntaxReferences.First().GetSyntax();
IsPartial = classDecl.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword));
HasInitializer = Symbol.AllInterfaces.Any(i =>
i.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.IModuleInitializer");
HasLifetimeInterface = Symbol.AllInterfaces.Any(i =>
i.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.IModuleLifetime");
IsBuiltIn = Has("BuiltIn");
}

@ -13,6 +13,14 @@ public partial class Doc
[Tag]
public struct Relation { }
/// <summary>
/// Tags are just <see cref="Flecs.Core.Component"/>s with 0 size.
/// This functions as a special entity that holds the appearance for tags.
/// Not related to <see cref="Flecs.Core.Tag"/> in any way.
/// </summary>
[Tag]
public struct Tag { }
// TODO: These need to actually be read at some point.

@ -6,13 +6,27 @@ namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class ModuleAttribute : SingletonAttribute { }
public interface IModuleLifetime
{
void OnEnable<TContext>(Entity<TContext> module);
void OnDisable<TContext>(Entity<TContext> module);
}
[AttributeUsage(AttributeTargets.Class)]
public class BuiltInAttribute : Attribute { }
public interface IModuleImport
{
static abstract Entity<TContext> Import<TContext>(World<TContext> world);
}
/// <summary>
/// A concrete implementation of this interface is generated by a source
/// generator for each type marked as <see cref="ModuleAttribute"/>.
/// (Do not implement this yourself.)
/// </summary>
public interface IModule
{
@ -22,10 +36,8 @@ public interface IModule
static abstract IReadOnlyList<string> Dependencies { get; }
static abstract void Initialize<TContext>(Entity<TContext> module);
}
public interface IModuleInitializer
{
static abstract void Initialize<TContext>(Entity<TContext> module);
static abstract void OnEnable<TContext>(Entity<TContext> module);
static abstract void OnDisable<TContext>(Entity<TContext> module);
}

@ -4,6 +4,7 @@ namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/core")]
public partial class Core
: IModuleImport
{
// Entity Tags
@ -105,4 +106,8 @@ public partial class Core
public static implicit operator string?(Identifier id)
=> id.ToString();
}
static Entity<T> IModuleImport.Import<T>(World<T> world)
=> world.LookupPathOrThrow("/flecs/core");
}

@ -0,0 +1,25 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/coredoc")]
[DependsOn<gaemstone.Flecs.Meta>]
[DependsOn<gaemstone.Flecs.Doc>]
public unsafe partial class CoreDoc
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &CoreDocImport } },
alloc.AllocateCString("FlecsCoreDoc"))));
}
[UnmanagedCallersOnly]
private static void CoreDocImport(ecs_world_t* world)
=> FlecsCoreDocImport(world);
}

@ -6,7 +6,8 @@ using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/doc")]
public partial class Doc
public unsafe partial class Doc
: IModuleImport
{
[Tag] public struct Brief { }
[Tag] public struct Detail { }
@ -23,6 +24,19 @@ public partial class Doc
public static implicit operator string?(Description desc)
=> desc.ToString();
}
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &DocImport } },
alloc.AllocateCString("FlecsDoc"))));
}
[UnmanagedCallersOnly]
private static void DocImport(ecs_world_t* world)
=> FlecsDocImport(world);
}
public static unsafe class DocExtensions

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/meta")]
public unsafe partial class Meta
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &MetaImport } },
alloc.AllocateCString("FlecsMeta"))));
}
[UnmanagedCallersOnly]
private static void MetaImport(ecs_world_t* world)
=> FlecsMetaImport(world);
}

@ -0,0 +1,26 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/timer")]
[DependsOn<gaemstone.Flecs.Pipeline>]
[DependsOn<gaemstone.Flecs.Meta>]
[DependsOn<gaemstone.Flecs.Units>]
public unsafe partial class Metrics
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &MetricsImport } },
alloc.AllocateCString("FlecsMetrics"))));
}
[UnmanagedCallersOnly]
private static void MetricsImport(ecs_world_t* world)
=> FlecsMetricsImport(world);
}

@ -3,18 +3,18 @@ using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs.Systems;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/monitor")]
public unsafe partial class Monitor
: IModuleInitializer
: IModuleImport
{
public static void Initialize<T>(Entity<T> module)
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
ecs_import_c(module.World,
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &MonitorImport } },
alloc.AllocateCString("FlecsMonitor"));
alloc.AllocateCString("FlecsMonitor"))));
}
[UnmanagedCallersOnly]

@ -1,9 +1,14 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/pipeline")]
public partial class Pipeline
[DependsOn<gaemstone.Flecs.System>]
public unsafe partial class Pipeline
: IModuleImport
{
[Entity] public struct Phase { }
@ -90,4 +95,17 @@ public partial class Pipeline
[Entity, Add<Phase>]
[DependsOn<OnStore>]
public struct PostFrame { }
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &PipelineImport } },
alloc.AllocateCString("FlecsPipeline"))));
}
[UnmanagedCallersOnly]
private static void PipelineImport(ecs_world_t* world)
=> FlecsPipelineImport(world);
}

@ -3,22 +3,26 @@ using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs.Systems;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/rest")]
[DependsOn<gaemstone.Flecs.Pipeline>]
public unsafe partial class Rest
: IModuleInitializer
: IModuleImport
{
public static void Initialize<T>(Entity<T> module)
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using (var alloc = TempAllocator.Use())
ecs_import_c(module.World,
new() { Data = new() { Pointer = &RestImport } },
alloc.AllocateCString("FlecsRest"));
using var alloc = TempAllocator.Use();
var module = Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &RestImport } },
alloc.AllocateCString("FlecsRest"))));
module.NewChild("Rest").Build()
.CreateLookup<EcsRest>()
.Set(new EcsRest { port = 27750 });
return module;
}
[UnmanagedCallersOnly]

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/script")]
[DependsOn<gaemstone.Flecs.Meta>]
public unsafe partial class Script
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &ScriptImport } },
alloc.AllocateCString("FlecsScript"))));
}
[UnmanagedCallersOnly]
private static void ScriptImport(ecs_world_t* world)
=> FlecsScriptImport(world);
}

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/system")]
public unsafe partial class System
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &SystemImport } },
alloc.AllocateCString("FlecsSystem"))));
}
[UnmanagedCallersOnly]
private static void SystemImport(ecs_world_t* world)
=> FlecsSystemImport(world);
}

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/timer")]
[DependsOn<gaemstone.Flecs.Pipeline>]
public unsafe partial class Timer
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &TimerImport } },
alloc.AllocateCString("FlecsTimer"))));
}
[UnmanagedCallersOnly]
private static void TimerImport(ecs_world_t* world)
=> FlecsTimerImport(world);
}

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[BuiltIn, Module, Path("/flecs/units")]
public unsafe partial class Units
: IModuleImport
{
static Entity<T> IModuleImport.Import<T>(World<T> world)
{
using var alloc = TempAllocator.Use();
return Entity<T>.GetOrThrow(world, new(ecs_import_c(world,
new() { Data = new() { Pointer = &UnitsImport } },
alloc.AllocateCString("FlecsUnits"))));
}
[UnmanagedCallersOnly]
private static void UnitsImport(ecs_world_t* world)
=> FlecsUnitsImport(world);
}

@ -1,8 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using gaemstone.ECS;
using static gaemstone.Flecs.Core;
using Module = gaemstone.Flecs.Core.Module;
@ -10,126 +6,100 @@ using Module = gaemstone.Flecs.Core.Module;
namespace gaemstone;
public class ModuleManager<TContext>
: IEnumerable<ModuleManager<TContext>.IModuleInfo>
{
private readonly Dictionary<Entity<TContext>, IModuleInfo> _modules = new();
public Universe<TContext> Universe { get; }
public ModuleManager(Universe<TContext> universe)
=> Universe = universe;
public World<TContext> World => Universe.World;
internal IModuleInfo? Lookup(Entity<TContext> entity)
=> _modules.GetValueOrDefault(entity);
private readonly Rule<TContext> _findDisabledDeps;
private readonly Rule<TContext> _findDependents;
public Entity<TContext> Register<T>()
where T : IModule
private readonly ECS.Variable _findDisabledDepsThisVar;
private readonly ECS.Variable _findDependentsModuleVar;
public ModuleManager(Universe<TContext> universe)
{
// if (!typeof(T).IsAssignableTo(typeof(IModule))) throw new ArgumentException(
// $"The specified type {typeof(T)} does not implement IModule", nameof(T));
Universe = universe;
var module = new ModuleInfo<T>(Universe);
_modules.Add(module.Entity, module);
TryEnableModule(module);
return module.Entity;
World.New("/gaemstone/ModuleInfo").Symbol("ModuleInfo")
.Build().InitComponent<IModuleInfo>();
_findDisabledDeps = new(World, new("(DependsOn, $dep), Disabled($dep)"));
_findDependents = new(World, new("ModuleInfo, Disabled, (DependsOn, $module)"));
_findDisabledDepsThisVar = _findDisabledDeps.ThisVar
?? throw new InvalidOperationException($"Could not find $this of {nameof(_findDisabledDeps)}");
_findDependentsModuleVar = _findDependents.Variables["module"]
?? throw new InvalidOperationException($"Could not find $module of {nameof(_findDependents)}");
}
private void TryEnableModule(IModuleInfo module)
public Entity<TContext> Import<T>()
where T : IModule, IModuleImport
{
if (!module.Dependencies.All(dep => dep.IsDependencyMet)) return;
foreach (var dep in T.Dependencies)
if (World.LookupPathOrNull(dep) == null) throw new InvalidOperationException(
$"Missing required dependency {dep} for built-in module {T.Path}");
Console.WriteLine($"Enabling module {module.Entity.Path}");
Console.WriteLine($"Importing built-in module {T.Path}");
module.Enable();
var entity = T.Import(World);
entity.Set<IModuleInfo>(new ModuleInfo<T>());
T.OnEnable(entity);
return entity;
}
// Find other modules that might be missing this module as a dependency.
foreach (var other in _modules.Values) {
if (other.IsInitialized) continue;
var dependency = other.Dependencies.FirstOrDefault(dep => dep.Entity == module.Entity);
if (dependency == null) continue;
public Entity<TContext> Register<T>()
where T : IModule
{
if (T.IsBuiltIn) throw new ArgumentException(
$"Unexpected operation, {T.Path} is a built-in module");
dependency.Info = module;
dependency.IsDependencyMet = true;
var builder = World.New(T.Path)
.Add<Module>().Add<Disabled>()
.Set<IModuleInfo>(new ModuleInfo<T>());
TryEnableModule(other);
foreach (var depPath in T.Dependencies) {
var dependency = World.LookupPathOrNull(depPath) ??
World.New(depPath).Add<Module>().Add<Disabled>().Build();
builder.Add<DependsOn>(dependency);
}
var module = builder.Build().CreateLookup<T>();
// Ensure all parent entities have the Module tag set.
for (var p = module.Parent; p is Entity<TContext> parent; p = parent.Parent)
parent.Add<Module>();
Console.WriteLine($"Registered module {module.Path}");
TryEnableModule(module);
return module;
}
public interface IModuleInfo
private void TryEnableModule(Entity<TContext> module)
{
Entity<TContext> Entity { get; }
IReadOnlyCollection<ModuleDependency> Dependencies { get; }
bool IsInitialized { get; }
void Enable();
}
// Skip if module is already enabled.
if (module.IsEnabled) return;
// Skip if module has any not-yet-enabled dependencies.
if (_findDisabledDeps.Iter().SetVar(_findDisabledDepsThisVar, module).Any()) return;
// IEnumerable implementation
public IEnumerator<IModuleInfo> GetEnumerator() => _modules.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
Console.WriteLine($"Enabling module {module.Path}");
module.GetOrThrow<IModuleInfo>().OnEnable(module);
module.Enable();
// Get all modules that depend on this one and try to enabled them if they now have their dependencies met.
foreach (var dependent in _findDependents.Iter().SetVar(_findDependentsModuleVar, module).GetAllEntities())
TryEnableModule(dependent);
}
public class ModuleDependency
public interface IModuleInfo
{
public Entity<TContext> Entity { get; }
public IModuleInfo? Info { get; internal set; }
public bool IsDependencyMet { get; internal set; }
public ModuleDependency(Entity<TContext> entity,
IModuleInfo? info = null, bool isDependencyMet = false)
{
Entity = entity;
Info = info;
IsDependencyMet = isDependencyMet;
}
void OnEnable(Entity<TContext> entity);
}
internal class ModuleInfo<T> : IModuleInfo
where T : IModule
{
public Entity<TContext> Entity { get; }
public IReadOnlyCollection<ModuleDependency> Dependencies { get; }
public bool IsInitialized { get; private set; }
public ModuleInfo(Universe<TContext> universe)
{
var world = universe.World;
if (T.IsBuiltIn)
{
Entity = world.LookupPathOrThrow(T.Path);
Dependencies = Array.Empty<ModuleDependency>();
}
else
{
var builder = world.New(T.Path);
var deps = new List<ModuleDependency>();
builder.Add<Module>();
foreach (var dependsPath in T.Dependencies) {
var dependency = world.LookupPathOrNull(dependsPath) ??
world.New(dependsPath).Add<Module>().Add<Disabled>().Build();
var depModule = universe.Modules.Lookup(dependency);
var isDepInit = (depModule?.IsInitialized == true);
deps.Add(new(dependency, depModule, isDepInit));
if (!isDepInit) builder.Add<Disabled>();
builder.Add<DependsOn>(dependency);
}
Entity = builder.Build().CreateLookup<T>();
Dependencies = deps.AsReadOnly();
// Ensure all parent entities have the Module tag set.
for (var p = Entity.Parent; p is Entity<TContext> parent; p = parent.Parent)
parent.Add<Module>();
}
}
public void Enable()
{
Entity.Enable();
T.Initialize(Entity);
IsInitialized = true;
}
public void OnEnable(Entity<TContext> entity)
=> T.OnEnable(entity);
}
}

@ -9,17 +9,22 @@ public class Universe<TContext>
public Universe()
{
World = new();
World = new(minimal: true);
Modules = new(this);
// Bootstrap [Relation] tag, since it will be added to some Flecs types.
World.New("/gaemstone/Doc/Relation").Build()
.CreateLookup<Doc.Relation>();
// Bootstrap built-in (static) modules from Flecs.
Modules.Register<Flecs.Core>();
Modules.Register<Flecs.Doc>();
Modules.Register<Flecs.Pipeline>();
// Bootstrap core module from Flecs.
Modules.Import<Flecs.Core>();
// Import addon modules from Flecs we use for the engine.
Modules.Import<Flecs.System>();
Modules.Import<Flecs.Pipeline>();
Modules.Import<Flecs.Meta>();
Modules.Import<Flecs.Doc>();
Modules.Import<Flecs.CoreDoc>();
Modules.Register<Doc>();
}

Loading…
Cancel
Save