Preface

Warning: This is going to be a long one. It’s a complicated topic that I’m only planning on scratching the surface of. Just enough to get you started.

If you haven’t already, check out part 1 of my Harmony and Rust “tutorial.” It covers the basics of Harmony mods in Rust. This part will cover more complicated topics, mostly Transpilers. In order to effectively create a patch utilizing transpilers you’ll need to be familiar with dnSpy or ILSpy or another .NET decompiler of your choice that will preferrably output C# and CIL next to each other.

Then you’ll need to be familiar with CIL (formerly IL or MSIL), or another bytecode / assembly language. These languages are all stack based. The most important thing to remember is the stack is sacred.

What are Transpilers

Okay so lets talk about what transpilers are. It’s my opinion that Harmony uses this term a little loosely. In the wider compsci world transpiling means taking something written in one language, and “translating / compiling” into another language. This is incredibly popular in JavaScript for example where base JavaScript is a real ugly language to work with, so people have created things like CoffeeScript, TypeScript, etc to make it easier to work with. Those languages get transpiled into JavaScript. For Harmony, Transpile is just a method that gets passed a list of instructions for the method being patched, and then accepting the return result as the new instructions for that method.

Why use Transpiler over Prefix?

One of the biggest reasons to use a Transpiler over a Prefix is the simple use case of wanting to modify something small in the middle of a method. Prefixes don’t offer a way to do this, they only hook at the top of the method. You could of course re-create the original method in your Prefix with the small modification you want. Then it becomes less update proof. It’s a lot more work as well when perhaps changing a single line of code would be all that is required.

A transpiler can target the specific “line” of code you want to modify, and modify it. With anything. Change a value, call a method, create a new loop with complex logic in it, anything you want.

The most basic Transpiler

Let’s create an incredibly basic Transpiler that does nothing. Much like Prefix you create a method named Transpiler or give a method the [HarmonyTranspiler] attribute in your patch class. The method takes takes one argument, an IEnumerable<CodeInstruction> and returns the same type.

static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
    return instructions;
}

And there you have it. Of course it doesn’t do anything, kind of.. but let’s analyze the one small thing it is doing.

The instructions parameter is essentially a list of Harmony CodeInstruction objects that define the method you are patching. The documentation of this class for 1.x is non-existent, but I’ve found the 2.x documentation works fine. The two most important fields you want to pay attention to are opcode and operand. They contain the CIL information for those things respectively. Harmony then takes the return of this method, and replaces the original method, in it’s entirety. This means if you return an empty List<CodeInstruction> for example, you effectively null out the entire function.

Unlike a Prefix, you don’t run things from the Transpiler during execution of the original method. You can’t use it like a “hook” or run your own code, and then bail on the original. A transpiler is what gives you the opportunity to modify the IEnumerable<CodeInstruction> list that defines the method in any way you want. This has advantages that you can do expensive things here to patch the method, since the patch is applied only once.

Figuring out where and what to patch

Transpilers take a lot more research to implement. My first step is to crack open ILSpy and poke around the method I want to patch. Typically this will be in a phase of determining do I use a Prefix, Postfix, or Transpiler to accomplish my goals.

Let’s take a look at SAM Site, and the code involved in firing the projectiles. I’ve always wanted to adjust the amount of missiles that can fire in a single burst, but there exists no way in Oxide to easily do this. Let’s take a look at why, and how we can use a Transpiler to do it.

samweapontick_code We can see where Rust defines the amount of missiles to fire in a burst before applying a cooldown between them. It’s a hard coded value of 6. The WeaponTick function is fairly large, and a Prefix wouldn’t work well, or cleanly. We could maybe use reflection with a Prefix to juke the firedCount variable, but lets use a transpiler instead. If you don’t know anything about CIL, buckle up this is going to be a fun one.

Step 1, Find the CIL responsible.

We have the decompiled C# code, but we can’t work with that directly. So in your decompiler of choice, hopefully there is a function to view C# and CIL (often called IL) together. In ILSpy it’s a little dropdown at the top.

ILSpy and now… WeaponTickCIL

Things just suddenly got a lot more complicated. Don’t panic, there’s a lot to unpack here but it’s really not that difficult. The first thing you’ll notice is that the amount of CIL required for even simple C# code can be significant. We’re going to have to scroll down to find our area of interest.

weapontickil Here’s the relevant code in our C#/IL view. ILSpy does its best to comment the C# code around the relevant areas of CIL code, but it doesn’t always translate well to the same flow or pattern. That’s because in CIL loops and other things do not exist as first class constructs. Loops are created by jumps to labels, and various opcodes that define when and how to jump. Often if statements are inverted, like in our above example:

