Compare commits

...

2 Commits

Author SHA1 Message Date
copygirl 976d9fd326 Replace [Symbol] with [Public] and [Private] 1 year ago
copygirl 03d672febf Source Generators, step 2: Modules 1 year ago
  1. 6
      src/gaemstone.Bloxel/Components/CoreComponents.cs
  2. 4
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  3. 26
      src/gaemstone.Client/Components/InputComponents.cs
  4. 8
      src/gaemstone.Client/Components/RenderingComponents.cs
  5. 8
      src/gaemstone.Client/Components/ResourceComponents.cs
  6. 15
      src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs
  7. 65
      src/gaemstone.SourceGen/Generators/ModuleGenerator.cs
  8. 4
      src/gaemstone/Components/TransformComponents.cs
  9. 18
      src/gaemstone/ECS/Module+Attributes.cs
  10. 19
      src/gaemstone/ECS/Module.cs
  11. 102
      src/gaemstone/Universe+Modules.cs

@ -2,17 +2,17 @@ using gaemstone.ECS;
namespace gaemstone.Bloxel.Components;
[Module]
[Public, Module]
public partial class CoreComponents
{
[Symbol, Component]
[Component]
public readonly struct Chunk
{
public ChunkPos Position { get; }
public Chunk(ChunkPos pos) => Position = pos;
}
[Symbol, Component]
[Component]
public class ChunkStoreBlocks
: ChunkPaletteStorage<Entity>
{

@ -6,7 +6,7 @@ using static gaemstone.Bloxel.Constants;
namespace gaemstone.Bloxel.Systems;
[Module]
[Public, Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
public partial class BasicWorldGenerator
{
@ -22,7 +22,7 @@ public partial class BasicWorldGenerator
_noise.SetFractalGain(0.6f);
}
[Symbol, Tag]
[Tag]
public struct HasBasicWorldGeneration { }
[System]

@ -4,33 +4,33 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
[Public, Module]
public partial class InputComponents
{
[Symbol, Entity, Path("/Input")]
[Entity, Path("/Input")]
[Add<Input>]
public struct Input { }
[Symbol, Entity, Path("/Input/Mouse")]
[Entity, Path("/Input/Mouse")]
[Add<Mouse>]
public struct Mouse { }
[Symbol, Entity, Path("/Input/Keyboard")]
[Entity, Path("/Input/Keyboard")]
[Add<Keyboard>]
public struct Keyboard { }
[Symbol, Tag]
[Tag]
public struct Gamepad { }
/// <summary> Present on inputs / actions that are currently active. </summary>
[Symbol, Component] public struct Active { public TimeSpan Duration; }
[Component] public struct Active { public TimeSpan Duration; }
/// <summary> Present on inputs / actions were activated this frame. </summary>
[Symbol, Tag] public struct Activated { }
[Tag] public struct Activated { }
/// <summary> Present on inputs / actions were deactivated this frame. </summary>
[Symbol, Tag] public struct Deactivated { }
[Tag] public struct Deactivated { }
/// <summary>
@ -41,7 +41,7 @@ public partial class InputComponents
/// This is set if a UI element is focused that captures
/// navigational or text input.
/// </remarks>
[Symbol, Relation, Tag, Exclusive]
[Relation, Tag, Exclusive]
public struct InputCapturedBy { }
/// <summary>
@ -52,7 +52,7 @@ public partial class InputComponents
/// This could for example include the mouse currently being over
/// a UI element, preventing the game from handling mouse input.
/// </remarks>
[Symbol, Relation, Tag, Exclusive]
[Relation, Tag, Exclusive]
public struct MouseInputCapturedBy { }
/// <summary>
@ -62,13 +62,13 @@ public partial class InputComponents
/// <remarks>
/// This is set when a camera controller assumes control of the mouse.
/// </remarks>
[Symbol, Relation, Tag, Exclusive]
[Relation, Tag, Exclusive]
[With<InputCapturedBy>]
[With<MouseInputCapturedBy>]
public struct CursorCapturedBy { }
[Component]
[Private, Component]
internal readonly struct RawValue1D
{
private readonly float _value;
@ -78,7 +78,7 @@ public partial class InputComponents
public static implicit operator RawValue1D(float value) => new(value);
}
[Component]
[Private, Component]
internal readonly struct RawValue2D
{
private readonly Vector2 _value;

@ -5,10 +5,10 @@ using Silk.NET.OpenGL;
namespace gaemstone.Client.Components;
[Module]
[Public, Module]
public partial class RenderingComponents
{
[Symbol, Component]
[Component]
public readonly struct MeshHandle
{
public uint Handle { get; }
@ -19,7 +19,7 @@ public partial class RenderingComponents
{ Handle = handle; Count = count; IsIndexed = indexed; }
}
[Symbol, Component]
[Component]
public readonly struct TextureHandle
{
public TextureTarget Target { get; }
@ -29,7 +29,7 @@ public partial class RenderingComponents
=> (Target, Handle) = (target, handle);
}
[Symbol, Component]
[Component]
public readonly struct TextureCoords4
{
public Vector2 TopLeft { get; }

@ -2,10 +2,10 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
[Public, Module]
public partial class ResourceComponents
{
[Symbol, Tag]
[Tag]
public struct Resource { }
// Entities can have for example Texture as a tag, in which case
@ -14,9 +14,9 @@ public partial class ResourceComponents
// Entities can also have a (Texture, $T) pair where $T is a resource,
// meaning the entity has that resource assigned as their texture.
[Symbol, Relation, Tag, IsA<Resource>]
[Relation, Tag, IsA<Resource>]
public struct Texture { }
[Symbol, Relation, Tag, IsA<Resource>]
[Relation, Tag, IsA<Resource>]
public struct Mesh { }
}

@ -9,6 +9,8 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace gaemstone.SourceGen.Generators;
// TODO: "Jump to Definition" feature to open the file + line in which a module / component / system is defined.
[Generator]
public partial class AutoRegisterComponentsGenerator
: ISourceGenerator
@ -123,7 +125,7 @@ public partial class AutoRegisterComponentsGenerator
// <auto-generated/>
using gaemstone.ECS;
namespace {{module.Namespace}};
namespace {{ module.Namespace }};
""");
@ -146,7 +148,7 @@ public partial class AutoRegisterComponentsGenerator
}
} else {
sb.AppendLine($$"""
public partial class {{module.Name}}
public partial class {{ module.Name }}
: IModuleAutoRegisterComponents
{
public void RegisterComponents(EntityRef module)
@ -158,7 +160,7 @@ public partial class AutoRegisterComponentsGenerator
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.New(module, {{ path }})""");
if (c.IsSymbol) sb.AppendLine($$""" .Symbol("{{ c.Name }}")""");
if (c.IsPublic) sb.AppendLine($$""" .Symbol("{{ c.Name }}")""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
if (c.IsTag) sb.AppendLine($$""" .Add<gaemstone.Flecs.Core.Tag>()""");
if (c.IsComponent) sb.AppendLine($$""" .Build().InitComponent<{{ c.Name }}>()""");
@ -194,6 +196,7 @@ public partial class AutoRegisterComponentsGenerator
public List<ComponentInfo> Components { get; } = new();
public string? Path { get; }
public bool IsPublic { get; }
public bool IsStatic => Symbol.IsStatic;
public ModuleInfo(INamedTypeSymbol symbol)
@ -201,6 +204,7 @@ public partial class AutoRegisterComponentsGenerator
Symbol = symbol;
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute");
}
}
@ -215,7 +219,7 @@ public partial class AutoRegisterComponentsGenerator
public List<(ITypeSymbol, ITypeSymbol)> RelationsToAdd { get; }
public string? Path { get; }
public bool IsSymbol { get; } // TODO: Get rid of this.
public bool IsPublic { get; }
public bool IsRelation { get; }
public bool IsTag => (Type is RegisterType.Tag);
public bool IsComponent => (Type is RegisterType.Component
@ -235,7 +239,8 @@ public partial class AutoRegisterComponentsGenerator
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
IsSymbol = symbol.HasAttribute("gaemstone.ECS.SymbolAttribute");
IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute")
|| (Module.IsPublic && !symbol.HasAttribute("gaemstone.ECS.PrivateAttribute"));
IsRelation = symbol.HasAttribute("gaemstone.ECS.RelationAttribute");
SingletonAddSelf = IsSingleton

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@ -71,6 +72,70 @@ public class ModuleGenerator
.ConstructorArguments.FirstOrDefault().Value == null))
context.ReportDiagnostic(Diagnostic.Create(ModuleBuiltInMustHavePath,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
// Static classes can't implement interfaces.
if (symbol.IsStatic) continue;
var modulePath = GetModulePath(symbol).ToStringLiteral();
var dependencies = new List<string>();
foreach (var attr in symbol.GetAttributes())
for (var type = attr.AttributeClass; type != null; type = type.BaseType)
if ((type.GetFullName(true) == "gaemstone.ECS.AddAttribute`2")
&& type.TypeArguments[0].GetFullName() == "gaemstone.Flecs.Core.DependsOn")
dependencies.Add(GetModulePath(type.TypeArguments[1]));
var sb = new StringBuilder();
sb.AppendLine($$"""
// <auto-generated/>
using System.Collections.Generic;
using System.Linq;
using gaemstone.ECS;
namespace {{ symbol.GetNamespace() }};
public partial class {{ symbol.Name }}
: IModule
{
public static string ModulePath { get; }
= {{ modulePath }};
""");
if (dependencies.Count > 0) {
sb.AppendLine($$"""
public static IEnumerable<string> Dependencies { get; } = new[] {
""");
foreach (var dependency in dependencies)
sb.AppendLine($$""" {{ dependency.ToStringLiteral() }},""");
sb.AppendLine($$"""
};
""");
} else sb.AppendLine($$"""
public static IEnumerable<string> Dependencies { get; } = Enumerable.Empty<string>();
""");
sb.AppendLine($$"""
}
""");
context.AddSource($"{symbol.GetFullName()}_Module.g.cs", sb.ToString());
}
}
private static string GetModulePath(ISymbol module)
{
var path = module.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
var isAbsolute = (path?.FirstOrDefault() == '/');
if (isAbsolute) return path!;
var fullPath = module.GetFullName().Replace('.', '/');
if (path != null) {
var index = fullPath.LastIndexOf('/');
fullPath = $"{fullPath.Substring(0, index)}/{path}";
}
return $"/{fullPath}";
}
}

@ -3,10 +3,10 @@ using gaemstone.ECS;
namespace gaemstone.Components;
[Module]
[Public, Module]
public partial class TransformComponents
{
[Symbol, Component]
[Component]
public struct GlobalTransform
{
public Matrix4x4 Value;

@ -14,12 +14,22 @@ public class PathAttribute : Attribute
}
/// <summary>
/// Register the entity under a globally unique symbol.
/// Uses the type's name by default.
/// <p>
/// Components marked with this attribute are automatically registered with
/// a <see cref="EntityRef.Symbol"/> equal to their <see cref="EntityRef.Name"/>.
/// Symbols are unique string identifiers used to look up their entities.
/// </p>
/// <p>
/// Modules marked with this attribute apply this property to all their
/// components, except ones marked with <see cref="PrivateAttribute"/>.
/// </p>
/// </summary>
// TODO: Remove [Symbol], introduce [Public] for modules and [Private] or so.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SymbolAttribute : Attribute { }
public class PublicAttribute : Attribute { }
/// <seealso cref="PublicAttribute"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class PrivateAttribute : Attribute { }
/// <summary>

@ -1,16 +1,27 @@
using System;
using System.Collections.Generic;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class)]
public class ModuleAttribute : Attribute { }
public interface IModuleAutoRegisterComponents
public interface IModuleInitializer
{
void RegisterComponents(EntityRef module);
void Initialize(EntityRef module);
}
public interface IModuleInitializer
// The following will be implemented on [Module]
// types automatically for you by source generators.
public interface IModule
{
void Initialize(EntityRef module);
static abstract string ModulePath { get; }
static abstract IEnumerable<string> Dependencies { get; }
}
public interface IModuleAutoRegisterComponents
{
void RegisterComponents(EntityRef module);
}

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using gaemstone.ECS;
using gaemstone.Utility;
@ -21,16 +20,13 @@ public class ModuleManager
=> _modules.GetValueOrDefault(entity);
public EntityRef Register<T>()
where T : class, new()
where T : class, IModule, new()
{
var moduleType = typeof(T);
if (moduleType.IsGenericType) throw new Exception(
$"Module {moduleType} must be a non-generic class");
if (!moduleType.Has<ModuleAttribute>()) throw new Exception(
$"Module {moduleType} must be marked with ModuleAttribute");
var path = GetModulePath(moduleType);
var module = new ModuleInfo(Universe, moduleType, path);
var module = new ModuleInfo<T>(Universe);
_modules.Add(module.Entity, module);
TryEnableModule(module);
return module.Entity;
@ -40,7 +36,7 @@ public class ModuleManager
{
if (module.UnmetDependencies.Count > 0) return;
Console.WriteLine($"Enabling module {module.Path} ...");
Console.WriteLine($"Enabling module {module.Entity.GetFullPath()} ...");
module.Enable();
// Find other modules that might be missing this module as a dependency.
@ -56,102 +52,66 @@ public class ModuleManager
}
}
public static EntityPath GetModulePath(Type type)
internal abstract class ModuleInfo
{
var attr = type.Get<ModuleAttribute>();
if (attr == null) throw new ArgumentException(
$"Module {type} must be marked with ModuleAttribute", nameof(type));
public abstract EntityRef Entity { get; }
public abstract bool IsActive { get; }
var path = EntityPath.Parse(
(type.Get<PathAttribute>() is PathAttribute pathAttr)
? pathAttr.Value : type.Name);
// If specified path is absolute, return it now.
if (path.IsAbsolute) return path;
// Otherwise, create it based on the type's assembly, namespace and name.
var assemblyName = type.Assembly.GetName().Name!;
if (!type.FullName!.StartsWith(assemblyName + '.')) throw new InvalidOperationException(
$"Module {type} must be defined under namespace {assemblyName}");
var fullNameWithoutAssembly = type.FullName![(assemblyName.Length + 1)..];
public HashSet<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> UnmetDependencies { get; } = new();
var parts = fullNameWithoutAssembly.Split('.')[..^1];
return new(true, parts.Prepend(assemblyName).Concat(path.GetParts()).ToArray());
public abstract void Enable();
}
internal class ModuleInfo
internal class ModuleInfo<T> : ModuleInfo
where T : IModule, new()
{
public Universe Universe { get; }
public Type Type { get; }
public EntityPath Path { get; }
public override EntityRef Entity { get; }
public override bool IsActive => (Instance != null);
public T? Instance { get; internal set; }
public EntityRef Entity { get; }
public object? Instance { get; internal set; }
public bool IsActive => Instance != null;
public ModuleInfo(Universe universe)
{
var builder = universe.New(T.ModulePath).Add<Module>();
public HashSet<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> UnmetDependencies { get; } = new();
foreach (var dependsPath in T.Dependencies) {
var dependency = universe.LookupByPath(dependsPath) ??
universe.New(dependsPath).Add<Module>().Disable().Build();
public ModuleInfo(Universe universe, Type type, EntityPath path)
{
Universe = universe;
Type = type;
Path = path;
if (Type.IsAbstract || Type.IsSealed) throw new Exception(
$"Module {Type} must not be abstract, sealed or static");
if (Type.GetConstructor(Type.EmptyTypes) == null) throw new Exception(
$"Module {Type} must define public parameterless constructor");
var module = Universe.New(Path).Add<Module>();
// Add module dependencies from [DependsOn<>] attributes.
foreach (var attr in Type.GetCustomAttributes()) {
// TODO: Do this without reflection.
var attrType = attr.GetType();
if (!attrType.IsGenericType) continue;
if (attrType.GetGenericTypeDefinition() != typeof(DependsOnAttribute<>)) continue;
var dependsTarget = attrType.GenericTypeArguments[0];
var dependsPath = GetModulePath(dependsTarget);
var dependency = Universe.LookupByPath(dependsPath) ??
Universe.New(dependsPath).Add<Module>().Disable().Build();
var depModule = Universe.Modules.Lookup(dependency);
var depModule = universe.Modules.Lookup(dependency);
if (depModule?.IsActive == true) MetDependencies.Add(depModule);
else { UnmetDependencies.Add(dependency); module.Disable(); }
else { UnmetDependencies.Add(dependency); builder.Disable(); }
module.Add<DependsOn>(dependency);
builder.Add<DependsOn>(dependency);
}
Entity = module.Build().CreateLookup(Type);
Entity = builder.Build().CreateLookup<T>();
// Ensure all parent entities have Module set.
for (var p = Entity.Parent; p != null; p = p.Parent)
p.Add<Module>();
}
public void Enable()
public override void Enable()
{
Entity.Enable();
Instance = Activator.CreateInstance(Type)!; // TODO: Replace with generic new() somehow.
if (Instance is IModuleAutoRegisterComponents generatedComponents)
generatedComponents.RegisterComponents(Entity);
Instance = new T();
(Instance as IModuleAutoRegisterComponents)?.RegisterComponents(Entity);
(Instance as IModuleInitializer)?.Initialize(Entity);
RegisterMethods(Instance);
}
private void RegisterMethods(object? instance)
{
foreach (var method in Type.GetMethods(
var world = Entity.World;
foreach (var method in typeof(T).GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance
)) {
if (method.Has<SystemAttribute>())
Universe.InitSystem(instance, method).ChildOf(Entity);
world.InitSystem(instance, method).ChildOf(Entity);
if (method.Has<ObserverAttribute>())
Universe.InitObserver(instance, method).ChildOf(Entity);
world.InitObserver(instance, method).ChildOf(Entity);
}
}
}

Loading…
Cancel
Save