Skip to content

Difference between Harmony and HarmonyX

Geoffrey Horsington edited this page Dec 21, 2021 · 7 revisions

While HarmonyX attempts to be mostly binary compatible with Harmony 2, there are some differences between how the two operate.
This document attempts to list all possible differences that are relevant to a developer coming from Harmony and wishing to use HarmonyX. Along with that, the document provides rationale for each of the changes.

Note that these are not bugs! All items listed here are intentional changes from original Harmony API!
If some difference is not listed here, it can be a bug, in which case please report it.

All prefix patchers are always run even if original method is skipped

Description

When applying multiple prefix patches on the same method, cancelling running a prefix will not cancel running the subsequent prefixes.

Example

Given the following pseudocode:

void Original(List<object> objects);

bool Prefix1(List<object> objects)
{
    Console.WriteLine("Patch1 with objects: " + objects);
    return false; // Skip running original
}

bool Prefix2(List<object> objects)
{
    Console.WriteLine("Patch2 with objects: " + objects);
    return true;
}

Applying this on Harmony and running Original prints

Patch1

However, on HarmonyX, the following is printed:

Patch1
Patch2

Rationale

Consider a profiling library that applies Harmony patches to time execution time of a method. In that case, the library would install a prefix to capture the time when method execution started and prefix to capture the time when method code is done. Now, if another prefix were to be patched in that wanted to skip the original method, it would not only skip the original method, but it would also break the profiling library because it never got the start time nor any indication that original method was skipped.

We have observed the same kind of issue while developing various BepInEx plugins and tools. Many developers assume that prefix and postfix behaviours are always symmetric: if a postfix is run, many assume the prefix has been run as well. While Harmony doesn't always skip execution (as long as a set of undocumented criteria is not true), they can lead to unpredictable results that are not properly documented. The issue gets complicated when developers don't even have a simple way to check if their prefix was skipped. Even with transpilers a proper workaround is not possible, as transpilers are applied on the original method only.

Workaround

HarmonyX provides a new __runOriginal parameter you can specify in prefixes/postfixes that allow you to check if the original method should be run (or was run).

For example:

void Original();

bool Prefix1()
{
    Console.WriteLine("Patch1");
    return false;
}

void Prefix2(bool __runOriginal)
{
    if (!__runOriginal)
    {
        Console.WriteLine("Some prefix wants to skip original, skipping this prefix!");
        return;
    }
    Console.WriteLine("Patch2");
}

Patching extern methods as managed

Description

Methods marked with extern can have prefixes, postfixes, transpilers and finalizers applied to them.

Example

[DllImport("my_native_library.dll")]
static extern void Original();

[HarmonyPatch(typeof(Foo), "Original"), HarmonyPrefix]
void Prefix()
{
    Console.WriteLine("Patch!");
}

On Harmony, applying the patch fails with method has no body exception.
On HarmonyX, applying the patch works and calling Original will print Patch!.

Rationale

With the help of MonoMod.RuntimeDetours it's possible to easily patch native methods as well. While Harmony does support patching native methods, it does only so via an empty transpiler while exposing a way to easily call the original method. Considering this lacking support and importance of patching native methods -- especially Mono internal calls when working with BepInEx -- we decided to properly implement full support for native method patching.

The only noticeable breaking change is that transpiler won't provide an empty instruction list. Instead, the HarmonyX provides a special wrapper method that the transpiler can run on.

Overhauled logging system

Description

HarmonyX provides an event-based logging system which separates messages into channels.

Example

To log to a file in Harmony, you'd have to do

Harmony.DEBUG = true;
FileLog.logPath = "<path to the log file>";

This will generate logs only for patching process.

To log to a file in HarmonyX, you do

// Specify which log messages you want to listen to
Logger.ChannelFilter = LogChannel.Info | LogChannel.Warn;
// Enable logging to file
HarmonyFileLog.Enabled = true;
// Optional: specify path to the log file to generate
HarmonyFileLog.FileWriterPath = "<path here>";

Rationale

One of the major issues we've had with Harmony when developing plugins is logging. Sometimes it's important for us to know which patches are being applied to a method without getting a massive dump of generated IL. Other times we need to debug an issue where a call to AccessTools returns null because a member got removed after a game update. Most importantly, however, is our need to be able to redirect logs to arbitrary locations -- not just a static file.

This change is breaking in a way that while FileLog and Harmony.DEBUG do exist, they do nothing reasonable in code. So, if logging is needed, you have to set it up the new way.

Alternatively, if you're using BepInEx, instead of setting up logging yourself, use the config options provided. BepInEx already integrates Harmony logging into itself thanks to the new system.

instance.UnpatchAll(string) is marked as obsolete and produces compile errors

Description

Calling instance.UnpatchAll() where instance is a Harmony instance produces a compilation error.

Example

var instance = new Harmony("my-instance");
instance.PatchAll(); // Other patch methods, etc

// Error: Use UnpatchSelf() to unpatch the current instance. The functionality to unpatch either other ids or EVERYTHING has been moved the static methods UnpatchID() and UnpatchAll() respectively
instance.UnpatchAll();

// Correct alternative
instance.UnpatchSelf();

Rationale

From our experience with modding and providing development support, the original UnpatchAll(string) is very often misinterpreted. The original behaviour of UnpatchAll(string) depends on the parameter:

  • If parameter is a string, the method unpatches any instance with the given ID
  • If parameter is null, the method unpatches all instances

The latter option along with UnpatchAll(string) being an instance method caused major misinterpretation by many mod developers: many assumed that calling instance.UnpatchAll() will unpatch only the instance. As such, many developers tend to ship plugins that accidentally unpatch all other patches as well.

HarmonyX already provides instance.UnpatchSelf() as a clear alternative that unpatches the current instance. However, this doesn't account for previous vanilla Harmony users trying to use HarmonyX. As such, it was deemed better to mark original UnpatchAll(string) as obsolete altogether. With this, plugin authors will be alerted of the issue and suggested a fix.

Workaround

Use one of the new methods depending on your needs:

  • instance.UnpatchSelf(): Use if you need to unpatch the current instance
  • Harmony.UnpatchID(string): Use if you need to unpatch a specific instance by ID
  • Harmony.UnpatchAll(): Use if you need to unpatch all Harmony instances currently loaded. This will unpatch even those instances that aren't owned by you! You are responsible for restoring the instances!