Skip to content

Harmony

Tutorials

Activation

In your SubModule.cs:

protected override void OnSubModuleLoad() {
    base.OnSubModuleLoad();
    Harmony harmony = new Harmony("YOUR_MOD_TEXT_ID");
    harmony.PatchAll();
    ...

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

Debug

Available via CTRL+ALT+H: