Tutorial 4: Patching Methods - rspforhp/WildfrostModdingDocumentation GitHub Wiki

Welcome back! In this tutorial, we will showcase some examples of modifying methods in the base game of Wildfrost.

Background

The modding framework of Wildfrost is done using the Harmony library, which allows for "patching, replacing, and decorating .NET methods during runtime". While a Harmony instance is active (always in our case), method calls can be rerouted to the Harmony instance's modified methods. This is a very powerful tool to use. The Harmony library has its own documentation and several helpful sections about patching: here. This tutorial will not try to cover patching at the level of detail the Harmony docs will. Instead, we show some applicable scenarios to use some of the techniques from the doc alongside helpful explanations. The only prerequisites to this tutorial is the Basic Project Setup and Tutorial 0. It is highly recommended to publicize your assembly at this point as there will modify some private variables. This tutorial does not publicize the assembly, but it requires Traverse-ing some hoops.


Tutorial 4-0 Harmony Schematic

Schematic of Harmony patching. Picture obtained from the Harmony website.


Prefixes

Skipping a method altogether with Prefix

When Wildfrost v1.2 (June 3rd, 2024) was released, it added several new Steam achievements among other stuff. For the modding branch, which still on v1.1 at the time, this created an interesting issue (picture snipped by Jacorb).



Calling this an issue is an overstatement. You can click "Oh No!" and the game will continue as if nothing happened. However, this error pops up every time you enter the town, so it can be a bit annoying. The solution: simply stop the CheckAchievement class from running Check(). Surprisingly, the solution is just a two-liner (credits to Hopeful/Hopeless).

[HarmonyPatch(typeof(CheckAchievements), "Start")]
class PatchAchievements { static bool Prefix() => false; }

Let's take this line-by-line. The first line specifies what method you are modifying. In this case, we are modifying the Start method in the class CheckAchievements. If a method is overloaded, you may also specify the argument types in the method signature (see the next example). Also note that Start is a private method, but you can patch it anyway.

The second line has the modifications itself. All modifications are written in the form of a class. In our case, our modification needs to prevent Start from properly running. In the patch class, we create a static method named Prefix, which Harmony recognize as a method to run before running the original method. Our Prefix method returns a bool. A value of true indicates to Harmony to run the original method and other prefixes. A value of false indicates to skip the original method and most other prefixes. Since our Prefix only returns false, CheckAchievements.Start() will never run. Now we move on to an important question:

Where do I put this patch?

Anywhere that does not cause a compile error. As long as these two lines are anywhere reasonable in your assembly, the patch will be found and used whenever your mod is turned on. That said, your assembly still has to be recognized as a mod, so you must do the bare minimum in that regard (make a class that extends WildfrostMod and implement all required fields. See Basic Project Setup).

After you do all of this, you can build the .dll file and run the mod and see. (To encounter the error and see the fix, you would have to revert back to v1.1 modding. It's probably not worth testing to be honest.)

Modifying parameters before a method runs

Suppose your leader somehow gains 20 frenzy. Usually, this means that they will trigger about 5 times and then perform the "No Target To Attack" dance 15 times. This takes up a large chunk of time for really no reason, so can we speed things up.

First, we need to know where this behavior occurs. Through some digging, we find that the behavior is performed in the class NoTargetTextSystem in the Run method, shown below.


Tutorial 4-1 NoTargetText

Before diving in, I think it's best to give a summary of what an IEnumerator is. On the surface, an IEnumerator is a set of instructions on how to iterate through a collection of objects (with the collection itself being an IEnumerable). This is technically true, but it does not explain why the above code's return type in a satisfying way. The real answer is that Unity, the game development program, has a feature called coroutines. Coroutines allow for code to be performed across several frames instead of one. And how do you make a Coroutine? Well you pass an IEnumerator into one of the variants of the StartCoroutine methods. You will not see StartCoroutine in the Wildfrost base code, but almost every IEnumerator you see is part of some coroutine!

So, the true answer is IEnumerator's are usually tied with animations or waiting for player inputs. Instead of returning, IEnumerator's yield instead, which means stop for now but continue where you left off once called again. Common yields return null (wait one frame), break (stop and never return), and WaitForX (check every frame until specific conditions are met). See more information on coroutines in the Unity API. Now, we are ready to read the code.

The first yield of _run waits for all other animations of the entity to end. Afterwards, the shake animation is determined using the shakeDurationRange and shakeAmount. The no target message is then written and "popped" onto the screen. Finally, the last yield tells the game to delay the next trigger for a bit of time (0.3~0.4 seconds).

Knowing this, the plan of attack will be to change the shakeDurationRange and shakeAmount before the method is run. Something like this should be good:


[HarmonyPatch(typeof(NoTargetTextSystem), "_Run", new Type[]
{
    typeof(Entity),
    typeof(NoTargetType),
    typeof(object[]),
})]
class PatchNoTargetDance
{
    internal static int count = 0;

    static void Prefix(NoTargetTextSystem __instance, ref Vector2 ___shakeDurationRange, ref Vector2 ___shakeAmount) //__instance is the instance calling the method
    {
        count++;
        Debug.Log($"[Tutorial] Prefix: {count}");
        if (count == 3)
        {
            ___shakeDurationRange = new Vector2(0.15f, 0.2f);   //Half the regular duration
            ___shakeAmount = new Vector3(0.75f, 0f, 0f));        //3/4 the regular shaking
            /* If you pulicized the assembly, you may replace the above two lines with this instead: 
            * __instance.shakeDurationRange = new Vector2(0.15f, 0.2f);
            * __instance.shakeAmount = new Vector3(0.75f, 0f, 0f);
            */
        }
    }
}

