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
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); |
|
} |
|
} |
|
}
|
|
|