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
- Insert code before/after a method is run.
- 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
- 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. - 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 callsorig(self)
(i.e. if first mod never callsorig(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. - 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 highlightedOnHCAwake
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
andMMHOOK_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:
- Modifying methods of other mods
- 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 beFunc<ModClass, string, int>
and for AStaticMethod it could beAction
), 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 usenameof
. -
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