From 03d672febfeb11ae540f369c903bb455ef1611eb Mon Sep 17 00:00:00 2001 From: copygirl Date: Tue, 3 Jan 2023 14:29:49 +0100 Subject: [PATCH] Source Generators, step 2: Modules - Add IModule interface, contains static interface methods to get its path and dependencies - ModuleGenerator generates the implementation - Universe.Modules.Register now expects an IModule - Replace ModuleInfo with generic version - Remove GetModulePath helper method --- .../AutoRegisterComponentsGenerator.cs | 4 +- .../Generators/ModuleGenerator.cs | 65 +++++++++++ src/gaemstone/ECS/Module.cs | 19 +++- src/gaemstone/Universe+Modules.cs | 102 ++++++------------ 4 files changed, 113 insertions(+), 77 deletions(-) diff --git a/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs b/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs index e744a70..75b4fc9 100644 --- a/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs +++ b/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs @@ -123,7 +123,7 @@ public partial class AutoRegisterComponentsGenerator // using gaemstone.ECS; - namespace {{module.Namespace}}; + namespace {{ module.Namespace }}; """); @@ -146,7 +146,7 @@ public partial class AutoRegisterComponentsGenerator } } else { sb.AppendLine($$""" - public partial class {{module.Name}} + public partial class {{ module.Name }} : IModuleAutoRegisterComponents { public void RegisterComponents(EntityRef module) diff --git a/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs b/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs index f3b8f30..880f4e8 100644 --- a/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs +++ b/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs @@ -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(); + 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($$""" + // + 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 Dependencies { get; } = new[] { + """); + foreach (var dependency in dependencies) + sb.AppendLine($$""" {{ dependency.ToStringLiteral() }},"""); + sb.AppendLine($$""" + }; + """); + } else sb.AppendLine($$""" + public static IEnumerable Dependencies { get; } = Enumerable.Empty(); + """); + + + 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}"; } } diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 77671be..3e01260 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -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 Dependencies { get; } +} + +public interface IModuleAutoRegisterComponents +{ + void RegisterComponents(EntityRef module); } diff --git a/src/gaemstone/Universe+Modules.cs b/src/gaemstone/Universe+Modules.cs index f247866..b0ea253 100644 --- a/src/gaemstone/Universe+Modules.cs +++ b/src/gaemstone/Universe+Modules.cs @@ -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() - 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()) 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(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(); - 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() 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 MetDependencies { get; } = new(); + public HashSet 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 : 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(); - public HashSet MetDependencies { get; } = new(); - public HashSet UnmetDependencies { get; } = new(); + foreach (var dependsPath in T.Dependencies) { + var dependency = universe.LookupByPath(dependsPath) ?? + universe.New(dependsPath).Add().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(); - - // 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().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(dependency); + builder.Add(dependency); } - Entity = module.Build().CreateLookup(Type); + Entity = builder.Build().CreateLookup(); // Ensure all parent entities have Module set. for (var p = Entity.Parent; p != null; p = p.Parent) p.Add(); } - 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()) - Universe.InitSystem(instance, method).ChildOf(Entity); + world.InitSystem(instance, method).ChildOf(Entity); if (method.Has()) - Universe.InitObserver(instance, method).ChildOf(Entity); + world.InitObserver(instance, method).ChildOf(Entity); } } }