You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

216 lines
7.4 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using gaemstone.Utility;
using static gaemstone.Flecs.Core;
namespace gaemstone.ECS;
public class ModuleManager
{
private readonly Dictionary<Entity, ModuleInfo> _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<Type> types)
{
foreach (var type in types)
if (type.Has<ModuleAttribute>())
Register(type);
}
public EntityRef Register<T>()
where T : class => Register(typeof(T));
public EntityRef Register(Type type)
{
if (!type.IsClass || type.IsGenericType || type.IsGenericTypeDefinition) throw new Exception(
$"Module {type} must be a non-generic class");
if (type.Get<ModuleAttribute>() is not ModuleAttribute attr) throw new Exception(
$"Module {type} must be marked with ModuleAttribute");
// Check if module type is static.
if (type.IsAbstract && type.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 (attr.Path == null) throw new Exception(
$"Existing module {type} must have ModuleAttribute.Name set");
var path = new EntityPath(true, attr.Path);
var entity = Universe.Lookup(path) ?? throw new Exception(
$"Existing module {type} with name '{path}' 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 nested in type.GetNestedTypes()) {
if (!nested.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
var name = nested.Get<EntityAttribute>()?.Path?.Single() ?? nested.Name;
Universe.LookupOrThrow(entity, name).CreateLookup(nested);
}
return entity;
} else {
var path = GetModulePath(type);
var module = new ModuleInfo(Universe, type, 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<ModuleAttribute>();
if (attr == null) throw new ArgumentException(
$"Module {type} must be marked with ModuleAttribute", nameof(type));
// If module is static, its path will be implictly global.
var global = (type.IsAbstract && type.IsSealed) || attr.Global;
// If global or path are specified in the attribute, return an absolute path.
if (global || attr.Path != null)
return new(true, attr.Path ?? new[] { type.Name });
// 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('.');
return new(true, parts.Prepend(assemblyName).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<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> 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 or sealed");
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 dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr =>
attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
var dependsPath = ModuleManager.GetModulePath(dependsAttr.Target);
var dependency = Universe.Lookup(dependsPath) ??
Universe.New(dependsPath).Add<Module>().Disable().Build();
var depModule = Universe.Modules.Lookup(dependency);
if (depModule?.IsActive == true) MetDependencies.Add(depModule);
else { UnmetDependencies.Add(dependency); module.Disable(); }
module.Add<DependsOn>(dependency);
}
Entity = module.Build().CreateLookup(Type);
}
public void Enable()
{
Entity.Enable();
Instance = Activator.CreateInstance(Type)!;
RegisterNestedTypes();
(Instance as IModuleInitializer)?.Initialize(Entity);
RegisterMethods(Instance);
}
private void RegisterNestedTypes()
{
foreach (var type in Type.GetNestedTypes()) {
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
// If proxied type is specified, use it instead of the marked type.
// Attributes are still read from the original type.
var proxyType = type.Get<ProxyAttribute>()?.Type ?? type;
if (!type.Has<ComponentAttribute>() && (!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 = (type.Get<EntityAttribute>() is EntityAttribute entityAttr)
? new EntityPath(entityAttr.Global, entityAttr.Path ?? new[] { proxyType.Name })
: new EntityPath(false, proxyType.Name);
var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path);
if (!type.Has<PrivateAttribute>()) builder.Symbol(path.Name);
var entity = builder.Build();
EntityRef Lookup(Type toLookup)
=> (type != toLookup) ? Universe.LookupOrThrow(toLookup) : entity;
foreach (var attr in type.GetMultiple<AddEntityAttribute>())
entity.Add(Lookup(attr.Entity));
foreach (var attr in type.GetMultiple<AddRelationAttribute>())
entity.Add(Lookup(attr.Relation), Lookup(attr.Target));
if (type.Get<SingletonAttribute>()?.AutoAdd == true) entity.Add(entity);
if (type.Has<ComponentAttribute>()) entity.CreateComponent(proxyType);
else entity.CreateLookup(proxyType);
}
}
private void RegisterMethods(object? instance)
{
foreach (var method in Type.GetMethods()) {
if (method.Has<SystemAttribute>())
Universe.RegisterSystem(instance, method).ChildOf(Entity);
if (method.Has<ObserverAttribute>())
Universe.RegisterObserver(instance, method).ChildOf(Entity);
}
}
}