There are some differences between this example and the previous example. First, the HarmonyPatch line includes the argument types in a Type[]. This is not needed as _run is not overloaded, but it serves as an example. Second, Prefix is a void return type. This means the prefix method cannot force the original method to be skipped. In addition, the method has three arguments, __instance, ___shakeDurationAmount, and ___shakeDuration. Similar to how Harmony recognizes a prefix by its name, Harmony associates __instance (TWO underscores '_') with whatever is calling the method. In addition, any argument with THREE underscores '_' is a field of __instance. This allows us to ignore the private modifier that is associated to shakeDurationAmount and shakeDuration. Since we are editing the two values, we use ref to signal that any changes to their ___-counterparts will affect the actual fields as well. The full list of special arguments, known as injections, may be found here. In general, two underscores for special argument and three underscores for private fields of __instance. (Note that if you have publicized the assembly, you can just access these fields directly with __instance.shakeDuration and similar. See Basic Project Setup for more details.)

The rest of the code just checks if this is the third time the method is used. If so, the parameters of the animation is modified to be faster. Since I prefer the count to reset every turn, I also wrote the following in the main mod class.


//This is written in the main mod class (whatever extends WildfrostMod)
        protected override void Load()
        {
            Events.OnBattlePreTurnStart += ResetCounter;
            base.Load();
        }

        protected override void Unload()
        {
            Events.OnBattlePreTurnStart -= ResetCounter;
            base.Unload();
        }

        public void ResetCounter(int _) //I don't care about the turn count, so it is _
        {
            PatchNoTargetDance.count = 0;
            Traverse.Create(typeof(NoTargetTextSystem)).Field("instance").Field("shakeDurationRange").SetValue(new Vector2(0.3f, 0.4f));
            Traverse.Create(typeof(NoTargetTextSystem)).Field("instance").Field("shakeAmount").SetValue(new Vector3(1f, 0f, 0f));
            /*
            * NoTargetTextSystem.instance.shakeDurationRange = new Vector2(0.3f, 0.4f);
            * NoTargetTextSystem.instance.shakeAmount = new Vector3(1f, 0f, 0f);
            */
            Debug.Log("[Tutorial] Cleared");
        }

Since we cannot simply let Harmony pass the private fields to us, we need to use Traverse here to access these private fields. The Traverse class is a class in the Harmony library that allows for getting/setting of private fields and methods. Note that Traverse does not check if the variable exists until run time. This means we are upgrading a couple of compile errors into runtime errors. Thus, I would recommend publicizing the assembly to avoid using this (alternate lines shown below).

After that, build the .dll and see what happens. Here is a side-by-side comparison of the target animation. The difference is slight because they are also other pauses in the overall trigger process. The most noticeable thing is that the faster one's text ends its animation prematurely.


Replacing a method with your own

The result is a bit mixed, so let's try something else. We cannot remove the entire pause without doing more code digging (feel free to do that if you want). So instead of making the animation simply move faster, why not try to remove it altogether? To do this, we will make our own IEnumerator method in our HarmonyPatch class. It will be similar to _Run, but it will allow us to display any message we want.


//Spelling __instance instead of instance was a stylistic 
static IEnumerator Etcetera(NoTargetTextSystem __instance, Entity entity, string s) choice here
{
    yield return Sequences.WaitForAnimationEnd(entity);
    TMP_Text textElement = Traverse.Create(__instance).Field("textElement").GetValue<TMP_Text>();
    //Publicized alt: TMP_Text textElement = __instance.textElement;
    //In theory, we could just have the prefix obtain ___textElement and pass it to this method, but I think that makes the code less intuitive.
    textElement.text = s;
    Traverse.Create(__instance).Method("PopText", new Type[1] { typeof(Vector3) }, new object[1] { entity.transform.position }).GetValue(); 
    //Publicized alt: __instance.PopText(entity.transform.position);
    yield return new WaitForSeconds(0.4f); //Long pause time = we won't use the Etcetera method often
}

