Harmony Primer
Feel free to skip this part if you’re a developer that knows what Harmony is. I’m going to try to keep this short, but this topic could fill a small book. This quasi-tutorial on utilizing Harmony with Rust will make assumptions that you have a firm grasp of general programming, C#, .NET decompilers, and at least one IDE capable of building .NET 4.8 libraries (DLLs).
For many years the only way to add mods the Rust server was by DLL injection or patching the files directly. This is where Oxide / uMod filled that void. There is one drawback to patching files, and that’s those files change every release. Since Rust has a monthly update schedule (previously weekly!) this requires rebuilding Oxide every release. Hook offsets would change, things would break, and it wasn’t exactly a smooth process. Sometimes out of schedule patches get released to fix urgent bugs and exploits, and huge delays in Oxide updates would follow.
This is where Harmony comes in. Though not directly added to accommodate server owners, it can be easily leveraged to implement mods that require no additional external resources. Harmony mods, especially simple ones, tend to work fine through large and small patches, rarely requiring any changes or updates. I have a skip queue Harmony mod that I have never had to update in the roughly 2 years I’ve been using it.
At the time of this writing, Rust uses Harmony 1.x. The latest Harmony is 2.x, and while there is a method to manually upgrade Rust’s version of Harmony, I’ll leave that for a future post. For now we’re going to stick to Harmony 1.x. The most important part of this is understanding the Harmony documentation for that version lives only in the GitHub Wiki.
IDE Configuration
Before we begin, we’re going to need a suitable IDE environment setup in order to build our Harmony mods. Harmony mods are just DLLs that follow a particular convention. I’m going to keep this part generic, but I will include screenshots of my Rider session. If you know how to build .NET 4.8 DLLs, then you can probably skip this section.
Here is a sample project configuration in Rider. The two most important things to verify are Type is set to Class Library
and Framework is set to .Net Framework v4.8
. However your IDE is configured, or whatever toolchain you use, you’ll want to make sure these similar settings are applied.
Our next step is to get our dependencies in order. For us to actually do anything we need to import at least the Harmony DLL itself.
You’ll probably want to grab some more DLLs, like Assembly-CSharp.dll
to actually do anything.
Boilerplate
Most Harmony mods are going to have the same bit of boilerplate. You’re going to define a class that handles all the patch functions. The resulting class will use either specially defined method names or C# attributes to locate the class and method you want to patch.
Here is a very simple example of a patch’s most basic boilerplate code.
using Harmony;
namespace SampleHarmonyMod
{
[HarmonyPatch(typeof(ServerMgr), "Shutdown")]
public class MyPatch
{
}
}
The way you tell Harmony where patches are in your codebase is by applying the HarmonyPatch
attribute to a class. That denotes the class will contain specially named methods used in patching. Alternatively, if you don’t know what you want to patch and want to use reflection to search or apply more complicated logic, you can omit the attribute parameters and add a special method named TargetMethod
that returns a MethodBase
type. Here’s the above example using that instead.
using System.Reflection;
using Harmony;
namespace SampleHarmonyMod
{
[HarmonyPatch]
public class MyPatch
{
public static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(ServerMgr), "Shutdown");
}
}
}
Here we use a Harmony utility method called AccessTools.Method
to do the reflection lookup for us. You wouldn’t actually want to use TargetMethod
this exact way, since the attribute parameters will suffice. However you can apply any kind of logic you want here. You can search for methods that match the name, alert or log when it can’t find it, etc.
So far we’re about halfway to a functional patch that can do something.
Prefix and Postfix
Now it’s time to actually do something. We’re going to focus on Prefix and Postfix methods in this post, and I’ll cover Transpilers on my next post. Transpilers are significantly more complicated and require working knowledge of CIL (formerly MSIL/IL), the bytecode for .NET.
Postfix - Do something after a thing.
I’m going to go the opposite of many tutorials here and start with the Postfix
method since it’s a lot easier to work with. Defining this method in your class patch runs after your target, always. It has no ability to affect change to the target method, or modify its return value. It’s a simple way to just say, “when this method runs, do this immediately after.” Similar to a finally clause in try/catch.
So let’s create a Postfix for the above boilerplate example, that outputs some debug logging to say the server has shutdown. Super useful I know.
using Harmony;
using UnityEngine;
namespace SampleHarmonyMod
{
[HarmonyPatch(typeof(ServerMgr), "Shutdown")]
public class MyPatch
{
[HarmonyPostfix]
public static void Postfix()
{
Debug.Log("Server has shutdown!!");
}
}
}
You can actually name the method anything you want, but the important part is that you apply the [HarmonyPostfix]
attribute to it. There’s nothing else left to do. Compiling this, and putting the resulting DLL into your HarmonyMods
folder and restart the server! Next time the server shutdowns, you’ll see the above log message.
Prefix - Do something before a thing, maybe change a thing, or maybe don’t even do a thing.
While you can do a lot of stuff with a Postfix patch, Prefix will actually let you alter the flow of the code. You can decide if your target method runs at all, or modify the parameters being passed into it. You can even alter the return value.
Let’s look at a brand new example that does something a lot more interesting.
using Harmony;
namespace SampleHarmonyMod
{
[HarmonyPatch(typeof(JunkPile), "PlayersNearby")]
public class MyPatch
{
[HarmonyPrefix]
public static bool Prefix(ref bool __result)
{
__result = false;
return false;
}
}
}
Let’s unpack this example. Like the Postfix, you only need the attribute [HarmonyPrefix]
if you want to name your method something other than Prefix
. Here it’s redundant.
Prefix
returns a bool. If it returns true
, the original method PlayersNearby
will run. If we return false
as we have here, then only the code in the Prefix
method is run, and the original code is not. Since the original method expects a bool return value, we set it to via a special reference passed to the method named __result
. Make sure you denote this as a reference. Alternatively, if you don’t care about any of this you can define Prefix as a void and pass no arguments.
So what did we do here? Well normally junk piles won’t de-spawn unless there are no nearby players. Here we override that behavior by always saying there are no nearby players.
Harmony Mod Installation & Troubleshooting
So now we’ve created a few examples of Harmony mods. To install them, you place the DLL files into a special folder named HarmonyMods
at the root of your Rust server installation. It is important to note that once these DLL files are loaded, you cannot update them while the Rust server is running. You cannot unload them either.
The ideal way to update them is by wrapping the update process into a script, or other process that manages your RustDedicated.exe
so when you do a scheduled restart, the new DLLs are copied into the HarmonyMods
folder.
Help my mods don’t work!
The first step to troubleshooting is to see if your patch even loaded correctly. Every time Rust starts, a Harmony log file is created (and not appended) in the root of your Rust server installation named harmony_log.txt
. The contents of that file log the patching process. If a patch was loaded, and the method was found you will see resulting CIL code of what the patch changed. Here’s an example of my skip queue mod:
### Harmony id=com.facepunch.rust_dedicated.QueueSkipHarmony, version=1.2.0.1, location=D:\rustservers\release\RustDedicated_Data\Managed\0Harmony.dll
### Started from HarmonyLoader.LoadHarmonyMods(), location D:\rustservers\release\RustDedicated_Data\Managed\Rust.Harmony.dll
### At 2022-06-02 05.47.03
### Patch ConnectionQueue, Boolean CanJumpQueue(Network.Connection)
L_0000: Local var 0: ServerUsers+User
L_0000: Local var 1: System.Object
L_0000: Local var 2: System.Boolean
L_0000: ldc.i4 0
L_0005: stloc 2 (System.Boolean)
L_0006: ldarg 1
L_000c: ldloca 2 (System.Boolean)
L_000e: call Boolean Prefix(Network.Connection, Boolean ByRef)
L_0013: brfalse Label0
L_0018: ldstr "CanBypassQueue"
L_001d: ldarg.1
L_001e: call System.Object CallHook(System.String, System.Object)
If we look here, we can see the code was successfully patched by finding the call
to Prefix
. Above this is on label L_000e
. This output is the actual modified CIL. I’ll try to cover CIL in another post when I go over Transpilers.
The next step is to check for errors in the normal Rust log. Make sure you have a -logFile
setup for Rust so you can look for exceptions in your code.
Lastly, you can modify your code with timeless debug logging with Debug.Log
or even write directly to the Harmony logfile by using Harmony.FileLog.Log(string)
.
Limitations
It’s important we talk about the limitations of Harmony, and what Harmony is and isn’t. Harmony is not an Oxide / uMod replacement. Not out of the box anyway. You could use Harmony to make one, almost. Harmony cannot add fields to classes, extend enums, or modify fields directly. It can’t be used to change access levels on anything. It does not contain libraries for doing user permissions, config files, chat command handling, or other utilities. It’s not a hot-patching solution, you cannot load and unload Harmony DLLs out of the box (again, you could use it to add this functionality.)
Harmony’s documentation states it may struggle with generics, either patching generic classes, or methods utilizing generics. Any method that is inlined is also inaccessible to Harmony directly. Though you could patch every method that utilized that inline method to overcome it.
In short, Harmony is a great way to make mods for the Rust server that does not rely on a third party. You will have to work for that benefit though. Hopefully this post was helpful in understanding how to build and use Harmony mods. Stay tuned for part 2 where we talk about CIL and Transpilers.
References and Credits
Harmony GitHub Harmony 1.x Documentation Harmony 2.x Documentation Facepunch experimental ModLoader
Credit to 🐇hoppel for helping me get started with Harmony.