In this part, we will use the Configurable Lifetime mod (source code) as an example:
- Players can configure the lifetime of beavers and bots using Mod Settings.
- We apply the changes using Harmony.
Add the dependencies
DLL references
Subscribe to the two mods above in the Steam Workshop or download them from Mod.io. You will need to add references to them in your project’s .csproj
file:
<ItemGroup>
<!-- Harmony -->
<Using Include="HarmonyLib"/>
<Reference Include="$(ModPath)3284904751\0Harmony.dll">
<Private>False</Private>
</Reference>
<!-- Mod Settings -->
<Reference Include="$(ModPath)3283831040\version-0.7\Scripts\*.dll">
<Private>False</Private>
</Reference>
</ItemGroup>
<Private>False</Private>
means the DLLs are only used for compilation. Players will have them in their mods.
ℹ️ Note:
If you use Steam, the path isSteamLibrary\steamapps\workshop\content\1062090\
(wherever yourSteamLibrary
folder is).
Manifest declaration
Declare them as required mods in your manifest.json
to ensure players have them installed.
{
// ...
"RequiredMods": [
{ "Id": "eMka.ModSettings" },
{ "Id": "Harmony" }
]
}
Create the Settings
Declaration
Create a new class MSettings
(or any name you like). It should inherit from ModSettingsOwner
. Here I use a primary constructor:
public class MSettings(
ISettings settings,
ModSettingsOwnerRegistry modSettingsOwnerRegistry,
ModRepository modRepository)
: ModSettingsOwner(settings, modSettingsOwnerRegistry, modRepository), IUnloadableSingleton
{
public override string ModId { get; } = nameof(ConfigurableLifetime);
public override ModSettingsContext ChangeableOn { get; } = ModSettingsContext.All;
}
ModId
is the ID of your mod so Mod Settings knows which one to show.- Since in this mod, settings can be changed at any time, we set
ChangeableOn
toModSettingsContext.All
. The default isMainMenu
only.
Settings properties
public ModSetting<float> BeaverLifeMul { get; } = new(1f, ModSettingDescriptor
.CreateLocalized("LV.CLt.BeaverLifeMul")
.SetLocalizedTooltip("LV.CLt.BeaverLifeMulDesc"));
public ModSetting<float> ChildhoodDaysMul { get; } = new(1f, ModSettingDescriptor
.CreateLocalized("LV.CLt.ChildhoodDaysMul")
.SetLocalizedTooltip("LV.CLt.ChildhoodDaysMulDesc"));
public ModSetting<float> BotLifeMul { get; } = new(1.0f, ModSettingDescriptor
.CreateLocalized("LV.CLt.BotLifeMul")
.SetLocalizedTooltip("LV.CLt.BotLifeMulDesc"));
Here we define the settings we want to expose to the player. There are many kinds of ModSetting
(bool, int, float, int slider, string, string select box, etc.). You can see them all in the mod’s source code.
Keys like LV.CLt.BotLifeMul
are defined in your Localizations file, as mentioned in the previous part.
The mod automatically finds all public ModSetting
properties and adds them. I did not know this at first and most of my earlier mods I add them manually using AddCustomModSetting
, in case you want to do that (for example, to loop through a list):
for (int i = 0; i < DefaultDepths.Length; i++)
{
var z = i;
RangeIntModSetting maxDepth = new(DefaultDepths[z], 1, 30,
ModSettingDescriptor.Create(t.T("LV.CE.MaxDepth", t.T(DynamiteNames[z]))));
AddCustomModSetting(maxDepth, MaxDepthKey + z);
maxDepth.ValueChanged += (_, v) => MaxDepths[z] = v;
MaxDepths[z] = maxDepth.Value;
}
Keep a reference
Harmony methods are static so we need to keep a reference to the settings instance. Since ModSettingsOwner
is already an ILoadableSingleton
, we can use its OnBeforeLoad
and OnAfterLoad
methods instead:
public static MSettings? Instance { get; private set; }
public override void OnAfterLoad()
{
base.OnAfterLoad();
Instance = this;
}
public void Unload()
{
Instance = null;
}
Register the MSettings
class
Now we need to register this class. I usually do this in a MConfigs.cs
file:
[Context("MainMenu")]
public class ModMenuConfig : Configurator
{
public override void Configure()
{
Bind<MSettings>().AsSingleton();
}
}
[Context("Game")]
public class ModGameConfig : Configurator
{
public override void Configure()
{
Bind<MSettings>().AsSingleton();
}
}
💡 Tip:
Even if players cannot change the settings in-game, you should still register theMSettings
class in theGame
context so that it can be used in Harmony patches, and so the players can see the settings while in-game.
Use Harmony patches
Source code: LifetimePatches.cs
Run Harmony patching
Harmony (documentation) only needs to run once because it patches the game code and then it is done. Usually, I run it in an IModStarter
class:
public class ModStarter : IModStarter
{
void IModStarter.StartMod(IModEnvironment modEnvironment)
{
new Harmony(nameof(ConfigurableLifetime)).PatchAll();
}
}
Patches
In this mod, I need to patch class Timberborn.LifeSystem.LifeProgressor
’s IncreaseLifeProgress
method:
[HarmonyPatch]
public static class LifetimePatches
{
// ...
[HarmonyPrefix, HarmonyPatch(typeof(LifeProgressor), nameof(LifeProgressor.IncreaseLifeProgress))]
public static bool StopLifeProgress(LifeProgressor __instance)
{
var mul = MSettings.Instance?.BeaverLifeMul.Value;
if (mul is null or 1f || IsChild(__instance))
{
return true;
}
__instance.LifeProgress +=
__instance._lifeProgressIncreasePerTick
* mul.Value
/ __instance._bonusManager.Multiplier(LifeProgressor.LifeExpectancyBonusId);
return false;
}
}
Here, we mark LifetimePatches
class with the [HarmonyPatch]
attribute so Harmony knows to look for patches in this class. The [HarmonyPrefix]
attribute means this method will run before the original method.
Then we use MSettings.Instance
to get the current MSettings
instance and access the value the player chose. Anything else is up to the logic of your mod.
ℹ️ Note:
It may be disappointing that this is all I can write in this section. Harmony is powerful, but it’s up to you to decide how to use it: what should I patch, what should I change, Prefix or Postfix or even the Transpiler patch. You have to read the game code and understand it to know what to do.
Conclusion
In this part, we learned how to use Harmony to patch Timberborn’s code and how to use Mod Settings to allow players to configure our mods. This is a powerful combination that allows you to change game behavior and make it more customizable.