Theory is all well and good, but a worked example can be a more practical way to learn. In this section, I’ll show 3 different ways of implementing something very similar to my most popular mod, Free Ranging Foragers to show the different options available to modders.
What we want to do is increase the radius of the working circle of forager shacks. From looking at the source code we learnt that what we want to change is the attribute foragingRadius in the ForagerShack class (a child of Building through a few intermediaries). From playing around with Unity Explorer we’ve decided that we want to set that range to 150 and, luckily, changing the working radius also changes the circle drawn in-game (circleRadius) and the villager AI / logistics system automatically adjusts to it. Always good to double check these things rather than taking them for granted!
Option 1 - Getter Replacement
The first way to mod this is the most direct. We use HarmonyPatch such that, every time the game tries to access the foragingRadius of an instance of ForagerShack, we intercept the call and replace it with our own value.
public class GetterReplacementMod : MelonMod
{
}
[HarmonyPatch(typeof(ForagerShack), "get_foragingRadius")]
public class PatchForagerShackRadius
{
public static void Postfix(ref float __result)
{
__result = 150;
}
}
And that’s it. Out MelonMod child does nothing explicitly, but will automatically apply all the HarmonyPatches we declare below it. PatchForagerShackRadius follows the syntax of HarmonyPatch linked above. One thing to note is that the method name is "get_foragingRadius" because we’re modifying the getter of a property rather than a typical method. We could also have used [HarmonyPatch(typeof(ForagerShack), "foragingRadius", MethodType.Getter)] for exactly the same effect. We could have also used a Prefix that returned false instead of a Postfix.
Option 2 - Initialisation Injection
In some ways, intercepting every single call to ForagerShack.foragingRadius is a little wasteful. We could just set that variable once when an instance of forager shack is created, and then let the mod sit back. The creation of GameObjects and Components in Unity is complicated and mostly happens “behind the scenes”. What matters is that, once something is almost ready to be used by the game, its Awake() and Start() methods get called, which basically handle the initialisation of the parts of the code that we’re dealing with. So we can use a HarmonyPatch here:
public class InitialisationInjectionMod : MelonMod
{
}
[HarmonyPatch(typeof(ForagerShack), "Awake")]
public class PatchForagerShackRadius
{
public static void Postfix(ForagerShack __instance)
{
__instance.foragingRadius = 150;
}
}
Very similar to the above, but now we set the value of foragingRadius once for an instance of ForagerShack at the end of its Awake() method and then don’t interfere with it.
Option 3 - Assets Alteration
Can we take the idea of minimising the number of things the mod does even further? Rather than setting the forager radius for each instance of ForagerShack can we just do it once by changing what the game considers the default value? The answer is yes and, best of all, we can do it without HarmonyPatch and instead just use MelonLoader:
public class AssetsAlterationMod : MelonMod
{
public override void OnSceneWasInitialized(int buildIndex, string sceneName)
{
if (sceneName == "Frontier")
{
foreach (BuildingData bData in GlobalAssets.buildingSetupData.buildingData)
{
if (bData.identifier == "ForagerShack")
{
foreach (var conPrefab in bData.prefabEntries)
{
var foragerShack = conPrefab.prefab.GetComponent<ForagerShack>();
if (foragerShack != null)
{
foragerShack.foragingRadius = 150;
}
}
}
}
}
}
}
The method we are overriding is a MelonMod hook that is called after a scene is loaded. A “Scene” is a Unity term for a chunk of the game that belongs together. It’s typically used to group together everything that belongs to the same level in a first or third person game. In FF there are a few scenes: one for the main menu, one for when you’re on the map, and a couple that gets called during the loading screen. As far as I can tell, “Frontier” is the scene that loads the assets from the game files, before it starts building the world from a save file (or for a new game). So just after its been loaded is a good place to make our changes.
GlobalAssets is a static class that holds a lot of the fixed game data - things that don’t change while playing or from one run to another. BuildingSetUpData is that part of the data used to set up buildings. So we go through the list and pick out the one with the name we want (UnityExplorer is very helpful for finding out what this name is - it’s not always what you think it should be). BuildingSetUpData has the variables that make sense for every building, eg, desirability and construction cost. But foragingRadius is specific to one building, so we have to dig deeper into the prefabs. Prefabs are a type of Unity GameObjects that serve as a template for others that are created (possibly in multiple copies) while playing the game. Most buildings only have 1 prefab but some, especially houses, have multiple each with unique graphics. So we go through all the prefabs (only one in this case) and pick the Component that has the class we want to change. One last null check (if we had the wrong name and had selected, eg, a bakery, this component would be missing), and set the foraging radius to what we want it to be.
And that’s it! This will happen once during the loading scene, and then the mod does nothing at all while playing the game. In fact, the way it’s done here, the change will persist until the game is closed and relaunched, so we only really need to do it the first time a map is created / loaded per play session.
This example is more specific than the others, but it can easily be adapted to changing other things. There are other lists in GlobalAssets than buildingSetupData. PrefabAssetMap might be the most generic. For some cases ObjectDataStore.GetAllDataRecords<>() is a very powerful tool to get all data of a particular type (eg, if you want to modify some / all crops). The singleton GameManager and the various sub-managers it contains is also a great way to get at game data, although they don’t all initialise early enough. Lastly, Unity comes with an inbuilt tool to search for all instances of a particular class: Resources.FindObjectsOfTypeAll(typeof())
Common Pitfalls
A word of caution about all of these is to make sure the changes are applied the expected number of times. Here we’re setting a hard value, so doing it multiple times doesn’t matter. But if we were, eg, doubling a value it would be a problem if we did it too many times. This is not a problem for Option 1. But for option 2, when we save the game, forager shacks will get saved with the bigger radius. When we later load that save, Awake() will be called once again, and we’ll double out already doubled value! Option 3 gets called on every loading scene, so if you reload a save without quitting, you’ll have doubled it twice. In that case it’s easy to set up a Boolean flag to only apply the correction once, but in general these kind of traps are something to be careful about and are definitely something you should be testing.
Which one should I use?
Which ever works! Mods are hard enough, if you have something which does what you want it to do, and you’ve tested that it works, well done! In this case we had an abundance of choice, but for some other projects this might not be the case.
If you do have the choice, then it comes down to personal preference. Personally, I usually like Option 3 the most and Option 1 the least. Despite the code looking a little more complicated, the fact that it’s not using HarmonyPatch makes it easier to debug. The mod also ‘does less’ as it only intervenes once during loading. This means that it doesn’t harm performance (not that any of those examples would in any appreciable way), and also that it’s less likely to conflict with other mods or with future patches. Or, at least, those conflicts will be easier to identify and resolve. That said, when I’m changing the behaviour of a function rather than the value of an attribute, something closer to Option 1 is often the only viable way to go.