-
Notifications
You must be signed in to change notification settings - Fork 43
Difference between Harmony and HarmonyX
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.
When applying multiple prefix patches on the same method, cancelling running a prefix will not cancel running the subsequent prefixes.
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
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.
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");
}
Methods marked with extern
can have prefixes, postfixes, transpilers and finalizers applied to them.
[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!
.
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.
HarmonyX provides an event-based logging system which separates messages into channels.
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>";
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.
Calling instance.UnpatchAll()
where instance
is a Harmony
instance produces a compilation error.
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();
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.
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!
- Basic usage
-
HarmonyX extensions
1.1. Patching and unpatching
1.2. Prefixes are flowthrough
1.3. Targeting multiple methods with one patch
1.4. Patching enumerators
1.5. Transpiler helpers
1.6. ILManipulators
1.7. Extended patch targets
1.8. New patch attributes -
Extending HarmonyX
2.1. Custom patcher backends -
Misc
4.1. Patch parameters - Implementation differences from Harmony