Skip to content

Hooking

In the C# world, hooking methods is easy thanks to libraries like MonoMod or Harmony.

For Risk of Rain 2, most of the modders in the community use MonoMod and the MMHOOK_X.dll files (To be more precise MMHOOK_RoR2.dll is the one that get generated by HookGenPatcher and that you will most likely see be used, even though a MMHOOK could be generated for other C# assemblies!)

Those MMHOOK files are generated through one of the MonoMod tool called MonoMod.RuntimeDetour.HookGen.

Having an assembly reference to one of these MMHOOK files will add the On and IL namespaces into your C# project for you to use.

There are two main type of hooks:

  • On Hooks means hooks which allow for adding code before and after the hooked method runs.

  • IL Hooks means hooks that modify the original method CIL

A hook is defined once and most of the time is:

  • Enabled inside your BaseUnityPlugin OnEnable method.
  • Disabled inside your BaseUnityPlugin OnDisable method.

On hooks will be executed every time the original method is called by the game.

This means that this hook is not bound to a specific instance of an object, but rather to all objects of that type.

On Hook

A very basic On hook goes as follows:

// Somewhere in your BaseUnityPlugin class
private void OnEnable()
{
    On.RoR2.RoR2Application.Awake += OnRoR2ApplicationAwake;
}

private void OnDisable()
{
    On.RoR2.RoR2Application.Awake -= OnRoR2ApplicationAwake;
}

...

private static void OnRoR2ApplicationAwake(On.RoR2.RoR2Application.orig_Awake orig, RoR2Application self)
{
    // code that will run before the original method

    orig(self);

    // code that will run after the original method
}

orig is the original function in the game, if you don't want to outright replace the function, you need to call the orig at the end of your function, or at the start if you want to run after it!

self allows you to access the instance member variables, if the method you are hooking is a member method !

If not, it means its a static method, and that there is no self parameter which represent the instance of the class.

If we run our own code before the original, we can perform logic prior to calling the original method, and even modify the parameters of the method before it is executed. Similarly, if we run our code after the original, the original will still perform its intended task, but will also execute our code right after. One problem that many people will find with this method, is that we are unable to modify the logic that exists within the method, without replacing it entirely.

Failing to call orig() will result in the original/vanilla method never being executed and other mods also hooking that methow will not be called!

For a beginner you basically never want to do this unless you have very good reasons. If what you want instead is modifiying some parts of the original method, you can do so using an IL Hook, a tutorial goes over them here

A little more advanced

Parameters can be intercepted prior to being passed to the games vanilla methods, for example, we could intercept the arguments passed to the PickupController, and instead of dropping lunar coins, we can now drop goat hoofs.

private static void PickupDropletController_CreatePickupDroplet(On.RoR2.PickupDropletController.orig_CreatePickupDroplet orig, PickupIndex pickupIndex, UnityEngine.Vector3 position, UnityEngine.Vector3 velocity)
{
    if (pickupIndex == PickupCatalog.FindPickupIndex("LunarCoin.Coin0"))
    {
        pickupIndex = PickupCatalog.FindPickupIndex(ItemIndex.Hoof);
    }

    orig(pickupIndex, position, velocity);
}

Replacing a large method in its entirety in order to modify a single line of logic would not be advised as it will create interoperability issues with other mods, for such scenario's something like an IL Hook is better.

IL Hook

IL Hook are more complicated, since you'll have to deal with the CIL.

A full guide can be found here

Hook Chaining

Assuming that multiple mods hook a method, load order follows LIFO (Last In First Out) priorities.

Vanilla -> Mod A -> Mod B -> Mod C

Becomes

C() -> B() -> A() -> Vanilla

If B does not orig(self), but A does, A will never execute, and neither will vanilla.

Back to top