IL_0025: ldarg.0  
IL_0026: callvirt instance bool IOEntity::IsPowered()  
IL_002b: brtrue.s IL_0035

In our C# code, we’re saying if (!IsPowered()) then return. In CIL, we check if IsPowered() is true, and if it is we jump over the code that resets firedCount and ret (return). You’ll see this very often everywhere so it’s important to pay attention and follow how the CIL flows and not the C# flow.

Step 2, Understanding the code we want to modify.

The next bit of code we jump to if the SAM Site is powered is at label IL_0035. This begins our small block of code we want to modify. Let’s dissect each line so we know what it is doing exactly before we begin thinking about changing it. If you’re using ILSpy you can click on the opcode ldarg.0 to see the Microsoft docs on it.

IL_0035: ldarg.0

This essentially loads argument 0 onto the stack. Since this is running from an instance method, this is pointing to the instance of the object for the class SamSite.

IL_0036: ldfld int32 SamSite::firedCount

This loads a field from the object stored on the stack, in this case our firedCount field.

IL_003b: ldc.i4.6

This loads a literal 6 onto the stack. There are a bunch of quick opcodes for literals, you’ll see them used in a lot of places.

IL_003c: blt.s IL_005f

Here’s the jump (aka branch) that uses the information evaluated above to determine where to go next. You can always spot the branches by opcodes that start with a b. This is where stack based languages get fun. If you’ve followed so far, we have two values pushed to the stack. First we pushed the value of firedCount onto it. Then we pushed a 6 onto it. blt.s then pops the last two values off the stack, and jumps to the label supplied if the first value is less than the second value. In this case it jumps over the bit of code to apply the burst cooldown.

Quick note: Sometimes the opcodes you see in your decompiler will not match the opcodes Harmony sees. Usually involving opcodes that have short versions. We’ll discuss this later, but it’s important to keep in the back of your mind.

Step 3, Decide how to modify the code.

There are a lot of ways to accomplish our goals, but let’s try to take the easiest one. Our goal currently is to change the amount of missiles that fire before triggering the burst cooldown. If we want to decrease it, this is easy. We can change the ldc.i4.6 to another opcode to push a smaller number on the stack. For example, ldc.i4.3 will change the resulting C# code from if (firedCount <= 6) to if (firedCount <= 3).

If we want to increase it, depending by how much, is a little different. The static quick opcodes to push literals to the stack only go to 8. So we could change it to ldc.i4.8 to get up to 8 missiles. Or, we can find a new opcode to load any number we want to the stack. In this case ldc.i4 will take the operand and load it onto the stack. So ultimately we want this instruction here:

ldc.i4 12

This would change the 6 missiles to 12 missiles. We could even go further, and call a function to get a return value that contains the burst number. The important part is that whenever we do at this position, it does not consume the fireCount pushed to the stack, and pushes (returns) a 32 bit int to the stack. Now we know what we need to change, we actually have to patch that.

Step 4, Construct the Transpiler method.

Finally we’re getting to something useful. Now what we need to do is take our list of instructions supplied as the argument to our patch method, find where we want to modify them, and then do that. It’s up to you to decide the best way you want to do it. You could assume the position of these opcodes won’t ever change and simply find in your list which positions to modify. Here’s some code that accomplishes that:

public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)  
{  
    var newInstructions = instructions as CodeInstruction[] ?? instructions.ToArray();  
    newInstructions[36].opcode = OpCodes.Ldc_I4;  
    newInstructions[36].operand = 12;  
  
    return newInstructions;  
}

This will certainly work so long as those instructions are at position 36 and remain there even after game updates. Depending on the code in question it may be fine for years, who knows. However, even if the underlying code and logic isn’t changed, updates to Unity and the compiler could add new optimizations or reordering that will.

Let’s find a better way.

Another, better, way would be to identify a unique combination of instructions that precede your patch. We can do some very simple traversal through the enumerable to find something. In our case we know the basic opcodes we want to find looks like this:

  1. ldarg.0
  2. ldfld
  3. ldc.i4.6
  4. blt.s

No where else in our method does this repeating pattern of opcodes exist. So we can simply check for those, and if we find them, we’ve found our patch offset. Here’s an example of that, however I wouldn’t recommend doing it this way every time. You should take some time to build a suite of utility functions to do the search and instruction compares so you can build in opcode, operand, and label matching as well.

