Harmony
Tutorials
- Harmony documentation
- Reading / Modifying private variables
- Changing the Return Value of a Method with Harmony
- Lesser Scholar Youtube - Using Harmony. Prefix, Postfix, Transpiler
Activation
In your SubModule.cs:
protected override void OnSubModuleLoad() {
base.OnSubModuleLoad();
Harmony harmony = new Harmony("YOUR_MOD_TEXT_ID");
harmony.PatchAll();
...
Priority
then
Changing property with private setter
public TextObject Name { get; private set; }
var propertyInfo = AccessTools.Property(typeof(ItemObject), "Name");
var setter = propertyInfo.GetSetMethod(true);
TextObject newItemName = new(item.Name + " ***");
setter.Invoke(item, new object[] { newItemName });
Postfix for the getter
protected virtual int SkillLevelToAdd
{
get
{
return 10;
}
}
[HarmonyPatch(typeof(CharacterCreationContentBase), "SkillLevelToAdd", MethodType.Getter)]
protected static void Postfix(int __result) => __result = 30;
Working with the List
[HarmonyPatch(typeof(MapEventSide), "ApplyRenownAndInfluenceChanges")]
public static void Postfix(MBList<MapEventParty> ____battleParties)
{
foreach(MapEventParty mapEventParty in ___battleParties)
{
//...
}
}
Private method match
[HarmonyPatch(typeof(BarterManager), "ApplyBarterOffer")]
vs when not private:
[HarmonyPatch(typeof(LeaveKingdomAsClanBarterable), nameof(LeaveKingdomAsClanBarterable.Apply))]
Ambiguous match for HarmonyMethod
HarmonyLib.HarmonyException: 'Ambiguous match for HarmonyMethod[(class=TaleWorlds.CampaignSystem.GameComponents.DefaultCharacterDevelopmentModel, methodname=CalculateLearningRate, type=Normal, args=undefined)]'
Means that several methods exist with the same name but different arguments. Method have overrides.
Define arguments for Harmony to properly match the method:
[HarmonyPatch(typeof(DefaultCharacterDevelopmentModel))]
[HarmonyPatch("CalculateLearningRate", typeof(int), typeof(int), typeof(int), typeof(int), typeof(TextObject), typeof(bool))]
Patching game Models
Usually when game Model is patched, that leads to an error: TypeInitializationException
Explanation from BannerlordCoop github:
/// Fixes issue with DefaultPartySpeedCalculatingModel._culture statically calls GameTexts.FindText
/// and when harmony patches, it calls the static constructor for DefaultPartySpeedCalculatingModel
/// and results in a null reference exception because _gameTextManager has not been initialized
It can be solved the same way as BannerlordCoop solved it - initializing _gameTextManager if it's null before Harmony patches. More info here.
Another way is to run Model's patch in the OnGameStart, and the rest of harmony patches in the OnSubModuleLoad.
For that this model's patch should not have [Harmony] tags and it should be executed separately. Example:
public class DefaultPartySpeedCalculatingModel_CalculateFinalSpeed_Patch
{
public static void Postfix(ref ExplainedNumber __result)
{
TextObject text = new TextObject("{=}Slow down", null);
__result.AddFactor(-0.5f, text);
__result.LimitMin(1f);
}
}
bool _lateHarmonyPatchApplied = false;
protected override void OnGameStart(Game game, IGameStarter gameStarter)
{
base.OnGameStart(game, gameStarter);
if (_lateHarmonyPatchApplied) return;
Harmony harmony = new Harmony("my_mod_harmony_late");
var original = typeof(DefaultPartySpeedCalculatingModel).GetMethod("CalculateFinalSpeed");
var postfix = typeof(DefaultPartySpeedCalculatingModel_CalculateFinalSpeed_Patch).GetMethod("Postfix");
if (original != null && postfix != null) {
harmony.Patch(original, postfix: new HarmonyMethod(postfix));
_lateHarmonyPatchApplied = true;
}
}
Without _lateHarmonyPatchApplied patch will be applied several times on new game start from menu
More info about Manual Harmony Patching here
Run before other patch
Unpatch other patches
[HarmonyPatch(typeof(TheType), "TheMethod")]
public static class MyPatches
{
[HarmonyBefore(new string[] { "other.mod's.id" })]
static void Prefix(out bool __state)
{
if (myCondition)
{
HarmonyInstance.Unpatch(AccessTools.Method(typeof(TheType), "TheMethod"), HarmonyPatchType.Prefix, "other.mod's.id");
__state = true;
}
}
static void Postfix(bool __state)
{
if (__state)
{
HarmomyInstance.Patch(AccessTools.Method(typeof(TheType), "TheMethod"), HarmonyPatchType.Prefix, "other.mod's.id");
}
}
}
Debug
Available via CTRL+ALT+H: