diff --git a/UXAssist/Common/ModFeatures/IModFeature.cs b/UXAssist/Common/ModFeatures/IModFeature.cs
index cc18519..c863744 100644
--- a/UXAssist/Common/ModFeatures/IModFeature.cs
+++ b/UXAssist/Common/ModFeatures/IModFeature.cs
@@ -1,10 +1,33 @@
namespace UXAssist.Common.ModFeatures;
+///
+/// Interface implemented by instance mod features registered with .
+/// All lifecycle methods are invoked by the registry in the order described below.
+///
public interface IModFeature
{
+ ///
+ /// Called once when the mod is initialized.
+ ///
void Init();
+
+ ///
+ /// Called once after initialization, when the mod should begin active behavior.
+ ///
void Start();
+
+ ///
+ /// Called once when the mod is being shut down or re-initialized.
+ ///
void Uninit();
+
+ ///
+ /// Called every frame for input handling. Should be lightweight.
+ ///
void OnInputUpdate();
+
+ ///
+ /// Called every frame for general updates. Should be lightweight.
+ ///
void OnUpdate();
}
diff --git a/UXAssist/Common/ModFeatures/ModFeatureAttribute.cs b/UXAssist/Common/ModFeatures/ModFeatureAttribute.cs
index d934859..9530612 100644
--- a/UXAssist/Common/ModFeatures/ModFeatureAttribute.cs
+++ b/UXAssist/Common/ModFeatures/ModFeatureAttribute.cs
@@ -2,12 +2,27 @@ using System;
namespace UXAssist.Common.ModFeatures;
+///
+/// Marks a static class as a mod feature so that can discover it.
+/// Lifecycle methods (Init, Start, Uninit, OnInputUpdate, OnUpdate) are optional.
+///
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ModFeatureAttribute : Attribute
{
+ ///
+ /// Optional display name of the feature.
+ ///
public string Name { get; }
+
+ ///
+ /// Execution order among discovered static features. Lower values run first.
+ ///
public int Order { get; set; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional display name of the feature.
public ModFeatureAttribute(string name = null)
{
Name = name;
diff --git a/UXAssist/Common/ModFeatures/ModFeatureRegistry.cs b/UXAssist/Common/ModFeatures/ModFeatureRegistry.cs
index a241069..bad9b39 100644
--- a/UXAssist/Common/ModFeatures/ModFeatureRegistry.cs
+++ b/UXAssist/Common/ModFeatures/ModFeatureRegistry.cs
@@ -5,12 +5,51 @@ using System.Reflection;
namespace UXAssist.Common.ModFeatures;
+///
+/// Registry that discovers static mod features from assemblies and holds registered instance mod features.
+/// Static lifecycle methods are optional; if a method is missing it is simply skipped.
+///
public static class ModFeatureRegistry
{
- private static readonly List _staticFeatures = [];
+ private sealed class StaticFeature
+ {
+ public Type Type { get; }
+ public Action Init { get; }
+ public Action Start { get; }
+ public Action Uninit { get; }
+ public Action OnInputUpdate { get; }
+ public Action OnUpdate { get; }
+
+ public StaticFeature(Type type)
+ {
+ Type = type;
+ Init = GetDelegate(type, "Init");
+ Start = GetDelegate(type, "Start");
+ Uninit = GetDelegate(type, "Uninit");
+ OnInputUpdate = GetDelegate(type, "OnInputUpdate");
+ OnUpdate = GetDelegate(type, "OnUpdate");
+ }
+
+ private static Action GetDelegate(Type type, string methodName)
+ {
+ var method = type.GetMethod(methodName,
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static,
+ null, Type.EmptyTypes, null);
+ if (method == null) return null;
+ return (Action)Delegate.CreateDelegate(typeof(Action), method);
+ }
+ }
+
+ private static readonly List _staticFeatures = [];
private static readonly List _instanceFeatures = [];
+ private static readonly HashSet _registeredInstanceTypes = [];
private static readonly HashSet _discoveredAssemblies = [];
+ ///
+ /// Discovers static mod feature classes marked with in the given assembly.
+ /// Each assembly is only discovered once.
+ ///
+ /// The assembly to scan.
public static void Discover(Assembly assembly)
{
if (!_discoveredAssemblies.Add(assembly)) return;
@@ -21,57 +60,76 @@ public static class ModFeatureRegistry
foreach (var type in staticTypes.OrderBy(GetOrder))
{
- if (!_staticFeatures.Contains(type))
- _staticFeatures.Add(type);
+ if (_staticFeatures.All(f => f.Type != type))
+ _staticFeatures.Add(new StaticFeature(type));
}
}
+ ///
+ /// Registers a new instance mod feature if an instance of the same type is not already registered.
+ ///
+ /// The mod feature type to register.
public static void Register() where T : class, IModFeature, new()
{
+ var type = typeof(T);
+ if (!_registeredInstanceTypes.Add(type)) return;
+
var instance = new T();
_instanceFeatures.Add(instance);
}
+ ///
+ /// Calls on all registered instance features
+ /// and invokes the cached static Init methods on all discovered static features.
+ ///
public static void InitAll()
{
- ForEachStatic("Init");
+ foreach (var f in _staticFeatures) f.Init?.Invoke();
foreach (var f in _instanceFeatures) f.Init();
}
+ ///
+ /// Calls on all registered instance features
+ /// and invokes the cached static Start methods on all discovered static features.
+ ///
public static void StartAll()
{
- ForEachStatic("Start");
+ foreach (var f in _staticFeatures) f.Start?.Invoke();
foreach (var f in _instanceFeatures) f.Start();
}
+ ///
+ /// Calls on all registered instance features
+ /// and invokes the cached static Uninit methods on all discovered static features.
+ ///
public static void UninitAll()
{
- ForEachStatic("Uninit");
+ foreach (var f in _staticFeatures) f.Uninit?.Invoke();
foreach (var f in _instanceFeatures) f.Uninit();
}
+ ///
+ /// Calls on all registered instance features
+ /// and invokes the cached static OnInputUpdate delegates on all discovered static features.
+ /// This method is meant to be called every frame; no reflection is performed here.
+ ///
public static void OnInputUpdateAll()
{
- ForEachStatic("OnInputUpdate");
+ foreach (var f in _staticFeatures) f.OnInputUpdate?.Invoke();
foreach (var f in _instanceFeatures) f.OnInputUpdate();
}
+ ///
+ /// Calls on all registered instance features
+ /// and invokes the cached static OnUpdate delegates on all discovered static features.
+ /// This method is meant to be called every frame; no reflection is performed here.
+ ///
public static void OnUpdateAll()
{
- ForEachStatic("OnUpdate");
+ foreach (var f in _staticFeatures) f.OnUpdate?.Invoke();
foreach (var f in _instanceFeatures) f.OnUpdate();
}
- private static void ForEachStatic(string methodName)
- {
- foreach (var type in _staticFeatures)
- {
- var method = type.GetMethod(methodName,
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
- method?.Invoke(null, null);
- }
- }
-
private static int GetOrder(Type type)
{
return type.GetCustomAttribute()?.Order ?? 0;
diff --git a/UXAssist/Common/Util.cs b/UXAssist/Common/Util.cs
index e186c9f..fa48176 100644
--- a/UXAssist/Common/Util.cs
+++ b/UXAssist/Common/Util.cs
@@ -8,16 +8,28 @@ namespace UXAssist.Common;
public static class Util
{
+ ///
+ /// Returns all types from the assembly matching the predicate, tolerating partially loadable assemblies.
+ ///
public static Type[] GetTypesFiltered(Assembly assembly, Func predicate)
{
- return [.. assembly.GetTypes().Where(predicate)];
+ try
+ {
+ return [.. assembly.GetTypes().Where(predicate)];
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ return [.. ex.Types.Where(t => t != null).Where(predicate)];
+ }
}
public static Type[] GetTypesInNamespace(Assembly assembly, string nameSpace) => GetTypesFiltered(assembly, t => string.Equals(t.Namespace, nameSpace, StringComparison.Ordinal));
public static Type[] GetTypesInNamespacePrefix(Assembly assembly, string prefix)
{
- return GetTypesFiltered(assembly, t => t.Namespace != null && t.Namespace.StartsWith(prefix, StringComparison.Ordinal));
+ return GetTypesFiltered(assembly, t =>
+ t.Namespace != null &&
+ (t.Namespace == prefix || t.Namespace.StartsWith(prefix + ".", StringComparison.Ordinal)));
}
public static byte[] LoadEmbeddedResource(string path, Assembly assembly = null)