using System; using System.Collections.Generic; using System.Linq; using gaemstone.ECS; using gaemstone.Utility; using static gaemstone.Flecs.Core; using BindingFlags = System.Reflection.BindingFlags; namespace gaemstone; public class ModuleManager { private readonly Dictionary _modules = new(); public Universe Universe { get; } public ModuleManager(Universe universe) => Universe = universe; internal ModuleInfo? Lookup(Entity entity) => _modules.GetValueOrDefault(entity); public void RegisterAllFrom(System.Reflection.Assembly assembly) => RegisterAll(assembly.GetTypes()); public void RegisterAll(IEnumerable types) { foreach (var type in types) if (type.Has()) Register(type); } public EntityRef Register() where T : class => Register(typeof(T)); public EntityRef Register(Type moduleType) { if (!moduleType.IsClass || moduleType.IsGenericType || moduleType.IsGenericTypeDefinition) throw new Exception( $"Module {moduleType} must be a non-generic class"); if (moduleType.Get() is not ModuleAttribute moduleAttr) throw new Exception( $"Module {moduleType} must be marked with ModuleAttribute"); // Check if module type is static. if (moduleType.IsAbstract && moduleType.IsSealed) { // Static modules represent existing modules, as such they don't // create entities, only look up existing ones to add type lookups // for use with the Lookup(Type) method. if (moduleType.Get()?.Value is not string modulePathStr) throw new Exception( $"Existing module {moduleType} must have {nameof(PathAttribute)} set"); var modulePath = EntityPath.Parse(modulePathStr); var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception( $"Existing module {moduleType} with name '{modulePath}' not found"); // This implementation is pretty naive. It simply gets all nested // types which are tagged with an ICreateEntityAttribute base // attribute and creates a lookup mapping. No sanity checking. foreach (var type in moduleType.GetNestedTypes()) { if (!type.GetCustomAttributes(true).OfType().Any()) continue; var attr = type.Get(); var path = EntityPath.Parse(attr?.Value ?? type.Name); var entity = Universe.LookupByPathOrThrow(moduleEntity, path); entity.CreateLookup(type); if (type.Has()) entity.Add(); } return moduleEntity; } else { var path = GetModulePath(moduleType); var module = new ModuleInfo(Universe, moduleType, path); _modules.Add(module.Entity, module); TryEnableModule(module); return module.Entity; } } private void TryEnableModule(ModuleInfo module) { if (module.UnmetDependencies.Count > 0) return; Console.WriteLine($"Enabling module {module.Path} ..."); module.Enable(); // Find other modules that might be missing this module as a dependency. foreach (var other in _modules.Values) { if (other.IsActive) continue; if (!other.UnmetDependencies.Contains(module.Entity)) continue; // Move the just enabled module from unmet to met depedencies. other.UnmetDependencies.Remove(module.Entity); other.MetDependencies.Add(module); TryEnableModule(other); } } public static EntityPath GetModulePath(Type type) { var attr = type.Get(); if (attr == null) throw new ArgumentException( $"Module {type} must be marked with ModuleAttribute", nameof(type)); 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)..]; var parts = fullNameWithoutAssembly.Split('.')[..^1]; return new(true, parts.Prepend(assemblyName).Concat(path.GetParts()).ToArray()); } internal class ModuleInfo { public Universe Universe { get; } public Type Type { get; } public EntityPath Path { get; } public EntityRef Entity { get; } public object? Instance { get; internal set; } public bool IsActive => Instance != null; public HashSet MetDependencies { get; } = new(); public HashSet UnmetDependencies { get; } = new(); 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 dependsAttr in Type.GetMultiple().Where(attr => attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) { var dependsPath = GetModulePath(dependsAttr.Target); var dependency = Universe.LookupByPath(dependsPath) ?? Universe.New(dependsPath).Add().Disable().Build(); var depModule = Universe.Modules.Lookup(dependency); if (depModule?.IsActive == true) MetDependencies.Add(depModule); else { UnmetDependencies.Add(dependency); module.Disable(); } module.Add(dependency); } Entity = module.Build().CreateLookup(Type); // Ensure all parent entities have Module set. for (var p = Entity.Parent; p != null; p = p.Parent) p.Add(); } public void Enable() { Entity.Enable(); Instance = Activator.CreateInstance(Type)!; foreach (var type in Type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) RegisterNestedType(type); (Instance as IModuleInitializer)?.Initialize(Entity); RegisterMethods(Instance); } private void RegisterNestedType(Type type) { if (!type.GetCustomAttributes(true).OfType().Any()) return; // If proxied type is specified, use it instead of the marked type. // Attributes are still read from the original type. var proxyType = type.Get()?.Type ?? type; if (!type.Has() && (!proxyType.IsValueType || (proxyType.GetFields().Length > 0))) { var typeHint = (proxyType != type) ? $"{proxyType.Name} (proxied by {type})" : type.ToString(); throw new Exception($"Type {typeHint} must be an empty, used-defined struct."); } var path = EntityPath.Parse(type.Get()?.Value ?? proxyType.Name); var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path); if (type.Get() is SymbolAttribute symbolAttr) builder.Symbol(symbolAttr.Value ?? path.Name); var entity = builder.Build(); EntityRef Lookup(Type toLookup) => (type != toLookup) ? Universe.LookupByTypeOrThrow(toLookup) : entity; foreach (var attr in type.GetMultiple()) entity.Add(Lookup(attr.Entity)); foreach (var attr in type.GetMultiple()) entity.Add(Lookup(attr.Relation), Lookup(attr.Target)); if (type.Get()?.AutoAdd == true) entity.Add(entity); if (type.Has()) entity.InitComponent(proxyType); else entity.CreateLookup(proxyType); if (type.Has()) entity.Add(); if (type.Has()) entity.Add(); } private void RegisterMethods(object? instance) { foreach (var method in Type.GetMethods( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance )) { if (method.Has()) Universe.InitSystem(instance, method).ChildOf(Entity); if (method.Has()) Universe.InitObserver(instance, method).ChildOf(Entity); } } } }