How do I use Harmony to repeatedly replace an arbitrary method?

Viewed 928

I need to replace a method with a call to a method with the same signature so that I can essentially replace the original method with a new method. Currently, I have the code below, which works, but when I try to patch the method again, it simply does nothing. I'm not sure if that's because Harmony doesn't like when I try to transpile it twice, or something else, either way it prevents me from repeatedly redirecting the original method.

// this is factored out of Transpiler() because yield return reasons
private static IEnumerable<CodeInstruction> TranspilerIterator(IEnumerable<CodeInstruction> instructions,
    MethodBase original) {
    var name = original.Name;
    var par = original.GetParameters();
    var method = newGuiType.GetMethod(name, (BindingFlags) FLAGS);
    Console.WriteLine($"{name} == null == {method == null}");

    if ((method.CallingConvention & CallingConventions.HasThis) != 0)
        yield return new CodeInstruction(OpCodes.Ldarg_0);

    for (var i = 0; i < par.Length; i++)
        yield return new CodeInstruction(OpCodes.Ldarg_S, par[i].Position + 1);

    yield return new CodeInstruction(OpCodes.Call, method);
    yield return new CodeInstruction(OpCodes.Ret);
}

which is called by this:

private void DoPatches() {
    Logger.Debug("Performing patches.");

    var methods = oldGuiType.GetMethods((BindingFlags) FLAGS);
    
    var t = this.GetType().GetMethod("Transpiler", BindingFlags.NonPublic | BindingFlags.Static);

    for (var i = 0; i < methods.Length; i++) {
        var name = methods[i].Name;
        Logger.Debug($"Transpiling {name}");
        harmony.Patch(methods[i], transpiler: new HarmonyMethod(t));
    }
}

I can't use a prefix because I need to know the signature to get the args in a prefix, and I don't know the signature.

I know there are other libraries to make essentially this, but the game I'm modding ships with Harmony so I don't have to ship a whole lib with my very small mod.

2 Answers

For every change to an original method (like adding or removing a transpiler) Harmony will recalculate the replacement method by doing this (in pseudo code):

original_IL -> transpiler1 -> transpiler2 -> ... -> transpiler_n -> replacement_IL

where transpiler 1..n are all active transpilers. It then build the replacement by (roughly) structuring it like this:

// Note: the following is very simplified and only used to illustrate
// the difference between prefix/postfix and a transpiler.

REPLACEMENT()
{
   if (Prefix_1() == false) return
   // ...
   if (Prefix_n() == false) return

   // replacement_IL_here

   Postfix_1()
   // ...
   Postfix_n()
}

Internally, Harmony has to therefore keep track of all patches and since those can be from different Assemblies it would need to serialize/deserialize state - which is in principle impossible if you have arbitrary state. Therefore, it only stores the simplest key for each patch possible: its MethodInfo which must be a static method.

As a result you cannot apply the same patch multiple times. It makes little sense since you could easily put all your code into one patch anyway.

For this application, ie updating a method to a newer method with an identical signature, Memory.DetourMethod(MethodBase original, MethodBase replacement) did what I needed.

// assuming newType is just a newer version of oldType
var newMethod = newType.getMethod("SomeMethod");
var oldMethod = oldType.getMethod("SomeMethod");
Memory.DetourMethod(oldMethod, newMethod);

This code effectively replaces the old method with the new method. The way it actually works is a bit more complicated.

Related