Tutorial: Harmony Patching

From Stardew Modding Wiki
Jump to navigation Jump to search

Introduction

The world of Harmony is a bit of a lawless land, but it's incredibly powerful. You can pretty much implement anything you want, but you can also break anything you want. With that in mind, you should begin by reading the main wiki's intro to Harmony to get you set up. This tutorial will focus on the actual patching and assumes you already understand C# and know what Harmony is and does. It is also recommended that you skim the patching docs before continuing. This tutorial is not a substitute for the docs. Rather, the intention is to provide a look into the considerations that mod authors may think about when deciding how and when to patch with Harmony and to show some simple-ish examples.

More resources

Planning your patches

The first step before writing a patch is to make sure you even need to write a patch at all! A lot can be done with SMAPI's event API, so you should check to make sure there isn't a useful event there first that would let you avoid a patch entirely. Then, you need to figure out where in the game code the behavior you want to change even is! This can be pretty difficult, but searching the entire decompiled game code folder at once for keywords can be a good start. Thirdly, you need to decide what type of patch you need to write. Prefixes run before the original C# method and can choose to let subsequent prefixes and the original method run, or they can stop the original and other prefixes from running entirely. They're great for changing arguments before the original method gets them. Postfixes are the most compatible type of patch, and they run after the original method. They are useful for things like editing the return value. Lastly, there are transpilers, which are the hardest to write and debug, but they're the most powerful. Transpilers edit the actual IL code of a method and can be used to insert or delete instructions in the middle of a method. Sometimes, you can avoid having to write a transpiler by using a prefix/postfix pair.

Patch types

Postfixes

Harmony postfixes run after the original C# method. They are the best choice for preserving compatibility with other mods and not breaking base game functionality. Let's do a rather silly example to start: every time we hit a tree with our axe, it says "ouch!". A postfix is a good choice here because this is a side effect we want to add.

The relevant code to patch for this action is in StardewValley.TerrainFeatures.Tree::performToolAction. Let's first create our postfix method. These should be static and return void. This method could really go anywhere in your codebase, and many modders like to separate their Harmony patches to separate folders and classes to stay better organized. However, you can put them directly into your mod entry file if you wish, which is what this small example will assume as it references the mod's Monitor.

