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