Skip to content

Transpiler Practice: CodeMatcher

Manually iterating CodeInstruction is tedious. Harmony provides CodeMatcher, a fluent API for clearer IL matching and replacement.

Basic CodeMatcher Usage

csharp
public static IEnumerable<CodeInstruction> Transpiler(
    IEnumerable<CodeInstruction> instructions)   // instructions: the target method's full IL
{
    return new CodeMatcher(instructions)
        .MatchStartForward(                        // Search forward from the start for "push constant 1"
            new CodeInstruction(OpCodes.Ldc_I4_1)
        )
        .SetOpcodeAndAdvance(OpCodes.Ldc_I4_2)     // Replace it with "push constant 2"
        .InstructionEnumeration();                 // Return the modified full IL
}
  • MatchStartForward(...): match a sequence of IL from the current position forward
  • SetOpcodeAndAdvance(...): replace the current instruction's opcode
  • .InstructionEnumeration(): return the modified full IL

Matching a Sequence

You can match multiple consecutive instructions:

csharp
new CodeMatcher(instructions)
    .MatchStartForward(
        new CodeInstruction(OpCodes.Ldarg_0),
        new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Math), nameof(Math.Abs)))
    )
    ...

When matching method calls, use AccessTools.Method(typeof(Math), nameof(Math.Abs)) to specify exactly, avoiding matching same-name methods.

Practical Example: Replacing a Damage Calculation

Suppose the target method has result = rawDamage * damageMultiplier and you want rawDamage / 4:

csharp
[HarmonyPatch(typeof(Player), nameof(Player.CalculateDamage))]   // Patch target: damage calculation method
public static class CalculateDamage_Transpiler
{
    // Cache Math.Abs MethodInfo for matching call instructions in IL (avoid repeated reflection)
    private static readonly MethodInfo Abs =
        AccessTools.Method(typeof(Math), nameof(Math.Abs));

    public static IEnumerable<CodeInstruction> Transpiler(
        IEnumerable<CodeInstruction> instructions)
    {
        return new CodeMatcher(instructions)
            .MatchStartForward(                    // Find the IL segment for damage multiplier constants
                new CodeInstruction(OpCodes.Ldc_R4, 1f),
                new CodeInstruction(OpCodes.Ldc_R4, 2f)
            )
            .SetOperandAndAdvance(0.25f)   // Change to 0.25f
            .InstructionEnumeration();
    }
}

⚠️ In real code, adjust based on the actual IL structure — don't blindly copy numbers. Use dnSpy / ILSpy to inspect the target method's IL first.

Inserting New Instructions

Use Insert to add logic at the current position:

csharp
new CodeMatcher(instructions)
    .MatchStartForward(
        new CodeInstruction(OpCodes.Ret)   // Find return
    )
    .Insert(
        new CodeInstruction(OpCodes.Ldc_I4_0),   // Push 0
        new CodeInstruction(OpCodes.Ret)          // Return 0
    )
    .InstructionEnumeration();

Debugging Transpiler

When a Transpiler goes wrong, the game may crash or behave unexpectedly. Recommendations:

  1. First write a MonoBehaviour / test method that runs CodeMatcher on instructions and outputs each instruction
  2. Use dnSpy to view the target method's IL and confirm the matched instruction sequence actually exists
  3. If matching fails, CodeMatcher throws InvalidOperationException by default — use .ThrowOnInvalid or manual checks to avoid this
csharp
var matcher = new CodeMatcher(instructions);
if (!matcher.MatchStartForward(...).IsValid)
{
    MyMod.Logger.Log("Match failed, not modifying");
    return instructions;
}

What You Learned

  • CodeMatcher's fluent API: matching, replacing, inserting
  • Using AccessTools.Method for precise method call matching
  • Degradation and debugging when matching fails

Next Step

Learn to patch dynamically with code instead of attributes → Manual Patching

An organization that researches and expands the functions of ADOFAI