The original method has a few parameters, but we are interested in what tool was used to hit the tree to determine if we should say "ouch!". We will inject this parameter (which in the game code is named t) into our postfix with the same name; Harmony will know that we want the value that was passed to the original method and inject it for us. We'll also include some basic error handling so that we catch errors early and don't propagate them to other mods. (Since this postfix is static, you will need to expose the main mod's Monitor in some way, such as making it a static field in your main mod entry class. This example has decided to name that static field ModMonitor).

public static void performToolAction_Postfix(Tool t)
{
    try {
        if (t is not Axe) return;  // We only care about axes

        Game1.showGlobalMessage("Ouch!");
    }
    catch (Exception e) {
        ModMonitor.Log($"Failed in {nameof(performToolAction_Postfix)}:\n{e}", LogLevel.Error);
    }
}

Now, we'll need to tell Harmony to apply this patch, and what method to apply it to. This is usually done in the Entry method in the main mod entry file. We will have to specify the original method to patch over, as well as what patch to apply to it.

using StardewValley.TerrainFeatures;

// namespace and class declaration, etc.

public override void Entry(IModHelper modHelper)
{
    Harmony harmony = new(ModManifest.UniqueID);
    harmony.Patch(
      original: AccessTools.Method(typeof(Tree), nameof(Tree.performToolAction)),
      postfix: new HarmonyMethod(typeof(ModEntry), nameof(performToolAction_Postfix))  // assumes main mod class is called ModEntry
    );
}

Prefixes

Harmony prefixes run before the original C# method. They're a good choice when you want to change the arguments to a method before it runs, save some state that you can check again in the postfix, or when you don't want the original method to run at all. For this example, we'll make it so that any time the farmer tries to give a gift to an NPC, it suddenly turns into a piece of trash. The original method of interest is StardewValley.NPC.tryToReceiveActiveObject.

We'll start by writing the prefix itself. These may return bool or void. void prefixes or bool prefixes that return true always allow the original method to run whereas bool prefixes that return false stop the original method (and any subsequent prefix patches by your mod or other mods) from running. Since we don't need to conditionally allow or disallow the original method, we will use a void prefix.

The original method has 2 parameters, both of which we will inject into our prefix. (This method may be called many times with probe set to true just to check something, so this allows us to stop our prefix from firing until we actually try to gift an object).

public static void tryToReceiveActiveObject_Prefix(Farmer who, bool probe)
{
    try
    {
        if (probe) return;
        StardewValley.Object trash = ItemRegistry.Create<StardewValley.Object>("(O)168");
        who.ActiveObject = trash;
    }
    catch (Exception e)
    {
        ModMonitor.Log($"Failed in {nameof(tryToReceiveActiveObject_Prefix)}:\n{e}", LogLevel.Error);
    }
}

Now, we just need to apply this patch in our Entry method:

public override void Entry(IModHelper modHelper)
{
    Harmony harmony = new(ModManifest.UniqueID);
    harmony.Patch(
      original: AccessTools.Method(typeof(NPC), nameof(NPC.tryToReceiveActiveObject)),
      prefix: new HarmonyMethod(typeof(ModEntry), nameof(tryToReceiveActiveObject_Prefix))
    );
}

Nice, everything turns to trash! But, it feels a bit mean to the player to take away whatever they were holding. Let's look at the next section to see how we can restore the original inventory state.

Prefix-postfix pairs

Adding a prefix and a postfix to the same method allows us to pass state from before the method runs to after it has run. This will build off of the code from the Prefixes section to restore the player's active object to whatever it was before it inexplicably became trash.

First, we will modify our prefix to take an out parameter called __state. You must name it this way or Harmony will not know to pass this to the postfix.

public static void tryToReceiveActiveObject_Prefix(Farmer who, bool probe, out StardewValley.Object? __state)
{
    try
    {
        if (probe)
        {
            __state = null;
            return;
            }
         __state = who.ActiveObject;

         StardewValley.Object trash = ItemRegistry.Create<StardewValley.Object>("(O)168");
         who.ActiveObject = trash;
    }
    catch (Exception e)
    {
        ModMonitor.Log($"Failed in {nameof(tryToReceiveActiveObject_Prefix)}:\n{e}", LogLevel.Error);
        __state = null;
    }
}

We've chosen to use null as the signal for "We didn't swap the player's active object out for trash". Now, in the postfix, we can see if the __state is not null and then restore the player's active object to the value of __state:

public static void tryToReceiveActiveObject_Postfix(Farmer who, StardewValley.Object? __state)
{
    try
    {
        if (__state is null) return;

        who.ActiveObject = __state;
    }
    catch (Exception e)
    {
        ModMonitor.Log($"Failed in {nameof(tryToReceiveActiveObject_Postfix)}:\n{e}", LogLevel.Error);
    }
}

Lastly, as per usual, we will apply the patch:

public override void Entry(IModHelper helper)
{
    Harmony harmony = new(ModManifest.UniqueID);
    harmony.Patch(
        original: AccessTools.Method(typeof(NPC), nameof(NPC.tryToReceiveActiveObject)),
        prefix: new HarmonyMethod(typeof(ModEntry), nameof(tryToReceiveActiveObject_Prefix)),
        postfix: new HarmonyMethod(typeof(ModEntry), nameof(tryToReceiveActiveObject_Postfix))
    );
}

Great! You've just passed state between the prefix and the postfix!

Transpilers

As previously mentioned, transpilers are very spooky and easy to break things with. They modify particular lines of code inside the original method. Make sure you properly review the code you want to modify before you write one! Let's do a transpiler that adds a random number to the harvest of ginger and spring onions when they're harvested. A transpiler is a good choice here because a prefix or postfix would require copying lots of the original code and would likely not be very compatible with other mods as a consequence. In contrast, a transpiler just requires us to insert a few lines of IL inside this fairly lengthy method. This will just require adding instructions, so it's a nice example.

First, let's look at the relevant game code in StardewValley.Crop.harvest (lines 490, 512, 526-540 in my decompile from ILSpy; this may very depending on your decompiler and how you choose to view the code. You can always search using ctrl+f or cmd+f for the method name as well):

// line 490
Random r = Utility.CreateDaySaveRandom(xTile * 1000, yTile * 2000);

// line 512
Game1.stats.ItemsForaged += (uint) o.Stack;  // o is an Object with the harvest crop (ginger/spring onion)

// line 526
if (Game1.player.addItemToInventoryBool(o))  
{
	Vector2 initialTile2 = new Vector2(xTile, yTile);
	Game1.player.animateOnce(279 + Game1.player.FacingDirection);
	Game1.player.canMove = false;
	Game1.player.currentLocation.playSound("harvest");
	DelayedAction.playSoundAfterDelay("coin", 260);
    if (!this.RegrowsAfterHarvest())
    {
	Game1.multiplayer.broadcastSprites(Game1.currentLocation,
        new TemporaryAnimatedSprite(17,
            new Vector2(initialTile2.X * 64f, initialTile2.Y * 64f), Color.White, 7, r.NextBool(), 125f)
        );
	Game1.multiplayer.broadcastSprites(Game1.currentLocation,
        new TemporaryAnimatedSprite(14,
            new Vector2(initialTile2.X * 64f, initialTile2.Y * 64f), Color.White, 7, r.NextBool(), 50f)
        );
	}
	Game1.player.gainExperience(2, experience);
    return true;
}

Our goal is to randomly modify o's Stack field. You can do this entirely by writing IL code, but a common pattern is to write just enough IL code to put a method you've written that does most of the processing into the original method. Then, you can focus on the logic more than the ins and outs of IL.

The method to modify the Stack field is just one line of C#. We will need a reference to o and a Random object. Thankfully, this method already has one instantiated, so we'll just pass it as a parameter too:

public static void ModifyObjectStackRandomly(StardewValley.Object o, Random r)
{
    // this is just one line of C#, but it'd be quite a few lines of IL!
    o.Stack = r.Next(10);
}

But, we still have to insert enough IL to call our method. It makes the most sense to modify the Stack field right before line 512 in the C# because that value is used for the game stats. Let's look at the same code in ILSpy. Open Stardew Valley.dll in ILSpy and be sure to change the language to decompile to "IL with C#" if necessary. Navigate through the classes to our method where we see:

// Game1.stats.ItemsForaged += (uint) @object.Stack;
IL_0110: call class StardewValley.Stats StardewValley.Game1::get_stats()
IL_0115: dup
IL_0116: callvirt instance uint32 StardewValley.Stats::get_ItemsForaged()
IL_011b: ldloc.1
IL_011c: callvirt instance int32 StardewValley.Item::get_Stack()
IL_0121: add

We'll want to modify the field right above this code. Let's start our transpiler. These should be static and both take in and return an IEnumerable<CodeInstruction>. We need to find the part of the code we want to insert into, which may be difficult. There are many ways to do this (such as with loops), but CodeMatcher is helpful. This set of functionality can be used to search for specific IL instructions, insert instructions, delete them, and much more complicated things like update branch labels. It looks like the call to get Game1.stats.ItemsForaged is a good landmark because it's the first (and only) occurrence of this method call in the harvest method.

public static IEnumerable<CodeInstruction> harvest_Transpiler(IEnumerable<CodeInstruction> instructions)
{
    CodeMatcher matcher = new(instructions);
    MethodInfo getStatsInfo = AccessTools.PropertyGetter(typeof(Game1), nameof(Game1.stats));
    MethodInfo getItemsForagedInfo = AccessTools.PropertyGetter(typeof(Stats), nameof(Stats.ItemsForaged));
    MethodInfo getStackInfo = AccessTools.PropertyGetter(typeof(Item), nameof(Item.stack));

    MethodInfo modifyInfo = AccessTools.Method(typeof(ModEntry), nameof(ModifyObjectStackRandomly));

    matcher.MatchStartForward(
        new CodeMatch(OpCodes.Call, getStatsInfo),
        new CodeMatch(OpCodes.Dup),
        new CodeMatch(OpCodes.Callvirt, getItemsForagedInfo),
        new CodeMatch(OpCodes.Ldloc_1),
        new CodeMatch(OpCodes.Callvirt, getStackInfo),
        new CodeMatch(OpCodes.Add)
        )
        .ThrowIfNotMatch($"Could not find entry point for {nameof(harvest_Transpiler)}")
        .Advance(1)
        .Insert(
            new CodeInstruction(OpCodes.Ldloc_1),  // StardewValley.Object o
            new CodeInstruction(OpCodes.Ldloc_3),  // Random r
            new CodeInstruction(OpCodes.Call, modifyInfo)
        );

    return matcher.InstructionEnumeration();
}

Well that's a bit overwhelming! Let's break it down.

  • Line 3: Instantiates a new CodeMatcher for us to use to search through the IL for the getter.
  • Lines 4-8: Give us a reference to several property getters that we're going to look for in the input instructions so we know where to insert. They also get a reference to our method we want to insert.
  • Lines 10-17: Search for the IL lines we want to match to find and insert at (from the snippet earlier).
  • Line 18: Throws an error if for some reason we can't find the place to insert our instructions ("fail fast" approach).
  • Line 19: Moves the CodeMatcher cursor forward one instruction. Technically, this will insert our instructions right after the call class StardewValley.Stats StardewValley.Game1::get_stats() instruction, but this is still before the callvirt instance uint32 StardewValley.Stats::get_ItemsForaged() instruction we care about and we will return the stack to the way it was before our instructions run so we don't interfere with other code. We do this to avoid dealing with the trickiness of labels, as this code is right at the top of an if-statement.
  • Lines 20-24: Put the parameters for our method on the stack and call our method!
    • Line 21: Puts o on the stack, which is the first argument our function will consume. Looking through ILSpy, we see that it's stored in local variable 1.
    • Line 22: Puts r on the stack, which is the Random object that has already been instantiated earlier in the original game code and which our method requires as a second parameter. Looking through ILSpy, we see that it's stored in local variable 3.
    • Line 23: Calls our ModifyObjectStackRandomly function that we wrote, which will consume the 2 things we just put on the stack.
  • Line 26: Returns the new modified list of instructions (CodeMatcher has handled this all for us).

Now, all that's left is to actually patch the original harvest method in our main mod's Entry function, which is very similar to the other kinds of patches:

public override void Entry(IModHelper helper)
{
    Harmony harmony = new(ModManifest.UniqueID);

    harmony.Patch(
        original: AccessTools.Method(typeof(Crop), nameof(Crop.harvest)),
        transpiler: new HarmonyMethod(typeof(ModEntry), nameof(harvest_Transpiler))
    );
}

This barely scratches the surface of the things you can accomplish with transpilers, but hopefully it gets you started!

Tips and tricks

  • Read the docs: The full API reference is quite daunting, but the main prefix, postfix, and transpiler pages include a lot of information that there isn't space for here!
  • Reference other mods: There are lots of popular C# mods which are open-source. You can find them by going to https://smapi.io/mods, toggling the advanced filters by the search bar, and filtering for only open-source mods. Just make sure if you re-use actual code that other authors have written, you check out their license and permissions first!