There is not much to say here about the code above except that we need to grab the entity in _Run now. This comes down to modifying our Prefix, which we will now look like the following.


static bool Prefix(ref IEnumerator __result, NoTargetTextSystem __instance, ref Vector2 ___shakeDurationRange, ref Vector2 ___shakeAmount, Entity entity) //MODIFIED HEADING
{
    count++;
    Debug.Log($"[Tutorial] Prefix: {count}");
    if (count == 3)
    {
        ___shakeDurationRange = new Vector2(0.15f, 0.2f);
        ___shakeAmount = new Vector3(0.75f, 0f, 0f);
        /* If you pulicized the assembly, you may write the above two lines like this instead: 
         * __instance.shakeDurationRange = new Vector2(0.15f, 0.2f);
         * __instance.shakeAmount = new Vector3(0.75f, 0f, 0f);
         */
    }
    if (count == 5)
    {
        __result = Etcetera(__instance, entity, "You get the idea"); //Tells the player to expect more sporadic text
    }
    if (count%10 == 0)
    {
        __result = Etcetera(__instance, entity, $"{count}!");        //There is still a pause, so throwing a message breaks the monotony
    }
    if (count >= 5)
    {
        return false; //skip original method
    }
    return true;      //don't skip original method
}

The most important changes are in the method heading. This Prefix now returns bool so that we can choose to skip the original method. We also added two new parameters: ref IEnumerator __result and Entity entity. The __result variable is what will be returned by the method call. Note prefixes are run before the original method, so __result is almost certainly null here. However, since it has the ref keyword, changing/replacing __result in Prefix will change/replace the __result outside of the method. We then replace __result on the 5th, 10th, 20th, ... trigger to display a custom message with our Etcetera method. Any other trigger past the 5th skips the original method, leaving the result as our method or null (this is effectively returning an IEnumerator of just yield break).

Finally, we also added the parameter entity. Since it is spelled exactly like _run's argument, this argument will be the same as run's. (Funny aside: you can put a ref for this argument and make other cards perform the no target dance instead)

Finally, let's build, run, and see what happens.


Final Remarks on Prefixes

Harmony prefixes is good for modifying parameters, skipping method calls, and sometimes useful debugs. But because this effectively changes the functionality of a method, there may cause compatibility issues, especially with other mods. Although writing your own method and using it instead is an easy approach for everything, it may skip prefixes from other mods. Even worse, other mods may skip your prefixes. This is not trying to disbar you from using prefixes, just remember to consider other ways a method might be changed.

Postfix

As you may expect, the Harmony postfixes happen after the original method is called. Some of the use cases of postfixes overlaps with prefixes, but there are some differences. Since postfix happens afterwards, a postfix can see the __result and decide what to do from there. In addition, postfixes cannot be skipped (unless an error is thrown earlier), but can be overwritten by other postfixes. This makes postfix a bit safer for calling your code.

Debugging and manipulating outputs with postfix

A simple use of postfix is outputting the __result. In fact, if you experimented with the "Hooks" tab in the Unity Explorer, that is precisely what they do. If I wanted to read the output of every call to Dead.Random.Range(int, int), I would write something like this:


[HarmonyPatch(typeof(Dead.Random), "Range", new Type[]
{
    typeof(int), 
    typeof(int)
})]   //Specifying the argument types is important because there are Dead.Random.Range is overloaded
class PatchRandom
{
    static void Postfix(int __result, int minInclusive, int maxInclusive)
    {
        Debug.Log($"[Tutorial] [{minInclusive}, {maxInclusive}] -> {__result}");
    }
}

Just like before, you could edit the __result by using the ref keyword, but postfix has a streamlined version of this. Changing the return type of your postfix to the return type of the original method allows you to overwrite it. For example, changing the above example to static int Postfix(...) means that whatever int is returned becomes the new output of the method. We could try this with Dead.Random.Range and skew the rng in the game, but I have a better example in mind.

Can we add variance to all applied effects?

That is, can we make it so that Snow Stick applies 0-4 snow (determined on use) instead of a flat 2 every time. The answer is yes, but we need to know what methods to patch. Poking around enough status effect classes, you may notice that they never use the count variable directly. Since cards like Lumin Vase and Lupa exist, it is safer just use the method GetAmount() in lieu of count. That is the first half of the solution, but this does not account for attack effects. Before attack effects are applied, they are ownerless, so using GetAmount() would not make sense. Instead they use the aptly named CalculateAttackEffectAmount() from the Hit class. Now that we know what to patch, here's how we can patch it.


