On Hooks

On Hooks are a type of hooks that are generated by MonoMod HookGen. This allows you to hook onto and replace any method in the games code. Any function (private or public) that the game's code has can be onHooked which makes the combination of Modhooks and OnHooks very useful while creating a mod. OnHooks allow you to do 2 main things

  1. Insert code before/after a method is run.
  2. Replace the games method with your code.

To subscribe to OnHook you'd first need to find a method. In this example we will be using HeroController.AddGeo(int amount) to demonstrate. So to OnHook HeroController.AddGeo we would do:

//We subscribe to the hook
On.HeroController.AddGeo += OnHCAddGeo;

   
private void OnHCAddGeo(On.HeroController.orig_AddGeo orig, HeroController self, int amount)
{
    //code before original method is called
    if (amount < 100)
    {
        //if amount of geo to be added is less than 100, return (and dont call original method)
        return;
    }
    
    orig(self, amount); // call original method
    
    //code after method is called
    Log($"the amount of total geo is {self.geoCounter.playerData.geo}"
}

From the above example, the function generated contains an odd looking argument On.HeroController.orig_AddGeo called orig. This is a delegate that will allow us to call the original function if we wish.
The second argument HeroController self is the object that the method is being called on. Since this method is not static, it is called on an instance of the class (in this case HeroController) which is then passed as an argument for us to use. Note that this argument will not be present for static methods.
The third argument int amount is the normal argument the function takes.

To call the original method we would do: orig(self, amount). We call orig and pass in self and the rest of the arguments of the function (in this case int amount). In the case the function has no other arguments we would just do orig(self). If the function had more arguments, we would pass those in too after self.

While using OnHooks we need to be careful of a few things

  1. If your goal is to insert code before/after a method has run, don't forget to call orig(self) or it might cause some unexpected behaviour especially for important functions like HeroController.Start and PlayerMakerFSM.Awake.
  2. Unless necessary for the mod, dont replace the method with your own because if orig(self) is not called, other mods that also OnHooked this method will not be able to run their code. This is because the second mod's onHook will only be called when the first mod's onHook calls orig(self) (i.e. if first mod never calls orig(self) second mod's hook wont be called). If the vanilla game code is conflicting with your mod then by all means do replace the method but just make sure to keep this in mind.
  3. If you want to replace a method, make sure to not call orig(self). Also if you do this it is very likely you will encounter private fields and methods that you would like to access. To be able to do this, Reflection is the best way to do it.

Note: Your IDE (Visual Studio Community/Jetbrains Rider) can generate this function for you with the correct parameters. To do this, see example video or type in On.HeroController.AddGeo += OnHCAddGeo;, Then right click on the now red highlighted OnHCAwake and click on the light bulb icon (called 'Quick actions and Refactoring') and choose 'Generate Method'.

Note: To be able to write OnHooks, you will need to import MMHOOK_Assembly-CSharp.dll and MMHOOK_PlayMaker.dll from your managed folder.

Order of Execution

If multiple mods On Hook the same method, it might be important to understand the order of importance. Lets say 3 mods exist, Mod A, B and C and they subscribe to the hook in that order, then the for the execution of the hooks are:

  • C does stuff before orig(self)
  • B does stuff before orig(self)
  • A does stuff before orig(self)
  • The original function runs
  • A does stuff after orig(self)
  • B does stuff after orig(self)
  • C does stuff after orig(self)

This order becomes especially imporant when one of the mods doesn't call orig(self). For example if B chooses not to call orig(self), then everything between "B does stuff before orig(self)" and "B does stuff after orig(self)" gets skipped (so C is fine but A is ignored).

Also, if the funtion has other parameters:

  • C's parameters when calling orig(self, ...) are seen by B
  • B's parameters when calling orig(self, ...) are seen by A
  • A's parameters when calling orig(self, ...) are seen by the original function i.e. A has final say in terms of what parameter to take effect

Creating custom On Hooks

MonoMod allows you to create OnHooks that are not present in the On namespace. There are 2 main purposes of doing this:

  1. Modifying methods of other mods
  2. To on hook a method that is patched by MAPI

1. Modifying methods of other mods

Before you start, you need to reference to MonoMod.RuntimeDetour and have a using MonoMod.RuntimeDetour;

Suppose the other mod defines the following class:

public class ModClass
{
    public void DoSomething() { /*...*/ }
    public int DoSomethingElse(string input) { /*...*/ }
    public static void AStaticMethod() { /*...*/ }
}

To hook DoSomething you do something like this:

// Defined as fields in your mod class
class YourClass
{
    private Hook _hook;
    private static MethodInfo _methodToHook = typeof(ModClass).GetMethod(nameof(ModClass.DoSomething));

    // In initialize
    _hook = new Hook(_methodToHook, MyOwnMethod);

    // In unload, if you want to unhook
    _hook?.Dispose();
    
    // The actual method
    private void MyOwnMethod(Action<ModClass> orig, ModClass self) { ... }
}
  • The delegate type of orig has to be something that the method you're hooking can be assigned to (so for DoSomethingElse it could be Func<ModClass, string, int> and for AStaticMethod it could be Action), though you can define your own delegate type if you want.

  • If the methods you're hooking are not public then it should be the same, except you need to add binding flags to GetMethod and you can't use nameof.

  • If the class you're hooking is a struct, then you'll need a ref parameter on the struct everywhere (so you'll need to define your own delegate type for orig).

  • If the class you're hooking is not accessible (particularly if it's a struct), then it might be more hassle than it's worth to use this style of hook. I recommend using an IL hook in this case.

2. To on hook a method that is patched by MAPI

If in dnspy while seeing the game's code you come across a method that you want to OnHook but you see in that method there is a call to a method with the same name but prefixed with orig_. For example in the PlayerData class the function AddHealth looks like this":

public void AddHealth(int amount)
{
    amount = ModHooks.BeforeAddHealth(amount);
    this.orig_AddHealth(amount);
}

If you see someything like this, it means the method has been patched by the modding API. This means that the original code (which we want to hook) is actually in orig_AddHealth. To do that, we need to make a custom On Hook which is very similar to the example above

// find the method - in this case we know it's a public, non-static method on PlayerData
private static MethodInfo origAddHealth = typeof(PlayerData).GetMethod("orig_AddHealth",
    BindingFlags.Public | BindingFlags.Instance);
// hold the actual hook
private Hook OnAddHealth;


//to hook
OnAddHealth = new Hook(origAddHealth, CustomAddHealth);

//to unhook
OnAddHealth?.Dispose();

//the actual method
private void CustomAddHealth(Action<PlayerData, int> orig, PlayerData self, int amount) { ... }

Possible TODOs

  • Show common hooks