public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)  
{  
    var newInstructions = instructions.ToList();  
  
    for (var i = 0; i < newInstructions.Count() - 4; i++)  
    {  
        if (newInstructions[i].opcode == OpCodes.Ldarg_0 &&  
            newInstructions[i + 1].opcode == OpCodes.Ldfld &&  
            newInstructions[i + 2].opcode == OpCodes.Ldc_I4_6 &&  
            newInstructions[i + 3].opcode == OpCodes.Blt)  
        {
            FileLog.Log($"Found our patch at offset {i}");        
            newInstructions[i + 2].opcode = OpCodes.Ldc_I4;  
            newInstructions[i + 2].operand = 12;  
            break;  // We're done, we can stop searching.
        }
    }  
    return newInstructions;  
}

Now we have a patch that will isolate and find that specific part of the code we want to modify. Facepunch could add or remove code around this spot, but unless they change these exact instructions our patch will succeed regardless of the game version. I also included a helpful logging method, FileLog.Log(string) part of the Harmony package, will output log data to the Harmony log. Refer to part 1 about where to find the Harmony logfile for Rust.

Step 5, Profit.

Loading up our mod and running the Rust server we can see if it worked. Harmony outputs all of the opcodes/operands/labels of the patched method, so we can scroll down and find our modifications:

### Harmony id=com.facepunch.rust_dedicated.SampleHarmonyMod, 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-23 03.06.30
Found our patch at offset 21
### Patch SamSite, Void WeaponTick()
L_0000: Local var 0: System.Single
L_0000: Local var 1: System.Single
L_0000: Local var 2: System.Int32
< ... snip ... >
L_003b: callvirt Boolean IsPowered()
L_0040: brtrue Label4
L_0045: ldarg.0
L_0046: ldc.i4.0
L_0047: stfld System.Int32 firedCount
L_004c: br Label18
L_0051: Label4
L_0051: ldarg.0
L_0052: ldfld System.Int32 firedCount
L_0057: ldc.i4 12
L_005c: blt Label5
L_0061: ldarg.0
L_0062: ldfld SamSite+SamTargetType mostRecentTargetType
L_0067: ldfld System.Single timeBetweenBursts

If I haven’t bored you to death by now and you’re paying attention, you may notice a discrepancy I warned about earlier. The branch instruction in ILSpy was blt.s while Harmony only sees blt. You’ll see a lot of instructions that end in .s and this means they’re the “short form” version of the instruction without the .s. That means the operand supplied is an 8bit integer instead of the default 32 bit. In this case, I do not know who is correct, ILSpy or Harmony. What’s important is that you supply what Harmony sees.

Finally, the finished product.

You can view the patch in action here. If you watch carefully you’ll see the SAM now fires 12 missiles before stopping. I know it seems like such mountain to climb to make a simple change, but the change is very future proof.

Troubleshooting & Tips

You should already know by now how to troubleshoot your Harmony mods, where logfiles are and how to utilize logging methods like Debug.Log and FileLog.Log. Let’s go over a few new scenarios that apply particularly to Transpilers.

I’ll repeat, the stack is sacred.

Everything revolves around the stack. Call a function? It’s arguments are pulled from the stack. Function returns a value? Onto the stack. Need to call a method on an instance? Object goes onto the stack. If you insert code, you better know the state of the stack at that point or you will fuck up things.

A note about the .maxstack metadata: This defines the maximum number of items this particular function can expect to be pushed onto the stack. Emphasis on expect. You can exceed that without error, but the code will become unverifiable. Since we’re doing this at runtime, this effectively does nothing.

Build a reliable system to log your Harmony transpilers.

The only thing that’ll really break a Prefix or Postfix is if a name changes or relevant code inside does something now incompatible with the changes. Transpilers are much more likely to break on updates due to optimizations, reordering, new versions of Unity, etc. Make your Transpilers as small and to the point as possible, and log the patching process so you can know when a new version breaks your mods.

Be careful with labels.

When patching instructions that contain labels, make sure to preserve those labels, or provide those labels to new relevant parts of code. CodeInstruction contains a labels field that maintains a list of labels associated with this instruction.

Make sure you understand Oxide’s impact on a method

In the example above, you can see the Assembly I’m using has been patched by Oxide: oxide This is even more important when searching for and comparing instructions you want to change. If you want your Harmony mod to work on “vanilla” Rust and Oxide patched Rust, you’ll need to understand where you may need to account for that.

Make sure you understand Oxide’s impact on visibility

Oxide will often patch field and method visibility. So things once private will become public under Oxide for easier use in plugins. Don’t take that for granted, and make sure you know if a field or method really is public. You can still access them with Harmony, you’ll just need to use Reflection instead.

References

Harmony 2.x CodeInstruction Reference

ILSpy

dnSpy

CIL

List of CIL Instructions