[HarmonyPatch(typeof(StatusEffectData), "GetAmount")]
class PatchAmountVariance
{
    static int Postfix(int __result)
    {
        int r = Dead.Random.Range(-2, 2); //roll a random number
        return Math.Max(0, __result + r); //add it to the result, clamp at 0
    }
}

[HarmonyPatch(typeof(Hit), "CalculateAttackEffectAmount")]
class PatchAttackAmountVariance
{
    static int Postfix(int __result)
    {
        int r = Dead.Random.Range(-2, 2); //rinse
        return Math.Max(0, __result + r); //repeat
    }
}

Unfortunately, these patches must be separate classes. There are ways to patch all of the methods of a single class in a single patch class though, but that is beyond the scope of this tutorial (see the API tab of this for more details).

Time to build and see the result through any method you like.


Tutorial 4-5 Shroomblade

Shroomblade Kabonker really knows how to distribute shroom optimally. Unfortunately, Snoffel got a normal roll :/


IEnumerators and Postfix

Postfixes interacts with IEnumerators in an interesting way. Since IEnumerators are effectively a list of actions to do, not the actual execution of them, applying a void Postfix will be the same as applying a prefix. If you want the true postfix experience, you would have to append your code to the IEnumerator result, something like


//Hypothetical code #1
static IEnumerator Postfix(IEnumerator __result)
{
    yield return __result;
    Debug.Log("The actions have been performed.");
}

This is fine, but we can do more. IEnumerator is a list of actions. Can we interlace our actions with the original method's?

Yes, we just have to know a bit more about how IEnumerator's work. They implement one method bool MoveNext() and one property Object Current. Coroutines call MoveNext() to move to the next yield statement, running any lines of code in the way. If MoveNext() returned true, then Current is the object returned by the yield statement. Knowing this, we can send a debug message between each yield like so:


//Hypothetical code #2
static IEnumerator Postfix(IEnumerator __result)
{
    int i = 0;
    while(__result.MoveNext())
    {
        Debug.Log($"Action {i}"!);
        yield return __result.Current;
    }
}

Let's have a real example. The ChooseNewCardSequence class deals with choosing a new card in an event, typically out of three choices. Its Run method is the IEnumerator in charge of sequencing the event properly. Since this event waits for player input, Run does a lot of waiting around. We will spruce up the decision by having the card choices randomly ping. First, we need to locate where the waiting occurs. Looking through the method, it happens here (lines 87-90):


image

Run faithfully checks every single frame to see if the player clicked a button. How loyal.


Yielding a null is not something that this method does often outside of these four lines. We can exploit this. All we have to do is check if the most recent Current is null (and some other conditions), and then we can run our own code instead. This is how it would look.


    [HarmonyPatch(typeof(ChooseNewCardSequence),"Run")]
    class PatchPickMe
    {
        static IEnumerator Postfix(IEnumerator __result, ChooseNewCardSequence __instance, GameObject ___cardGroupLayout, CardContainer ___cardContainer)
//___cardGroupLayout and ___cardContainer have 3 underscores.
        {
            while (__result.MoveNext()) //Move to the next item on the IEnumerator list
            {
                object obj = __result.Current;
                if (obj == null && ___cardGroupLayout.activeSelf && Dead.PettyRandom.Range(0f, 1f) < 0.1f) //yielding null? Great! Let's also sprinkle in some randomness.
                {
                    Debug.Log("[Tutorial] Hit!");
                    ___cardContainer.RandomItem().curveAnimator.Ping(); //Ping!
                    yield return Sequences.Wait(0.4f);               // We don't want it to roll a ping chance every frame, so we will yield a 0.3 second wait instead.
                }
                else                               //Oh, this yield might be important.
                {
                    yield return __result.Current; //Let's send it through.
                }
            }
            Debug.Log("[Tutorial] Ending");
        }
    }

Time to build and test!

Tutorial 4-5 Pinging

Loose Ends

There are some other topics about patching that did not end up in the above. A couple are listed here without examples, so you know that they exist. More information is provided on the Harmony website.

Passthrough

If a patch class has a prefix and a postfix, then you can add the argument __state to pass information from the prefix to the postfix. This can be used for something along the lines of modifying parameters in the prefix, have the method run with these new parameters, and then set them back in the postfix.

Transpilers

It is possible to modify the code of the method itself. This requires editing the lines in IL, a language one level lower than C#. If you are using Visual Studio, you may want to download the tool ILSpy.

Finalizers

Finalizers turn the method into a try-catch statement, with finalizers being the catch. Use this if you want to guarantee the method gives an output.

Reverse Patching

Reverse patching allows you to take a part of the source code and use it as its own method. This can be done before other patches modify the method.

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