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.
- Original ILThe method's instructions
- TranspilerEdit the instruction sequence
- ReplacementCombine the edits with registered patches
- PrefixBefore the body
- Edited bodyRun the instructions produced above
- 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
- PrefixRead arguments, change them, or skip the original.
- OriginalRun the method body, including any instruction edits.
- PostfixRead or change the result after completion or a skip.
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;