Skip to content

Patching a method

rootcan edited this page Apr 12, 2022 · 6 revisions

Objectives

  • Learn how to find and cancel a base game method
  • Learn the different patching possibilities

Pre-requesites

Getting Started

First of all you want to know what code you're willing to change. For that, you want to look into the game code thanks to any assembly editor like dnSpy.

  1. In my case, I found interesting to patch the OnTriggerEnter method from the SignalPing class.

My final goal will be to send an event to the server when this method happens.

  1. To do so, I'll create a new class in the NitroxPatcher project, under Patches/Dynamic, which I'll call SignalPing_OnTriggerEnter_Patch

NB 1: The naming convention is <Class>_<Method>_Patch
NB 2: Dynamic patches are applied once the player has loaded into the world. Persistent patches are applied on game launch. For most of the cases, you will want to make a dynamic patch.

  1. You need to make your class implement NitroxPatch and IDynamicPatch and add the override for the Patch method
using HarmonyLib;

namespace NitroxPatcher.Patches.Dynamic;

public class SignalPing_OnTriggerEnter_Patch : NitroxPatch, IDynamicPatch
{
    public override void Patch(Harmony harmony)
    {
        
    }
}
  1. Now, you need to tell the class to actually patch the method. To do so, first create a reference to the method itself from the base game.
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SignalPing t) => t.OnTriggerEnter(default));

NB 1: It should always be private (or internal), static and be named TARGET_METHOD (by convention)
NB 2: Import from System.Reflection and NitroxModel.Helper

  1. Then in the Patch method, depending on your goal, you want to choose the type of patch you want to apply. In my case, I just want to patch the prefix of this method.

You can find a list of them in the Harmony documentation For basic understanding this is what happens when you call a function foo()

... foo() 
{
    foo_Prefix();
    foo_actual();
    foo_Postfix();
}

Where foo_Prefix() is the prefix you (or someone else) may have set for this method and foo_Postfix() is the postfix that may have been set for this method. foo_actual() is evidently the method itself.


  1. (bis) Thus, I'll add the following line inside the Patch() method
PatchPrefix(harmony, TARGET_METHOD);

By default, NitroxPatcher (the class charged of loading and executing every patch) will look for a static method named Prefix in the current class, therefore, we need to create it.


  1. We create the Prefix() method just like this
public static bool Prefix(SignalPing __instance, Collider collider)
{
    return true;
}

Please look at the list of injections you can take from the actual method in the Harmony documentation.
In my case, I'll be using the __instance one which refers to the instance of the SignalPing class in which this method was called. I'll also use the base method's parameter Collider other (which is optional) because I need it

  1. As I want to be able to cancel the method's execution (it depends on the situation), I need to make Prefix() return a bool. If I return true, it means the actual method will happen, but if I return false, the actual method won't happen.

This is what the actual method looks like btw

public void OnTriggerEnter(Collider other)
{
    if (this.disableOnEnter && other.gameObject.Equals(Player.main.gameObject))
    {
        this.pingInstance.SetVisible(false);
    }
}

My goal is to add some code inside the if condition to make it look like this

public void OnTriggerEnter(Collider other)
{
    if (this.disableOnEnter && other.gameObject.Equals(Player.main.gameObject))
    {
        // In the case the ping instance was still visible, we want to acknowledge it's "removal"
        if (__instance.pingInstance.visible)
        {
            // DO SOMETHING
        }
        this.pingInstance.SetVisible(false);
    }
}

Usually, we would use a transpiler for that, which lets you insert code at a precise place in the method but it also is usually a pain to make so I'll go with the prefix to keep readability and because this method is kinda small :)

  1. In this particular case, because I need the exact same conditions as in the original method, I'll just return false at the end of the executed code (inside the if statement) so that it doesn't execute twice. But for now, I'll just let the method like this because it will be useful in another guide.

This is what the final class looks like

using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
using UnityEngine;

namespace NitroxPatcher.Patches.Dynamic;

public class SignalPing_OnTriggerEnter_Patch : NitroxPatch, IDynamicPatch
{
    private static readonly MethodInfo TARGET_METHOD = Reflect.Method((SignalPing t) => t.OnTriggerEnter(default));

    public static bool Prefix(SignalPing __instance, Collider other)
    {
        if (__instance.disableOnEnter && other.gameObject.Equals(Player.main.gameObject))
        {
            // In the case the ping instance was still visible, we want to acknowledge it's "removal"
            if (__instance.pingInstance.visible)
            {
                Log.Debug("A Signal Ping was removed, now do something");
            }
            __instance.pingInstance.SetVisible(false);
            return false;
        }
        return false;
    }

    public override void Patch(Harmony harmony)
    {
        PatchPrefix(harmony, TARGET_METHOD);
    }
}
Clone this wiki locally