-
Notifications
You must be signed in to change notification settings - Fork 47
Patching Basics
Garrett Luskey edited this page Mar 12, 2024
·
3 revisions
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
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;
}
}
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);
}
}