Why MonoDetour
Harmony and MonoMod.RuntimeDetour are completely fine and you may not see any problems with them. But I, Hamunii, was never fully satisfied with either. Not all of that dissatisfaction is actually coming from my experience, but comes from seeing other people struggle with both Harmony and MonoMod.
Existing Solutions
Issues With Harmony
- Patch never runs/fails to be applied
- Missing HarmonyPatch attribute on the class
- Missing patch target type
- Missing patch method name
- Target method has overloads
- How to get/set parameters in patch method
- How to get/set return value in patch method
These also include preference, such as that I don’t like how much is “magic” — stuff you need to look up the documentation for to figure out, such as the special argument names.
What I Like About Harmony
- Convenient without any setup—just start patching
- Almost every hook can be performed the same familiar way
- Prefixes and Postfixes don’t wrap the whole target method
Originally I liked MonoMod Hooks more because they wrap the whole target method so there is no need for Prefixes and Postfixes and therefore there are no workarounds required for keeping state between them.
In my experience though, you actually almost never need to keep state between prefix and postfix, and you rarely need to do something both before and after the target method. Not to mention that you also need to remember to call the trampoline (commonly called orig
) or you accidentally skip the “original” method from running.
Issues With MonoMod
- Manual Hooking with
MonoMod.RuntimeDetour.Hook
is tedious - HookGen (v1) requires external hook helper assemblies to be present, which usually means something like (Auto)HookGenPatcher needs to be there to generate those files at runtime
- You may also want to hook something which hasn’t already been generated a hook helper assembly
- HookGen (v1) doesn’t generate hooks for properties (getters and setters)
Hook
s wrap the whole target method, requiring calling the trampolineorig
with its arguments
What I Like About MonoMod
Hook
s wrap the whole target method, removing the need for a lot of Harmony’s “magic”- Accessing and changing parameter values and overriding return value becomes obvious
- HookGen is still incredibly convenient despite what I listed earlier
ILHook
manipulators are great as they get passed anILContext
which holds a lot of information
MonoDetour Design
While I am the main target user of MonoDetour, I have designed the API so that it should as easy as possible for anyone to use. I like to help people, which is why I’m writing this documentation in the first place.
Taking everything I like from Harmony and MonoMod, we end up with MonoDetour.
Convenience Is Priority #1
If MonoDetour is not any more convenient than Harmony or MonoMod with HookGen in all cases, then it has failed. This ties in closely with being easy and intuitive to use. MonoDetour also borrows Prefix and Postfix patches from Harmony, which means less code to write.
Avoiding Common Pitfalls
MonoDetour solves most of the Harmony problems I listed with HookGen. This means that MonoDetour can generate hook helpers to easily hook methods and figure out what you can do in the hook, right in your IDE! Gone are the days of failing to hook a method due to a small mistake.
MonoDetour also differentiates methods with overloads right in HookGen just like in MonoMod so they are just as easy to hook as methods without overloads.
HookGen With Source Generators
MonoMod’s HookGen v1 works with Mono.Cecil and generates assemblies directly. These are external assemblies which are large as they contain all hooks for all types, except some hooks are missing, such as for properties.
MonoDetour only generates hooks for types you tell it about, and the generated hooks stay in your assembly for maximum portability.
Minimizing Damages
MonoDetour is strict by default about target methods for hooks. If the targe method signature for a hook changes, MonoDetour throws as soon as the target method is attempted to be retrieved via reflection if it isn’t found with the exact signature MonoDetour expects.
While this may appear as a downside compared to Harmony which is much less strict, your hook not breaking when a signature changes for a method you are hooking can result in missing new possible issues. Throwing will help catch changes you would otherwise have missed.
Additionally, Prefix and Postfix hooks which throw are caught by MonoDetour in an attempt to recover the target method from throwing and preventing as much damage as possible, and MonoDetour will immediately dispose of all hooks attached to the MonoDetourManager the hook is attached to. This is done to try prevent mods that are outdated or broken from an incompatibility from breaking a game for example.
Comparing Solutions
Let’s perform the same simple hook on the following method with MonoDetour, Harmony, MonoMod, and MonoMod with HookGen. The target type and method will be:
namespace Lib;
public class TargetClass{ // We want to hook this. public int TakeAndReturnInt(int number) { // Call this.PrintFoo() here // and increment 'number' argument by one. return number; }
public void PrintFoo() { Console.WriteLine("Foo"); }}
// Generate hooks for the target class.[MonoDetourTargets(typeof(TargetClass))]static class TargetClassHook{ // This attribute is not required, but will use this as hook init method. [MonoDetourHookInit] static void Init() { // These hooks are generated, so they are quick to write // and nearly impossible to get wrong! On.Lib.TargetClass.TakeAndReturnInt.Prefix(Prefix_TakeAndReturnInt); }
// This method signature can be generated by your IDE from the PrefixSignature // delegate argument in the On.Lib.TargetClass.TakeAndReturnInt.Prefix method! private static void Prefix_TakeAndReturnInt(TargetClass self, ref int number) { self.PrintFoo(); number += 1; }}
// Tell Harmony to use this targe type in patch methods inside this class.[HarmonyPatch(typeof(TargetClass))]static class TargetClassPatch{ // Specify target method name to patch. // Note: methods with overloads require specifying argument types to // identify the method to hook. [HarmonyPatch(nameof(TargetClass.TakeAndReturnInt))] // Mark patch as a Prefix. Alternatively the method name could be 'Prefix'. [HarmonyPrefix] // Use special argument names to get parameters. // Note: Your IDE can't help you with your patch signature here like with HookGen! static void Prefix_TakeAndReturnInt(TargetClass __instance, ref int number) { __instance.PrintFoo(); number += 1; }}
static class TargetClassHook{ // Store the hook instance. static Hook hookTakeAndReturnInt;
internal static void Init() { // Specify target method name to patch. // Note: methods with overloads require specifying argument types to // identify the method to hook. hookTakeAndReturnInt = new Hook( typeof(TargetClass).GetMethod(nameof(TargetClass.TakeAndReturnInt)), Hook_TakeAndReturnInt ); }
// Specify the original method signature as the first argument, and also // separately as individual parameters including the return type for your hook. // Note: Your IDE can't help you with your patch signature here like with HookGen! static int Hook_TakeAndReturnInt( Func<TargetClass, int, int> orig, TargetClass self, int number) { self.PrintFoo(); return orig(self, number + 1); }}
static class TargetClassHook{ internal static void Init() { // These hooks are generated, so they are quick to write // and nearly impossible to get wrong! On.Lib.TargetClass.TakeAndReturnInt += Hook_TakeAndReturnInt; }
// This method signature can be generated by your IDE from the hook_TakeAndReturnInt // delegate argument in the On.Lib.TargetClass.TakeAndReturnInt event! static int Hook_TakeAndReturnInt( On.Lib.TargetClass.orig_TakeAndReturnInt orig, TargetClass self, int number) { self.PrintFoo(); return orig(self, number + 1); }}
Conclusion
It is up to you to decide if MonoDetour makes sense for you, as it is about preference in the end. Nevertheless MonoDetour tries its best to be as convenient and easy as possible.
However if you are sold on MonoDetour, you can include MonoDetour into your existing projects that are using Harmony or MonoMod, as they are compatible with each other.
Though, note that if you are using HookGen with MonoMod you may need to configure MonoDetour’s Hook generation namespace to something other than On
to avoid collisions with generated hooks.