Browse documentation

Execution flow

Adding a patch does not replace existing patches. Harmony combines them using its ordering rules and rebuilds the replacement whenever patches are added or removed.

Prefixes run before the original. Returning false skips the original and later prefixes that Harmony considers able to affect it. Postfixes run after completion or a skip, but not after an exception.

Transpilers edit the original IL in sequence while Harmony builds the replacement. They do not run on each call.

Use a finalizer for cleanup on success or failure, or to observe, replace, or suppress exceptions.

Two different moments · building a patch and running it
1. When Harmony builds or rebuilds the replacement
  1. Original ILThe method's instructions
  2. TranspilerEdit the instruction sequence
  3. ReplacementCombine the edits with registered patches
2. Each time the patched method is called
  1. PrefixBefore the body
  2. Edited bodyRun the instructions produced above
  3. PostfixAfter completion or a skip

The transpiler itself runs during generation. Code it inserts runs when execution reaches it. The second row shows the ordinary path; finalizers can also be installed to handle success and exceptions.

Anatomy of a patched method

The pseudocode below shows the replacement's structure.

Without finalizer patches

Harmony calls prefixes, the (possibly transpiled) original, then postfixes.

After a prefix returns false, Harmony still runs prefixes whose signatures it classifies as observation-only. See the prefix skip rules. Postfixes still run after a skip.

An exception stops this sequence and reaches the caller unless a finalizer handles it.


// while patching, the method ModifiedOriginal is created by chaining
// all transpilers. This happens only once when you patch, not during runtime
//
// var codes = GetCodeFromOriginal(originalMethod);
// codes = Transpiler1(codes);
// codes = Transpiler2(codes);
// codes = Transpiler3(codes);
// static ModifiedOriginal = GenerateDynamicMethod(codes);

static R ReplacementMethod(T optionalThisArgument, params object[] arguments)
{
    R result = default;
    var run = true;

    // Harmony separates all Prefix patches into those that change the
    // original methods result/execution and those who have no side efects
    // Lets call all prefixes with no side effect "SimplePrefix" and add
    // a number to them that indicates their sort order after applying
    // priorities to them:

    SimplePrefix1(arguments);
    if (run) run = Prefix2();
    SimplePrefix3(arguments);
    SimplePrefix4(arguments);
    if (run) Prefix5(ref someArgument, ref result);
    // ...

    if (run) result = ModifiedOriginal(arguments);

    Postfix1(ref result);
    result = Postfix2(result, arguments);
    Postfix3();
    // ...

    return result;
}

With finalizer patches

On each call · the ordinary execution path
  1. PrefixRead arguments, change them, or skip the original.
  2. OriginalRun the method body, including any instruction edits.
  3. PostfixRead or change the result after completion or a skip.
Finalizer · success or exceptionWhen installed, handles the outcome of the sequence above.

A prefix can skip the original; postfixes still run. An exception interrupts the sequence and skips remaining postfixes. A finalizer can observe, change, or suppress that exception.

Finalizers add try/catch handling around the prefixes, original, and postfixes. In this simplified pseudocode, Original stands for that whole sequence:

static R ReplacementMethod(T optionalThisArgument /*, ... arguments ... */ )
{
    R result = default;
    var finalized = false;
    Exception ex = null;

    // All this code is generated dynamically, which means that
    // Harmony can build it depending on
    //
    // - if there are any finalizers (otherwise, skip try-catch)
    //
    // - re-throwing can be dynamic too depending on if at least
    //   one finalizer returns a non-void result

    try
    {
        result = Original(/* ... arguments ... */);

        // finalizers get all the arguments a prefix could get too
        // plus one new one: "Exception __exception"
        // they SHOULD NOT edit the passed exception but instead
        // signal to Harmony that they change it by returning it

        // here finalizers are called without try-catch so they are
        // allowed to throw exceptions. note, that it is perfectly
        // fine to get null passed into the exception argument

        SimpleFinalizer(ref result);
        ex = EditFinalizer(ex, ref result);
        finalized = true;

        if (ex is not null) throw ex;
        return result;
    }
    catch (Exception e)
    {
        ex = e;

        // finalizers will get another chance here, so they are
        // guaranteed to run even if their first invocation threw
        // an exception

        if (!finalized)
        {
            try { SimpleFinalizer(ref result); } catch { }
            try { ex = EditFinalizer(ex, ref result); } catch { }
        }

        if (allVoid)
        {
            // alternative 1: all finalizers are returning void
            throw;
        }
        else
        {
            // alternative 2: at least one non-void finalizer
            if (ex is not null) throw ex;
        }

        return result;
    }
}

// given the following signatures:
public static R Original() => new("original");
public static void SimpleFinalizer(ref R result) { }
public static Exception EditFinalizer(Exception ex, ref R result) => ex;
Harmony 3 preview

For the stable release, read the 2.x documentation.