Using Harmony - brandonandzeus/Trainworks2 GitHub Wiki

Using Harmony

If this tutorial spooks you a bit, know that not everything is this hard; Harmony is only used to do things that the Modding framework does not provide an easier way to do. Adding new cards, clans, and the like is much less confusing than this. If you have a difficult time wrapping your head around Harmony, jump ahead to the next tutorial and come back some other time when you've got a better grasp on things.

If you're trying to do things that have never been done before, this is the tutorial for you.

Not every mod wants or needs to depend on the framework. Even if yours does, the framework can only do so much; for things outside the scope of the framework, there's Harmony. It allows for patching nearly any method the original game provides.

For detailed information on using Harmony, reference its official documentation here.

The most common flavors of Harmony Patches come in two forms: Prefix and Postfix. A Prefix patch happens before the target method, and a Postfix patch happens after.

Each patch is its own class. It has either a Prefix or Postfix method (or both, or neither...). The method must be static, but it doesn't matter if it's public, private, etc.

The easiest way to mark a class as being a Harmony patch is to put a HarmonyPatch annotation on it. There are several kinds, but the most common one is this:

[HarmonyPatch(typeof(ClassName), "MethodName")]

Example:

[HarmonyPatch(typeof(TooltipGenerator), nameof(TooltipGenerator.GetRelicTooltips))]
class TooltipGenerator_GetRelicTooltips_Patch
{
    static void Postfix()
    {
    }
}

The Prefix and Postfix methods can take the following as arguments:

  • Any arguments the original method took, with the same type and name
  • The instance of the class the method has been called on: ClassName __instance (two underscores)
  • Any fields of the instance, whether public, private, or protected. They should have the same name and type as the original, but with three underscores before: FieldType ___fieldName
  • The return value of the method: __result (two underscores)
  • A couple of other things that aren't used as often. Reference the official Harmony documentation for details.

All arguments to a Prefix or Postfix method should be marked ref if you intend on modifying it. This means if you change it in the method, it'll change the actual value, too. If they aren't marked as ref, any changes you make are temporary and won't last past the method.

This is a lot to take in, but let's take it one step at a time.

Baby's first patch

The following example patch will add a tooltip to all relics.

[HarmonyPatch(typeof(TooltipGenerator), "GetRelicTooltips")]
class TooltipGenerator_GetRelicTooltips_Patch
{
    static void Postfix(ref List<TooltipContent> tooltips)
    {
        tooltips.Add(new TooltipContent("Testi", "Moddy mod tooltip", TooltipDesigner.TooltipDesignType.Default));
    }
}

Note the ref List<TooltipContent> tooltips. The original method, GetRelicTooltips() in the TooltipGenerator class, takes a List<TooltipContent> tooltips as an argument. Since the name and type in the Postfix method match exactly, Harmony reaches in and grabs it for us.

If you tried to run this patch, it wouldn't do anything. This is because we haven't told Harmony to execute our patches yet. Go back to your plugin class (the one marked with the BepInEx annotations). Add the following method:

private void Awake()
{

}

If you're familiar with Unity, you'll probably recognize this. It does the same thing here. If you're unfamiliar, think of this as being called when your plugin starts up.

Add the following code to the Awake() method:

var harmony = new Harmony(GUID);
harmony.PatchAll();

Recall that we made a constant for the plugin GUID earlier. This is the first instance of it being useful.

Harmony will hunt through your dll, find all Harmony patches, and apply them. Build the project, run the game, and hover over any relic. It should have a new tooltip.

An interesting thing to note with Harmony is that more than one plugin can patch the same method. Harmony will make sure they don't conflict. If order matters, you can mark your plugin as depending on another one using the [BepInDependency("plugin_GUID")] annotation; this will guarantee that your plugin's patches are executed after the dependency.

Patching an overloaded method and changing a return value

Let's write another patch! This one will be a bit more complex.

We're going to patch the GetCardsForClass() method in the CardPoolHelper class: CardPoolHelper.GetCardsForClass(). There's actually more than one of these with different arguments. This is referred to as "overloading" a method. When this happens, we need to provide the HarmonyPatch annotation a specification of the types of the arguments. This is done via the third parameter to HarmonyPatch.

[HarmonyPatch(typeof(CardPoolHelper), "GetCardsForClass", new Type[] { typeof(CardPool), typeof(ClassData), typeof(CollectableRarity), typeof(CardPoolHelper.RarityCondition), typeof(bool) })]
class NoFactionRestrictionPatch
{
    static void Postfix()
    {
    }
}

It looks scary, but that third parameter is just a single Type array of the arguments the method we're patching took.

Let's add some arguments to the Postfix() method:

static void Postfix(ref List<CardData> __result, ref CardPool cardPool)
{
}

The cardPool argument is just like the tooltips argument from the previous example; it's an argument to the original method.
Note the __result argument. This is the return value of the method. The original method returns a list of all cards in the pool meeting the necessary faction restriction, rarity restriction, etc. What we want to do with this patch is return a list of all cards in the card pool, regardless of faction or rarity.

The finished patch looks something like this:

[HarmonyPatch(typeof(CardPoolHelper), "GetCardsForClass", new Type[] { typeof(CardPool), typeof(ClassData), typeof(CollectableRarity), typeof(CardPoolHelper.RarityCondition), typeof(bool) })]
class NoCardRestrictionsPatch
{
    static void Postfix(ref List<CardData> __result, ref CardPool cardPool)
    {
        List<CardData> list = new List<CardData>();
        for (int i = 0; i < cardPool.GetNumCards(); i++)
        {
            CardData cardAtIndex = cardPool.GetCardAtIndex(i);
            list.Add(cardAtIndex);
        }
        __result = list;
    }
}

We iterate through all the cards in the card pool, add them to a new list, then set the method's return value to that list.

Boot up the game and play through to your first card reward. You'll note that it's offering you cards from outside your clans.

Preventing the original method from running

Let's write another one. This patch will prevent the original method from running entirely! You should avoid doing this when possible since it means if anyone else has patched the method, their patch won't get executed, but sometimes it's unavoidable.

If you want to block the original method, you have to use a Prefix patch. The reasoning is fairly intuitive: you can't prevent a method from running if it's already been run.

We actually need two patches to make this one work completely. The first gets the card to show up in the logbook, and the second allows the searchbar to find it.

[HarmonyPatch(typeof(MetagameSaveData), "HasDiscoveredCard", new Type[] { typeof(CardData) })]
class ShowAllLogbookCardsPatch
{
    static bool Prefix(ref bool __result)
    {
        __result = true;
        return false;
    }
}

[HarmonyPatch(typeof(MetagameSaveData), "HasDiscoveredCard", new Type[] { typeof(string) })]
class ShowAllLogbookCardsPatch2
{
    static bool Prefix(ref bool __result)
    {
        __result = true;
        return false;
    }
}

Notice how the Prefix patch is returning bool. This return value says whether or not to run the original; if true, the original is run, and if false, it is not. The patch itself is pretty simple. It just returns true, always. If you have some cards that are still locked, you can load up the Logbook and they should be visible now. Most of the example code in these tutorials is pretty useless by design, but this is actually a nice patch to have around since it makes testing modded cards much easier-- without it, they'll show up as being "undiscovered" in the Logbook.

Next: Using the Trainworks Modding Tools

⚠️ **GitHub.com Fallback** ⚠️