From 98d94b3dcaba890af27f15e13a5dedb50f476ad0 Mon Sep 17 00:00:00 2001 From: Linniel Driver-Williams Date: Wed, 29 Apr 2026 16:39:22 +0200 Subject: [PATCH] implemented spiremethod --- Patches/PostModInitPatch.cs | 3 +- SpireMethod/SpireMethod.cs | 98 +++++++++++++++++++++++ SpireMethod/SpireMethodHandlers.cs | 39 +++++++++ SpireMethod/SpireMethodRegistry.cs | 122 +++++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 SpireMethod/SpireMethod.cs create mode 100644 SpireMethod/SpireMethodHandlers.cs create mode 100644 SpireMethod/SpireMethodRegistry.cs diff --git a/Patches/PostModInitPatch.cs b/Patches/PostModInitPatch.cs index 65c48ddd..795155cc 100644 --- a/Patches/PostModInitPatch.cs +++ b/Patches/PostModInitPatch.cs @@ -93,7 +93,8 @@ private static void CheckSpecialSpireField(FieldInfo field) var genericTypeDef = fType.GetGenericTypeDefinition(); if (genericTypeDef != typeof(SavedSpireField<,>) && - genericTypeDef != typeof(AddedNode<,>)) + genericTypeDef != typeof(AddedNode<,>) && + genericTypeDef != typeof(SpireMethod.SpireMethod<>)) return; field.GetValue(null); //Trigger field initialization diff --git a/SpireMethod/SpireMethod.cs b/SpireMethod/SpireMethod.cs new file mode 100644 index 00000000..d5dcd503 --- /dev/null +++ b/SpireMethod/SpireMethod.cs @@ -0,0 +1,98 @@ +using System.Reflection; +using HarmonyLib; + +namespace BaseLib.SpireMethod; + +/// +/// Attaches new behavior to a virtual method on an existing type without requiring +/// every mod author to write their own Harmony patch with runtime type checks. +/// +/// Register handlers via the static Register methods: +/// +/// // Async +/// static SpireMethod<MyRelic> hook = SpireMethod<MyRelic>.Register( +/// nameof(AbstractModel.AfterCardPlayed), +/// async (MyRelic instance, object[] args) => { /* ... */ } +/// ); +/// +/// // Value-returning +/// static SpireMethod<MyRelic> hook = SpireMethod<MyRelic>.Register( +/// nameof(AbstractModel.ModifyDamageAdditive), +/// (MyRelic instance, decimal current, object[] args) => current + 5m +/// ); +/// +/// // Void +/// static SpireMethod<MyRelic> hook = SpireMethod<MyRelic>.Register( +/// nameof(AbstractModel.SomeMethod), +/// (MyRelic instance, object[] args) => { /* ... */ } +/// ); +/// +/// +/// Handlers are invoked after the base implementation, in registration order. +/// +/// must not declare its own override of the target +/// method. If it does, patch the override directly with Harmony instead. +/// +/// The concrete type whose instances should receive the handler. +public sealed class SpireMethod where T : class +{ + private SpireMethod() + { + } + + /// Register a handler for an async (Task-returning) virtual method. + public static SpireMethod Register(string methodName, AsyncSpireMethodHandler handler) + { + var declaring = ResolveAndValidate(methodName); + SpireMethodRegistry.Register(declaring, new AsyncHandler(handler)); + return new SpireMethod(); + } + + /// Register a handler for a void virtual method. + public static SpireMethod Register(string methodName, VoidSpireMethodHandler handler) + { + var declaring = ResolveAndValidate(methodName); + SpireMethodRegistry.Register(declaring, new VoidHandler(handler)); + return new SpireMethod(); + } + + /// + /// Register a handler for a value-returning virtual method. + /// Each handler receives the current return value and may return a modified one. + /// + public static SpireMethod Register(string methodName, ValueSpireMethodHandler handler) + { + var declaring = ResolveAndValidate(methodName); + SpireMethodRegistry.Register(declaring, new ValueHandler(handler)); + return new SpireMethod(); + } + + private static MethodInfo ResolveAndValidate(string methodName) + { + var resolved = AccessTools.Method(typeof(T), methodName) + ?? throw new ArgumentException( + $"SpireMethod<{typeof(T).Name}>: method '{methodName}' not found on " + + $"'{typeof(T).FullName}' or any base class."); + + var declaredOnT = AccessTools.DeclaredMethod(typeof(T), methodName); + if (declaredOnT != null) + throw new InvalidOperationException( + $"SpireMethod<{typeof(T).Name}>: '{typeof(T).Name}' already declares an override " + + $"of '{methodName}'. Use a direct Harmony patch instead."); + + return resolved.GetBaseDefinition(); + } +} + +/// Handler delegate for an async (Task-returning) virtual method. +public delegate Task AsyncSpireMethodHandler(T instance, object[] args) where T : class; + +/// Handler delegate for a void virtual method. +public delegate void VoidSpireMethodHandler(T instance, object[] args) where T : class; + +/// +/// Handler delegate for a value-returning virtual method. +/// Receives the current return value and the original method arguments; +/// returns a value that replaces the method's return value. +/// +public delegate TReturn ValueSpireMethodHandler(T instance, TReturn current, object[] args) where T : class; \ No newline at end of file diff --git a/SpireMethod/SpireMethodHandlers.cs b/SpireMethod/SpireMethodHandlers.cs new file mode 100644 index 00000000..bcf2139b --- /dev/null +++ b/SpireMethod/SpireMethodHandlers.cs @@ -0,0 +1,39 @@ +namespace BaseLib.SpireMethod; + +internal abstract class SpireMethodHandlerBase +{ + public abstract Type TargetType { get; } + + public virtual void InvokeVoid(object instance, object[] args) => + throw new InvalidOperationException("This handler does not support void invocation."); + + public virtual Task InvokeAsync(object instance, object[] args) => + throw new InvalidOperationException("This handler does not support async invocation."); + + public virtual object? InvokeValue(object instance, object? current, object[] args) => + throw new InvalidOperationException("This handler does not support value invocation."); +} + +internal sealed class AsyncHandler(AsyncSpireMethodHandler handler) : SpireMethodHandlerBase where T : class +{ + public override Type TargetType => typeof(T); + + public override Task InvokeAsync(object instance, object[] args) => + handler((T)instance, args); +} + +internal sealed class VoidHandler(VoidSpireMethodHandler handler) : SpireMethodHandlerBase where T : class +{ + public override Type TargetType => typeof(T); + + public override void InvokeVoid(object instance, object[] args) => + handler((T)instance, args); +} + +internal sealed class ValueHandler(ValueSpireMethodHandler handler) : SpireMethodHandlerBase where T : class +{ + public override Type TargetType => typeof(T); + + public override object? InvokeValue(object instance, object? current, object[] args) => + handler((T)instance, (TReturn)current!, args); +} diff --git a/SpireMethod/SpireMethodRegistry.cs b/SpireMethod/SpireMethodRegistry.cs new file mode 100644 index 00000000..2df414c1 --- /dev/null +++ b/SpireMethod/SpireMethodRegistry.cs @@ -0,0 +1,122 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using HarmonyLib; + +namespace BaseLib.SpireMethod; + +/// +/// Central registry for SpireMethod handlers. +/// +/// Holds a Dictionary<MethodInfo, List<SpireMethodHandlerBase>> keyed by the +/// declaring base-class method, applies lazy Harmony postfixes, and dispatches at runtime +/// by matching __instance.GetType() against each handler's +/// . +/// +/// The postfix dispatchers receive the patched MethodBase via Harmony's +/// __originalMethod injection, so a single static postfix method handles all +/// registrations for a given return-type category. +/// +internal static class SpireMethodRegistry +{ + private static readonly Dictionary> _handlers = []; + + // Tracks which methods have already had a Harmony postfix applied. I did have concerns on startup impact, but adapt to normal patching if lazy patching like this is not preferable. + private static readonly HashSet _patched = []; + + internal static void Register(MethodInfo declaringMethod, SpireMethodHandlerBase handler) + { + ref var handlers = ref CollectionsMarshal.GetValueRefOrAddDefault(_handlers, declaringMethod, out _); + handlers ??= []; + handlers.Add(handler); + + // Apply the Harmony postfix lazily (once per method). + LazyPatch(declaringMethod); + } + + private static void LazyPatch(MethodInfo method) + { + if (!_patched.Add(method)) return; + + var returnType = method.ReturnType; + + var postfix = new HarmonyMethod(returnType switch + { + _ when returnType == typeof(Task) => AccessTools.Method(typeof(AsyncPostfixDispatcher), + nameof(AsyncPostfixDispatcher.Postfix)), + _ when returnType == typeof(void) => AccessTools.Method(typeof(VoidPostfixDispatcher), + nameof(VoidPostfixDispatcher.Postfix)), + _ => AccessTools.Method(typeof(ValuePostfixDispatcher<>).MakeGenericType(returnType), + nameof(ValuePostfixDispatcher.Postfix)) + }); + + BaseLibMain.MainHarmony.Patch(method, postfix: postfix); + BaseLibMain.Logger.Info( + $"SpireMethod: patched {method.DeclaringType?.Name}.{method.Name} (return: {returnType.Name})"); + } + + private static List? GetHandlers(MethodBase originalMethod, Type instanceType) + { + var key = ((MethodInfo)originalMethod).GetBaseDefinition(); + if (!_handlers.TryGetValue(key, out var all)) return null; + + // Filter to handlers whose TargetType is assignable from the instance's runtime type + // (so handlers for RunicPyramid only fire on RunicPyramid instances). + List? result = null; + foreach (var handler in all) + { + if (!handler.TargetType.IsAssignableFrom(instanceType)) continue; + result ??= []; + result.Add(handler); + } + + return result; + } + + private static class AsyncPostfixDispatcher + { + public static void Postfix(object __instance, ref Task __result, object[] __args, MethodBase __originalMethod) + { + var handlers = GetHandlers(__originalMethod, __instance.GetType()); + if (handlers == null) return; + + __result = ChainAsync(handlers, __instance, __args); + } + + private static async Task ChainAsync(List handlers, object instance, object[] args) + { + foreach (var handler in handlers) + { + await handler.InvokeAsync(instance, args); + } + } + } + + private static class VoidPostfixDispatcher + { + public static void Postfix(object __instance, object[] __args, MethodBase __originalMethod) + { + var handlers = GetHandlers(__originalMethod, __instance.GetType()); + if (handlers == null) return; + + foreach (var handler in handlers) + { + handler.InvokeVoid(__instance, __args); + } + } + } + + private static class ValuePostfixDispatcher + { + public static void Postfix(object __instance, ref TReturn __result, object[] __args, + MethodBase __originalMethod) + { + var handlers = GetHandlers(__originalMethod, __instance.GetType()); + if (handlers == null) return; + + foreach (var handler in handlers) + { + __result = (TReturn)handler.InvokeValue(__instance, __result, __args)!; + } + } + } +} \ No newline at end of file