From 91259bbd17db0380ba714fb074d30f98a9c4b45e Mon Sep 17 00:00:00 2001 From: Soar Qin Date: Tue, 23 Jun 2026 02:23:20 +0800 Subject: [PATCH] refactor(UXAssist): harden ModFeatureRegistry --- UXAssist/Common/ModFeatures/IModFeature.cs | 23 +++++ .../Common/ModFeatures/ModFeatureAttribute.cs | 15 +++ .../Common/ModFeatures/ModFeatureRegistry.cs | 94 +++++++++++++++---- UXAssist/Common/Util.cs | 16 +++- 4 files changed, 128 insertions(+), 20 deletions(-) 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)