Skip to content

Patching Basics

Garrett Luskey edited this page Mar 12, 2024 · 3 revisions

Property Patching

Property setters only need to be patched because when syncing state only data needs to be synchronized and getters do not need to be patched (unless they are changing the data they store).

For patching setters, a tool exists to do this automatically called IAutoSync.
Read more about it here https://github.com/Bannerlord-Coop-Team/BannerlordCoop/wiki/Property-AutoSync

Field Patching

The only way of syncing fields currently is by using a transpiler and injecting an intercept method.

WARNING - if the field is public, it can be changed from outside the class so consider adding those external functions to the target methods as well.

[HarmonyPatch]
internal class HeroLevelPatches
{
    private static readonly ILogger Logger = LogManager.GetLogger<ExSpousesPatches>();

    private static IEnumerable<MethodBase> TargetMethods()
    {
        return AccessTools.GetDeclaredMethods(typeof(Hero));
    }

    [HarmonyTranspiler]
    private static IEnumerable<CodeInstruction> ExSpousesTranspiler(IEnumerable<CodeInstruction> instructions)
    {
        var heroLevelField = AccessTools.Field(typeof(Hero), nameof(Hero.Level));
        var fieldIntercept = AccessTools.Method(typeof(HeroLevelPatches), nameof(FieldIntercept));

        foreach (var instruction in instructions)
        {
            if (instruction.opcode == OpCodes.Stfld && instruction.operand as FieldInfo == heroLevelField)
            {
                // Load instance onto stack
                yield return new CodeInstruction(OpCodes.Ldarg_0);
                yield return new CodeInstruction(OpCodes.Call, fieldIntercept);
            }
            else
            {
                yield return instruction;
            }
        }
    }

    public static void FieldIntercept(int newLevel, Hero instance)
    {
        // Allows original method call if this thread is allowed
        if (CallOriginalPolicy.IsOriginalAllowed())
        {
            instance.Level = newLevel;
            return;
        }

        // Skip method if called from client and allow origin
        if (ModInformation.IsClient)
        {
            Logger.Error("Client added unmanaged item: {callstack}", Environment.StackTrace);
            instance.Level = newLevel;
            return;
        }

        MessageBroker.Instance.Publish(instance, new LevelChanged(instance, newLevel));

        instance.Level = newLevel;
    }
}

Collection Patching

For collections we only need to sync when something is added or removed.

For this example let's sync the Hero._exSpouses collection

[HarmonyPatch]
internal class ExSpousesPatches
{
    private static readonly ILogger Logger = LogManager.GetLogger<ExSpousesPatches>();

    // Run transpiler on all methods in the Hero class\
    // If the field we are patching is public then it can be accessed outside of the Hero class and those will have to be added to the target methods.
    private static IEnumerable<MethodBase> TargetMethods()
    {
        return AccessTools.GetDeclaredMethods(typeof(Hero));
    }

    [HarmonyTranspiler]
    private static IEnumerable<CodeInstruction> ExSpousesTranspiler(IEnumerable<CodeInstruction> instructions)
    {
        var listAddMethod = AccessTools.Method(typeof(List<Hero>), "Add");
        var listAddOverrideMethod = AccessTools.Method(typeof(ExSpousesPatches), nameof(ListAddOverride));

        foreach (var instruction in instructions)
        {
            // Find List<Hero>.Add in the intermediate language instructions (MSIL)
            if (instruction.opcode == OpCodes.Callvirt && instruction.operand as MethodInfo == listAddMethod)
            {
                // Load instance onto stack
                yield return new CodeInstruction(OpCodes.Ldarg_0);
                // Replace our add call with our intercept function (line above adds instance to the parameters, specifically as the last parameter by adding it to the stack)
                yield return new CodeInstruction(OpCodes.Call, listAddOverrideMethod);
            }
            else
            {
                // Return original instruction if it is not the one we are looking for
                yield return instruction;
            }
        }
    }

    public static void ListAddOverride(MBList<Hero> _exSpouses, Hero exSpouse, Hero instance)
    {
        // Allows original method call if this thread is allowed
        if (CallOriginalPolicy.IsOriginalAllowed())
        {
            _exSpouses.Add(exSpouse);
            return;
        }

        // Skip method if called from client and allow origin
        if (ModInformation.IsClient)
        {
            Logger.Error("Client added unmanaged item: {callstack}", Environment.StackTrace);
            _exSpouses.Add(exSpouse);
            return;
        }

        MessageBroker.Instance.Publish(instance, new ExSpouseAdded(instance, exSpouse));

        _exSpouses.Add(exSpouse);
    }
}