Skip to content

Transpiler Patch

BlazingTwist edited this page May 10, 2024 · 5 revisions

Transpiler Patches are useful whenever you want to change a Methods behaviour beyond the "early returns" Prefix and Midfix Patches offer.

Before we can dig into an example, we must learn about a major parameter for Transpiler Patches: 'pullNextLabelUp'.
It determines on which side of a branch Instructions are inserted.

To explain how it affects patching, I will be using this example:

Console.WriteLine("msg0");
if (condition) {
    Console.WriteLine("msg1");
} else {
    Console.WriteLine("msg2");
}
Console.WriteLine("msg3");
CodeInstruction representation

When you're replacing code, you typically don't want to enable pullUp.
In this example, we replace 'Console.WriteLine("msg2")' with 'Console.WriteLine("my_message")'

PullUp = 'false' PullUp = 'true'
CodeInstruction representation CodeInstruction representation

Result:

Console.WriteLine("msg0");
if (condition) {
    Console.WriteLine("msg1");
} else {
    Console.WriteLine("my_message");
}
Console.WriteLine("msg3");

Which is exactly what we wanted.

Result:

Console.WriteLine("msg0");
if (condition) {
    Console.WriteLine("msg1");
}
Console.WriteLine("my_message");
Console.WriteLine("msg3");

Which is very likely not what we wanted.


However when you're inserting new code, it isn't that clear-cut.
In this example, we insert 'Console.WriteLine("my_message")' between 'Console.WriteLine("msg2")' and 'Console.WriteLine("msg3")'

PullUp = 'false' PullUp = 'true'
CodeInstruction representation CodeInstruction representation

Result:

Console.WriteLine("msg0");
if (condition) {
    Console.WriteLine("msg1");
} else {
    Console.WriteLine("msg2");
    Console.WriteLine("my_message");
}
Console.WriteLine("msg3");

Which could be what we want.

Result:

Console.WriteLine("msg0");
if (condition) {
    Console.WriteLine("msg1");
} else {
    Console.WriteLine("msg2");
}
Console.WriteLine("my_message");
Console.WriteLine("msg3");

Which also could be what we want.


Example

Okay, now that we understand PullUp, let's have fun with an example.
Suppose the game you're modding always discards all ammo in the magazine when reloading.

public enum AmmoType {
    Sidearm,
    Primary,
    Special,
}

public class Player {
    private Dictionary<AmmoType, int> ammoReserves = new Dictionary<AmmoType, int> {
            { AmmoType.Sidearm, 150 },
            { AmmoType.Primary, 60 },
            { AmmoType.Special, 15 },
    };

    public int TakeAmmoFromReserves(AmmoType type, int desiredAmount) {
        int ammoRemaining = ammoReserves[type];
        int ammoTaken = Math.Min(desiredAmount, ammoRemaining);
        ammoReserves[type] = ammoRemaining - ammoTaken;
        return ammoTaken;
    }
}

public class Gun {
    public AmmoType AmmoType { get; }
    public int MagazineSize { get; }
    public int AmmoInMagazine { get; private set; }

    public void Reload(Player player) {
        /* important code we don't want to touch ... */
        
        AmmoInMagazine = player.TakeAmmoFromReserves(AmmoType, MagazineSize);
        
        /* ... important code we don't want to touch */
    }
}

But you want to add a Shotgun with a tubular magazine, so discarding the ammo doesn't make sense - oh no!
Transpiler:

[HarmonyPatch(declaringType: typeof(Gun))]
public class Gun_Patch {
    private static readonly ManualLogSource logger = Logger.CreateLogSource(nameof(Gun_Patch));

    [HarmonyPatch(methodName: nameof(Gun.Reload))]
    [HarmonyTranspiler]
    private static IEnumerable<CodeInstruction> Reload_Transpiler(IEnumerable<CodeInstruction> codeInstructions) {
        List<CodeInstruction> instructions = codeInstructions.ToList();
        MethodInfo takeAmmoFromReserves = AccessTools.DeclaredMethod(typeof(Player), nameof(Player.TakeAmmoFromReserves));
        MethodInfo getAmmoConsumption = SymbolExtensions.GetMethodInfo(() => GetAmmoConsumption(null));
        MethodInfo getAmmoCarryOver = SymbolExtensions.GetMethodInfo(() => GetAmmoCarryOver(null));

        CodeReplacementPatch patch = new CodeReplacementPatch(
                expectedMatches: 1,
                targetInstructions: new[] {
                        // The property getter for 'MagazineSize' is called as a method. 'this.get_MagazineSize()'
                        // In this example I'm matching for any method call, but you could also obtain the Property-Getter using AccessTools
                        InstructionMask.MatchOpCode(OpCodes.Ldarg_0),
                        InstructionMask.MatchOpCode(OpCodes.Call),

                        InstructionMask.MatchInstruction(OpCodes.Call, takeAmmoFromReserves),
                },
                insertInstructions: new[] {
                        new CodeInstruction(OpCodes.Ldarg_0),
                        new CodeInstruction(OpCodes.Call, getAmmoConsumption),
                        new CodeInstruction(OpCodes.Callvirt, takeAmmoFromReserves),
                        new CodeInstruction(OpCodes.Ldarg_0),
                        new CodeInstruction(OpCodes.Call, getAmmoCarryOver),
                        new CodeInstruction(OpCodes.Add),
                }
        );
        patch.ApplySafe(instructions, logger);

        return instructions;
    }

    private static int GetAmmoConsumption(Gun gun) {
        if (HasTubularMagazine(gun)) {
            return gun.MagazineSize - gun.AmmoInMagazine;
        }
        return gun.MagazineSize;
    }

    private static int GetAmmoCarryOver(Gun gun) {
        return HasTubularMagazine(gun) ? gun.AmmoInMagazine : 0;
    }

    private static bool HasTubularMagazine(Gun gun) {
        /* ... */
    }
}

As a result, 'Gun::Reload' will look like this:

public void Reload(Player player) {
    /* important code we don't want to touch ... */

    AmmoInMagazine = player.TakeAmmoFromReserves(AmmoType, Gun_Patch.GetAmmoConsumption(this)) + Gun_Patch.GetAmmoCarryOver(this);

    /* ... important code we don't want to touch */
}

See also:


Obsolete

You may have noticed that 'CodeReplacementPatch' has an obsolete Constructor.

This Constructor exists for backwards compatibility and should not be used, as it uses an inconsistent PullUp behaviour:
If a 'PostfixInstructionSequence' is provided, it will behave like 'PullUp = true'. But if no Postfix is specified, it behaves like 'PullUp = false'.